Skip to content

Commit e98ce80

Browse files
committed
fix: quote column names in json_to_recordset AS clause
Column names in the json_to_recordset AS clause were unquoted, causing PostgreSQL to lowercase them. This meant camelCase JSON keys didn't match the lowercased column names, producing NULLs. The insert and csv/COPY methods already properly double-quote column names; this aligns the json method with them.
1 parent ff2802b commit e98ce80

2 files changed

Lines changed: 93 additions & 1 deletion

File tree

packages/pglite-sync/src/apply.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ export async function applyMessagesToTableWithJson({
271271
SELECT x.* from json_to_recordset($1) as x(${columns
272272
.map(
273273
(x) =>
274-
`${x.column_name} ${x.udt_name.replace(/^_/, '')}` +
274+
`"${x.column_name}" ${x.udt_name.replace(/^_/, '')}` +
275275
(x.data_type === 'ARRAY' ? `[]` : ''),
276276
)
277277
.join(', ')})

packages/pglite-sync/test/sync.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,4 +1670,96 @@ describe('pglite-sync', () => {
16701670

16711671
shape.unsubscribe()
16721672
})
1673+
1674+
it('handles camelCase column names with json_to_recordset', async () => {
1675+
await pg.exec(`
1676+
CREATE TABLE IF NOT EXISTS camel_test (
1677+
id SERIAL PRIMARY KEY,
1678+
"firstName" TEXT,
1679+
"lastName" TEXT
1680+
);
1681+
`)
1682+
await pg.exec(`TRUNCATE camel_test;`)
1683+
1684+
let feedMessages: (messages: MultiShapeMessage[]) => Promise<void> =
1685+
async (_) => {}
1686+
MockMultiShapeStream.mockImplementation(() => ({
1687+
subscribe: vi.fn(
1688+
(cb: (messages: MultiShapeMessage[]) => Promise<void>) => {
1689+
feedMessages = (messages) =>
1690+
cb([
1691+
...messages,
1692+
{
1693+
shape: 'shape',
1694+
headers: {
1695+
control: 'up-to-date',
1696+
global_last_seen_lsn: '0',
1697+
},
1698+
},
1699+
])
1700+
},
1701+
),
1702+
unsubscribeAll: vi.fn(),
1703+
isUpToDate: true,
1704+
shapes: {
1705+
shape: {
1706+
subscribe: vi.fn(),
1707+
unsubscribeAll: vi.fn(),
1708+
},
1709+
},
1710+
}))
1711+
1712+
const shape = await pg.electric.syncShapeToTable({
1713+
shape: {
1714+
url: 'http://localhost:3000/v1/shape',
1715+
params: { table: 'camel_test' },
1716+
},
1717+
table: 'camel_test',
1718+
primaryKey: ['id'],
1719+
initialInsertMethod: 'json',
1720+
shapeKey: null,
1721+
})
1722+
1723+
const messages: MultiShapeMessage[] = [
1724+
{
1725+
headers: { operation: 'insert' as const },
1726+
key: 'id1',
1727+
value: {
1728+
id: 1,
1729+
firstName: 'Alice',
1730+
lastName: 'Smith',
1731+
},
1732+
shape: 'shape',
1733+
},
1734+
{
1735+
headers: { operation: 'insert' as const },
1736+
key: 'id2',
1737+
value: {
1738+
id: 2,
1739+
firstName: 'Bob',
1740+
lastName: 'Jones',
1741+
},
1742+
shape: 'shape',
1743+
},
1744+
]
1745+
1746+
await feedMessages(messages)
1747+
1748+
await vi.waitUntil(async () => {
1749+
const result = await pg.sql<{ count: number }>`
1750+
SELECT COUNT(*) as count FROM camel_test;
1751+
`
1752+
return result.rows[0].count === 2
1753+
})
1754+
1755+
const result = await pg.sql`
1756+
SELECT * FROM camel_test ORDER BY id;
1757+
`
1758+
expect(result.rows).toEqual([
1759+
{ id: 1, firstName: 'Alice', lastName: 'Smith' },
1760+
{ id: 2, firstName: 'Bob', lastName: 'Jones' },
1761+
])
1762+
1763+
shape.unsubscribe()
1764+
})
16731765
})

0 commit comments

Comments
 (0)