Skip to content

Commit 6bdd74e

Browse files
copiltembeltudor
and
tudor
authored
PostgreSQL case sensitivity in listen (#530)
* case sensitivity et all. --------- Co-authored-by: tudor <tudor@swisstch.com>
1 parent b3640ee commit 6bdd74e

File tree

6 files changed

+184
-19
lines changed

6 files changed

+184
-19
lines changed

.changeset/giant-spiders-beam.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@electric-sql/pglite': patch
3+
---
4+
5+
listen and unlisten case sensitivity behaviour aligned to default PostgreSQL behaviour'

docs/docs/api.md

+2
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ const unsub = await pg.listen('test', (payload) => {
269269
await pg.query("NOTIFY test, 'Hello, world!'")
270270
```
271271

272+
Channel names are case sensitive if double-quoted (`pg.listen('"TeST"')`). Otherwise channel name will be lower cased (`pg.listen('TeStiNG')` == `pg.listen('testing')`).
273+
272274
### unlisten
273275

274276
`.unlisten(channel: string, callback?: (payload: string) => void): Promise<void>`

packages/pglite/src/pglite.ts

+27-12
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ import type {
1919
PGliteOptions,
2020
} from './interface.js'
2121
import PostgresModFactory, { type PostgresMod } from './postgresMod.js'
22-
import { getFsBundle, instantiateWasm, startWasmDownload } from './utils.js'
22+
import {
23+
getFsBundle,
24+
instantiateWasm,
25+
startWasmDownload,
26+
toPostgresName,
27+
} from './utils.js'
2328

2429
// Importing the source as the built version is not ESM compatible
2530
import { Parser as ProtocolParser, serialize } from '@electric-sql/pg-protocol'
@@ -716,13 +721,22 @@ export class PGlite
716721
* @param callback The callback to call when a notification is received
717722
*/
718723
async listen(channel: string, callback: (payload: string) => void) {
719-
if (!this.#notifyListeners.has(channel)) {
720-
this.#notifyListeners.set(channel, new Set())
724+
const pgChannel = toPostgresName(channel)
725+
if (!this.#notifyListeners.has(pgChannel)) {
726+
this.#notifyListeners.set(pgChannel, new Set())
727+
}
728+
this.#notifyListeners.get(pgChannel)!.add(callback)
729+
try {
730+
await this.exec(`LISTEN ${channel}`)
731+
} catch (e) {
732+
this.#notifyListeners.get(pgChannel)!.delete(callback)
733+
if (this.#notifyListeners.get(pgChannel)?.size === 0) {
734+
this.#notifyListeners.delete(pgChannel)
735+
}
736+
throw e
721737
}
722-
this.#notifyListeners.get(channel)!.add(callback)
723-
await this.exec(`LISTEN "${channel}"`)
724738
return async () => {
725-
await this.unlisten(channel, callback)
739+
await this.unlisten(pgChannel, callback)
726740
}
727741
}
728742

@@ -732,15 +746,16 @@ export class PGlite
732746
* @param callback The callback to remove
733747
*/
734748
async unlisten(channel: string, callback?: (payload: string) => void) {
749+
const pgChannel = toPostgresName(channel)
735750
if (callback) {
736-
this.#notifyListeners.get(channel)?.delete(callback)
737-
if (this.#notifyListeners.get(channel)?.size === 0) {
738-
await this.exec(`UNLISTEN "${channel}"`)
739-
this.#notifyListeners.delete(channel)
751+
this.#notifyListeners.get(pgChannel)?.delete(callback)
752+
if (this.#notifyListeners.get(pgChannel)?.size === 0) {
753+
await this.exec(`UNLISTEN ${channel}`)
754+
this.#notifyListeners.delete(pgChannel)
740755
}
741756
} else {
742-
await this.exec(`UNLISTEN "${channel}"`)
743-
this.#notifyListeners.delete(channel)
757+
await this.exec(`UNLISTEN ${channel}`)
758+
this.#notifyListeners.delete(pgChannel)
744759
}
745760
}
746761

packages/pglite/src/utils.ts

+17
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,20 @@ export function debounceMutex<A extends any[], R>(
226226
return promise
227227
}
228228
}
229+
230+
/**
231+
* Postgresql handles quoted names as CaseSensitive and unquoted as lower case.
232+
* If input is quoted, returns an unquoted string (same casing)
233+
* If input is unquoted, returns a lower-case string
234+
*/
235+
export function toPostgresName(input: string): string {
236+
let output
237+
if (input.startsWith('"') && input.endsWith('"')) {
238+
// Postgres sensitive case
239+
output = input.substring(1, input.length - 1)
240+
} else {
241+
// Postgres case insensitive - all to lower
242+
output = input.toLowerCase()
243+
}
244+
return output
245+
}

packages/pglite/src/worker/index.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type {
88
} from '../interface.js'
99
import type { PGlite } from '../pglite.js'
1010
import { BasePGlite } from '../base.js'
11-
import { uuid } from '../utils.js'
11+
import { toPostgresName, uuid } from '../utils.js'
1212

1313
export type PGliteWorkerOptions<E extends Extensions = Extensions> =
1414
PGliteOptions<E> & {
@@ -359,14 +359,15 @@ export class PGliteWorker
359359
channel: string,
360360
callback: (payload: string) => void,
361361
): Promise<() => Promise<void>> {
362-
await this.waitReady
363-
if (!this.#notifyListeners.has(channel)) {
364-
this.#notifyListeners.set(channel, new Set())
362+
const pgChannel = toPostgresName(channel)
363+
364+
if (!this.#notifyListeners.has(pgChannel)) {
365+
this.#notifyListeners.set(pgChannel, new Set())
365366
}
366-
this.#notifyListeners.get(channel)?.add(callback)
367+
this.#notifyListeners.get(pgChannel)!.add(callback)
367368
await this.exec(`LISTEN ${channel}`)
368369
return async () => {
369-
await this.unlisten(channel, callback)
370+
await this.unlisten(pgChannel, callback)
370371
}
371372
}
372373

packages/pglite/tests/notify.test.js

+126-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, it, expect } from 'vitest'
1+
import { describe, it, expect, vi } from 'vitest'
22
import { PGlite } from '../dist/index.js'
3+
import { expectToThrowAsync } from './test-utils.js'
34

45
describe('notify API', () => {
56
it('notify', async () => {
@@ -41,4 +42,128 @@ describe('notify API', () => {
4142

4243
await new Promise((resolve) => setTimeout(resolve, 1000))
4344
})
45+
46+
it('check notify case sensitivity + special chars as Postgresql', async () => {
47+
const pg = new PGlite()
48+
49+
const allLower1 = vi.fn()
50+
await pg.listen('postgresdefaultlower', allLower1)
51+
await pg.query(`NOTIFY postgresdefaultlower, 'payload1'`)
52+
53+
const autoLowerTest1 = vi.fn()
54+
await pg.listen('PostgresDefaultLower', autoLowerTest1)
55+
await pg.query(`NOTIFY PostgresDefaultLower, 'payload1'`)
56+
57+
const autoLowerTest2 = vi.fn()
58+
await pg.listen('PostgresDefaultLower', autoLowerTest2)
59+
await pg.query(`NOTIFY postgresdefaultlower, 'payload1'`)
60+
61+
const autoLowerTest3 = vi.fn()
62+
await pg.listen('postgresdefaultlower', autoLowerTest3)
63+
await pg.query(`NOTIFY PostgresDefaultLower, 'payload1'`)
64+
65+
const caseSensitive1 = vi.fn()
66+
await pg.listen('"tesT2"', caseSensitive1)
67+
await pg.query(`NOTIFY "tesT2", 'paYloAd2'`)
68+
69+
const caseSensitive2 = vi.fn()
70+
await pg.listen('"tesT3"', caseSensitive2)
71+
await pg.query(`NOTIFY tesT3, 'paYloAd2'`)
72+
73+
const caseSensitive3 = vi.fn()
74+
await pg.listen('testNotCalled2', caseSensitive3)
75+
await pg.query(`NOTIFY "testNotCalled2", 'paYloAd2'`)
76+
77+
const quotedWithSpaces = vi.fn()
78+
await pg.listen('"Quoted Channel With Spaces"', quotedWithSpaces)
79+
await pg.query(`NOTIFY "Quoted Channel With Spaces", 'payload1'`)
80+
81+
const unquotedWithSpaces = vi.fn()
82+
await expectToThrowAsync(
83+
pg.listen('Unquoted Channel With Spaces', unquotedWithSpaces),
84+
)
85+
await expectToThrowAsync(
86+
pg.query(`NOTIFY Unquoted Channel With Spaces, 'payload1'`),
87+
)
88+
89+
const otherCharsWithQuotes = vi.fn()
90+
await pg.listen('"test&me"', otherCharsWithQuotes)
91+
await pg.query(`NOTIFY "test&me", 'paYloAd2'`)
92+
93+
const otherChars = vi.fn()
94+
await expectToThrowAsync(pg.listen('test&me', otherChars))
95+
await expectToThrowAsync(pg.query(`NOTIFY test&me, 'payload1'`))
96+
97+
expect(allLower1).toHaveBeenCalledTimes(4)
98+
expect(autoLowerTest1).toHaveBeenCalledTimes(3)
99+
expect(autoLowerTest2).toHaveBeenCalledTimes(2)
100+
expect(autoLowerTest3).toHaveBeenCalledTimes(1)
101+
expect(caseSensitive1).toHaveBeenCalledOnce()
102+
expect(caseSensitive2).not.toHaveBeenCalled()
103+
expect(caseSensitive3).not.toHaveBeenCalled()
104+
expect(otherCharsWithQuotes).toHaveBeenCalledOnce()
105+
expect(quotedWithSpaces).toHaveBeenCalledOnce()
106+
expect(unquotedWithSpaces).not.toHaveBeenCalled()
107+
})
108+
109+
it('check unlisten case sensitivity + special chars as Postgresql', async () => {
110+
const pg = new PGlite()
111+
112+
const allLower1 = vi.fn()
113+
{
114+
const unsub1 = await pg.listen('postgresdefaultlower', allLower1)
115+
await pg.query(`NOTIFY postgresdefaultlower, 'payload1'`)
116+
await unsub1()
117+
}
118+
119+
const autoLowerTest1 = vi.fn()
120+
{
121+
const unsub2 = await pg.listen('PostgresDefaultLower', autoLowerTest1)
122+
await pg.query(`NOTIFY PostgresDefaultLower, 'payload1'`)
123+
await unsub2()
124+
}
125+
126+
const autoLowerTest2 = vi.fn()
127+
{
128+
const unsub3 = await pg.listen('PostgresDefaultLower', autoLowerTest2)
129+
await pg.query(`NOTIFY postgresdefaultlower, 'payload1'`)
130+
await unsub3()
131+
}
132+
133+
const autoLowerTest3 = vi.fn()
134+
{
135+
const unsub4 = await pg.listen('postgresdefaultlower', autoLowerTest3)
136+
await pg.query(`NOTIFY PostgresDefaultLower, 'payload1'`)
137+
await unsub4()
138+
}
139+
140+
const caseSensitive1 = vi.fn()
141+
{
142+
await pg.listen('"CaSESEnsiTIvE"', caseSensitive1)
143+
await pg.query(`NOTIFY "CaSESEnsiTIvE", 'payload1'`)
144+
await pg.unlisten('"CaSESEnsiTIvE"')
145+
await pg.query(`NOTIFY "CaSESEnsiTIvE", 'payload1'`)
146+
}
147+
148+
const quotedWithSpaces = vi.fn()
149+
{
150+
await pg.listen('"Quoted Channel With Spaces"', quotedWithSpaces)
151+
await pg.query(`NOTIFY "Quoted Channel With Spaces", 'payload1'`)
152+
await pg.unlisten('"Quoted Channel With Spaces"')
153+
}
154+
155+
const otherCharsWithQuotes = vi.fn()
156+
{
157+
await pg.listen('"test&me"', otherCharsWithQuotes)
158+
await pg.query(`NOTIFY "test&me", 'payload'`)
159+
await pg.unlisten('"test&me"')
160+
}
161+
162+
expect(allLower1).toHaveBeenCalledOnce()
163+
expect(autoLowerTest1).toHaveBeenCalledOnce()
164+
expect(autoLowerTest2).toHaveBeenCalledOnce()
165+
expect(autoLowerTest3).toHaveBeenCalledOnce()
166+
expect(caseSensitive1).toHaveBeenCalledOnce()
167+
expect(otherCharsWithQuotes).toHaveBeenCalledOnce()
168+
})
44169
})

0 commit comments

Comments
 (0)