Skip to content

Commit 19d1a09

Browse files
fix(table): drop sql.raw quote-escaping in column-name interpolation
Six call sites in lib/table/service.ts built JSON-key string literals at runtime via `sql.raw(\`'\${name.replace(/'/g, "''")}'\`)` for use with PostgreSQL's `data->'key'` / `data->>'key'` operators. Practically safe (NAME_PATTERN gates column names to alphanumeric+underscore at insert time) but a smelly pattern that breaks the moment validation loosens. Both `data->` and `data->>` accept a parameterized text value as the key, so the `sql.raw` is unnecessary. Replace each with a normal `${name}::text` binding. No behavior change; eliminates the manual quote-escaping surface. Affected sites: renameColumn (the data-rewrite UPDATE), upsertRow's match filter, updateColumnType's IS-NOT-NULL gate, updateColumnConstraints' required-check + unique-duplicate-check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c07041b commit 19d1a09

1 file changed

Lines changed: 10 additions & 16 deletions

File tree

apps/sim/lib/table/service.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,16 +1303,12 @@ export async function upsertRow(
13031303
throw new Error(`Schema validation failed: ${schemaValidation.errors.join(', ')}`)
13041304
}
13051305

1306-
// Validate column name before raw interpolation (defense-in-depth)
1307-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(targetColumnName)) {
1308-
throw new Error(`Invalid column name: ${targetColumnName}`)
1309-
}
1310-
1311-
// Build the single-column match filter
1306+
// `data->` and `data->>` accept the JSON key as a parameterized text value;
1307+
// no need for `sql.raw` interpolation.
13121308
const matchFilter =
13131309
typeof targetValue === 'string'
1314-
? sql`${userTableRows.data}->>${sql.raw(`'${targetColumnName}'`)} = ${String(targetValue)}`
1315-
: sql`(${userTableRows.data}->${sql.raw(`'${targetColumnName}'`)})::jsonb = ${JSON.stringify(targetValue)}::jsonb`
1310+
? sql`${userTableRows.data}->>${targetColumnName}::text = ${String(targetValue)}`
1311+
: sql`(${userTableRows.data}->${targetColumnName}::text)::jsonb = ${JSON.stringify(targetValue)}::jsonb`
13161312

13171313
// Capacity enforcement for the insert path lives in the `increment_user_table_row_count`
13181314
// trigger (migration 0198). The update path doesn't change row_count, so no check needed.
@@ -2295,8 +2291,10 @@ export async function renameColumn(
22952291
.set({ schema: updatedSchema, metadata: updatedMetadata, updatedAt: now })
22962292
.where(eq(userTableDefinitions.id, data.tableId))
22972293

2294+
// All bindings parameterized — `data->` accepts a text parameter for the
2295+
// key, no need to drop into `sql.raw` with hand-rolled quote escaping.
22982296
await trx.execute(
2299-
sql`UPDATE user_table_rows SET data = data - ${actualOldName}::text || jsonb_build_object(${data.newName}::text, data->${sql.raw(`'${actualOldName.replace(/'/g, "''")}'`)}) WHERE table_id = ${data.tableId} AND data ? ${actualOldName}::text`
2297+
sql`UPDATE user_table_rows SET data = data - ${actualOldName}::text || jsonb_build_object(${data.newName}::text, data->${actualOldName}::text) WHERE table_id = ${data.tableId} AND data ? ${actualOldName}::text`
23002298
)
23012299
})
23022300

@@ -2566,8 +2564,6 @@ export async function updateColumnType(
25662564
return table
25672565
}
25682566

2569-
const escapedName = column.name.replace(/'/g, "''")
2570-
25712567
// Validate existing data is compatible with the new type
25722568
const rows = await db
25732569
.select({ id: userTableRows.id, data: userTableRows.data })
@@ -2576,7 +2572,7 @@ export async function updateColumnType(
25762572
and(
25772573
eq(userTableRows.tableId, data.tableId),
25782574
sql`${userTableRows.data} ? ${column.name}`,
2579-
sql`${userTableRows.data}->>${sql.raw(`'${escapedName}'`)} IS NOT NULL`
2575+
sql`${userTableRows.data}->>${column.name}::text IS NOT NULL`
25802576
)
25812577
)
25822578

@@ -2646,16 +2642,14 @@ export async function updateColumnConstraints(
26462642
`Cannot change constraints on workflow-output column "${column.name}". Constraints aren't applicable to columns whose values come from workflow execution.`
26472643
)
26482644
}
2649-
const escapedName = column.name.replace(/'/g, "''")
2650-
26512645
if (data.required === true && !column.required) {
26522646
const [result] = await db
26532647
.select({ count: count() })
26542648
.from(userTableRows)
26552649
.where(
26562650
and(
26572651
eq(userTableRows.tableId, data.tableId),
2658-
sql`(NOT (${userTableRows.data} ? ${column.name}) OR ${userTableRows.data}->>${sql.raw(`'${escapedName}'`)} IS NULL)`
2652+
sql`(NOT (${userTableRows.data} ? ${column.name}) OR ${userTableRows.data}->>${column.name}::text IS NULL)`
26592653
)
26602654
)
26612655

@@ -2668,7 +2662,7 @@ export async function updateColumnConstraints(
26682662

26692663
if (data.unique === true && !column.unique) {
26702664
const duplicates = (await db.execute(
2671-
sql`SELECT ${userTableRows.data}->>${sql.raw(`'${escapedName}'`)} AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${data.tableId} AND ${userTableRows.data} ? ${column.name} AND ${userTableRows.data}->>${sql.raw(`'${escapedName}'`)} IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1`
2665+
sql`SELECT ${userTableRows.data}->>${column.name}::text AS val, count(*) AS cnt FROM ${userTableRows} WHERE table_id = ${data.tableId} AND ${userTableRows.data} ? ${column.name} AND ${userTableRows.data}->>${column.name}::text IS NOT NULL GROUP BY val HAVING count(*) > 1 LIMIT 1`
26722666
)) as { val: string; cnt: number }[]
26732667

26742668
if (duplicates.length > 0) {

0 commit comments

Comments
 (0)