Skip to content

Commit 41cebe9

Browse files
committed
Reject table names with empty segments in pglite
quoteIdentifier() treated every dot as a schema separator and quoted whatever surrounded it, so an empty table name became "" and names like "schema..table" or ".table" produced empty identifiers such as "schema".""."table". Quote each dot-separated segment individually and throw a TypeError when any segment is empty. PgliteKvStore now quotes the table name once in its constructor, so an invalid name fails at construction time instead of at the first query. #1020 (comment) Assisted-by: Claude Code:claude-fable-5
1 parent 6037eba commit 41cebe9

2 files changed

Lines changed: 35 additions & 12 deletions

File tree

‎packages/pglite/src/kv.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ test("PgliteKvStore.drop() is idempotent", async () => {
6464
}
6565
});
6666

67+
test("PgliteKvStore rejects table names with empty segments", () => {
68+
const pg = {} as unknown as PGlite;
69+
for (const tableName of ["", ".", "schema..table", ".table", "table."]) {
70+
assert.throws(
71+
() => new PgliteKvStore(pg, { tableName }),
72+
TypeError,
73+
`tableName: ${JSON.stringify(tableName)}`,
74+
);
75+
}
76+
});
77+
6778
test("PgliteKvStore supports qualified mixed-case table names", async () => {
6879
const pg = new PGlite();
6980
const suffix = crypto.randomUUID().replaceAll("-", "");

‎packages/pglite/src/kv.ts‎

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,15 @@ import { getLogger } from "@logtape/logtape";
1212
const logger = getLogger(["fedify", "pglite", "kv"]);
1313

1414
function quoteIdentifier(identifier: string): string {
15-
return `"${identifier.replaceAll('"', '""').replaceAll(".", '"."')}"`;
15+
const parts = identifier.split(".");
16+
if (parts.includes("")) {
17+
throw new TypeError(
18+
`Invalid table name for the key–value store: ${
19+
JSON.stringify(identifier)
20+
}`,
21+
);
22+
}
23+
return parts.map((part) => `"${part.replaceAll('"', '""')}"`).join(".");
1624
}
1725

1826
function serializeJson(value: unknown): string {
@@ -29,6 +37,8 @@ function serializeJson(value: unknown): string {
2937
export interface PgliteKvStoreOptions {
3038
/**
3139
* The table name to use for the key–value store.
40+
* A `.` separates the schema from the table name, so neither the whole
41+
* name nor any segment between dots can be empty.
3242
* `"fedify_kv_v2"` by default.
3343
* @default `"fedify_kv_v2"`
3444
*/
@@ -68,6 +78,7 @@ export interface PgliteKvStoreOptions {
6878
export class PgliteKvStore implements KvStore {
6979
readonly #pg: PGliteInterface;
7080
readonly #tableName: string;
81+
readonly #quotedTableName: string;
7182
#initialized: boolean;
7283
#initializing?: Promise<void>;
7384

@@ -82,12 +93,13 @@ export class PgliteKvStore implements KvStore {
8293
) {
8394
this.#pg = pg;
8495
this.#tableName = options.tableName ?? "fedify_kv_v2";
96+
this.#quotedTableName = quoteIdentifier(this.#tableName);
8597
this.#initialized = options.initialized ?? false;
8698
}
8799

88100
async #expire(): Promise<void> {
89101
await this.#pg.query(`
90-
DELETE FROM ${quoteIdentifier(this.#tableName)}
102+
DELETE FROM ${this.#quotedTableName}
91103
WHERE ttl IS NOT NULL AND created + ttl < CURRENT_TIMESTAMP;
92104
`);
93105
}
@@ -97,7 +109,7 @@ export class PgliteKvStore implements KvStore {
97109
const result = await this.#pg.query<{ value: T }>(
98110
`
99111
SELECT value
100-
FROM ${quoteIdentifier(this.#tableName)}
112+
FROM ${this.#quotedTableName}
101113
WHERE key = $1::text[]
102114
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP);
103115
`,
@@ -117,7 +129,7 @@ export class PgliteKvStore implements KvStore {
117129
: Temporal.Duration.from(options.ttl).toString();
118130
await this.#pg.query(
119131
`
120-
INSERT INTO ${quoteIdentifier(this.#tableName)} (key, value, ttl)
132+
INSERT INTO ${this.#quotedTableName} (key, value, ttl)
121133
VALUES ($1::text[], $2::text::jsonb, $3::text::interval)
122134
ON CONFLICT (key)
123135
DO UPDATE SET
@@ -134,7 +146,7 @@ export class PgliteKvStore implements KvStore {
134146
await this.initialize();
135147
await this.#pg.query(
136148
`
137-
DELETE FROM ${quoteIdentifier(this.#tableName)}
149+
DELETE FROM ${this.#quotedTableName}
138150
WHERE key = $1::text[];
139151
`,
140152
[key],
@@ -164,7 +176,7 @@ export class PgliteKvStore implements KvStore {
164176
if (expectedValue === undefined) {
165177
const result = await this.#pg.query(
166178
`
167-
INSERT INTO ${quoteIdentifier(this.#tableName)} AS existing
179+
INSERT INTO ${this.#quotedTableName} AS existing
168180
(key, value, created, ttl)
169181
VALUES (
170182
$1::text[],
@@ -187,7 +199,7 @@ export class PgliteKvStore implements KvStore {
187199
} else if (newValue === undefined) {
188200
const result = await this.#pg.query(
189201
`
190-
DELETE FROM ${quoteIdentifier(this.#tableName)}
202+
DELETE FROM ${this.#quotedTableName}
191203
WHERE key = $1::text[]
192204
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
193205
AND value = $2::text::jsonb
@@ -199,7 +211,7 @@ export class PgliteKvStore implements KvStore {
199211
} else {
200212
const result = await this.#pg.query(
201213
`
202-
UPDATE ${quoteIdentifier(this.#tableName)}
214+
UPDATE ${this.#quotedTableName}
203215
SET
204216
value = $3::text::jsonb,
205217
created = CURRENT_TIMESTAMP,
@@ -230,14 +242,14 @@ export class PgliteKvStore implements KvStore {
230242
const result = prefix == null || prefix.length === 0
231243
? await this.#pg.query<{ key: KvKey; value: unknown }>(`
232244
SELECT key, value
233-
FROM ${quoteIdentifier(this.#tableName)}
245+
FROM ${this.#quotedTableName}
234246
WHERE ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP
235247
ORDER BY key;
236248
`)
237249
: await this.#pg.query<{ key: KvKey; value: unknown }>(
238250
`
239251
SELECT key, value
240-
FROM ${quoteIdentifier(this.#tableName)}
252+
FROM ${this.#quotedTableName}
241253
WHERE array_length(key, 1) >= $1
242254
AND key[1:$1] = $2::text[]
243255
AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP)
@@ -260,7 +272,7 @@ export class PgliteKvStore implements KvStore {
260272
tableName: this.#tableName,
261273
});
262274
await this.#pg.query(`
263-
CREATE TABLE IF NOT EXISTS ${quoteIdentifier(this.#tableName)} (
275+
CREATE TABLE IF NOT EXISTS ${this.#quotedTableName} (
264276
key text[] PRIMARY KEY,
265277
value jsonb NOT NULL,
266278
created timestamp with time zone DEFAULT CURRENT_TIMESTAMP,
@@ -286,7 +298,7 @@ export class PgliteKvStore implements KvStore {
286298
async drop(): Promise<void> {
287299
await this.#pg.waitReady;
288300
await this.#pg.query(
289-
`DROP TABLE IF EXISTS ${quoteIdentifier(this.#tableName)};`,
301+
`DROP TABLE IF EXISTS ${this.#quotedTableName};`,
290302
);
291303
this.#initialized = false;
292304
this.#initializing = undefined;

0 commit comments

Comments
 (0)