@supabase/pg-delta 1.0.0-alpha.30 → 1.0.0-alpha.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/plan/sql-format/sql-scanner.d.ts +10 -0
- package/dist/core/plan/sql-format/sql-scanner.js +9 -0
- package/dist/core/plan/sql-format/tokenizer.js +20 -1
- package/package.json +2 -2
- package/src/core/plan/sql-format/format-trigger-quoted-name.test.ts +36 -0
- package/src/core/plan/sql-format/sql-scanner.ts +19 -0
- package/src/core/plan/sql-format/tokenizer.test.ts +16 -0
- package/src/core/plan/sql-format/tokenizer.ts +20 -1
|
@@ -27,6 +27,16 @@ type WalkSqlOptions = {
|
|
|
27
27
|
* called once with the full sequence.
|
|
28
28
|
*/
|
|
29
29
|
onSkipped?: (chunk: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Called once for each top-level double-quoted identifier, after its closing
|
|
32
|
+
* quote, with the index of the opening quote (`start`), the index just past
|
|
33
|
+
* the closing quote (`end`), and the parenthesis depth at the quote.
|
|
34
|
+
*
|
|
35
|
+
* Lets callers treat a quoted identifier (e.g. `"my-trigger"`) as an atomic
|
|
36
|
+
* token even though its interior is reported via `onSkipped`. Returning
|
|
37
|
+
* `false` stops the walk early, like `onTopLevel`.
|
|
38
|
+
*/
|
|
39
|
+
onQuotedIdentifier?: (start: number, end: number, depth: number) => boolean;
|
|
30
40
|
};
|
|
31
41
|
/**
|
|
32
42
|
* Fast character-code check: A-Z, a-z, 0-9, _
|
|
@@ -59,9 +59,11 @@ export function walkSql(text, onTopLevel, options) {
|
|
|
59
59
|
const trackDepth = options?.trackDepth ?? false;
|
|
60
60
|
const startIndex = options?.startIndex ?? 0;
|
|
61
61
|
const onSkipped = options?.onSkipped;
|
|
62
|
+
const onQuotedIdentifier = options?.onQuotedIdentifier;
|
|
62
63
|
let inSingleQuote = false;
|
|
63
64
|
let singleQuoteEscapeMode = false;
|
|
64
65
|
let inDoubleQuote = false;
|
|
66
|
+
let doubleQuoteStart = -1;
|
|
65
67
|
let inLineComment = false;
|
|
66
68
|
let inBlockComment = false;
|
|
67
69
|
let dollarTag = null;
|
|
@@ -137,6 +139,12 @@ export function walkSql(text, onTopLevel, options) {
|
|
|
137
139
|
continue;
|
|
138
140
|
}
|
|
139
141
|
inDoubleQuote = false;
|
|
142
|
+
onSkipped?.(char);
|
|
143
|
+
if (onQuotedIdentifier?.(doubleQuoteStart, i + 1, depth) === false) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
i += 1;
|
|
147
|
+
continue;
|
|
140
148
|
}
|
|
141
149
|
onSkipped?.(char);
|
|
142
150
|
i += 1;
|
|
@@ -164,6 +172,7 @@ export function walkSql(text, onTopLevel, options) {
|
|
|
164
172
|
}
|
|
165
173
|
if (char === '"') {
|
|
166
174
|
inDoubleQuote = true;
|
|
175
|
+
doubleQuoteStart = i;
|
|
167
176
|
onSkipped?.(char);
|
|
168
177
|
i += 1;
|
|
169
178
|
continue;
|
|
@@ -23,7 +23,26 @@ export function scanTokens(statement) {
|
|
|
23
23
|
skipUntil = end;
|
|
24
24
|
}
|
|
25
25
|
return true;
|
|
26
|
-
}, {
|
|
26
|
+
}, {
|
|
27
|
+
trackDepth: true,
|
|
28
|
+
// Double-quoted identifiers (e.g. `"my-trigger"`) are atomic tokens too.
|
|
29
|
+
// Without this, the walker skips their interior entirely and positional
|
|
30
|
+
// token logic (e.g. "the name follows the TRIGGER keyword") lands on the
|
|
31
|
+
// wrong token. The value keeps the surrounding quotes; a quoted
|
|
32
|
+
// identifier never matches a keyword, which is correct.
|
|
33
|
+
onQuotedIdentifier: (start, end, depth) => {
|
|
34
|
+
const value = statement.slice(start, end);
|
|
35
|
+
tokens.push({
|
|
36
|
+
value,
|
|
37
|
+
upper: value.toUpperCase(),
|
|
38
|
+
start,
|
|
39
|
+
end,
|
|
40
|
+
depth,
|
|
41
|
+
});
|
|
42
|
+
skipUntil = end;
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
27
46
|
return tokens;
|
|
28
47
|
}
|
|
29
48
|
export function findTopLevelParen(statement, startIndex) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/pg-delta",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.31",
|
|
4
4
|
"description": "PostgreSQL migrations made easy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"diff",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@stricli/core": "^1.2.4",
|
|
82
|
-
"@supabase/pg-topo": "^1.0.0-alpha.
|
|
82
|
+
"@supabase/pg-topo": "^1.0.0-alpha.3",
|
|
83
83
|
"@ts-safeql/sql-tag": "^0.2.0",
|
|
84
84
|
"chalk": "^5.6.2",
|
|
85
85
|
"debug": "^4.3.7",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { formatSqlStatements } from "./index.ts";
|
|
3
|
+
|
|
4
|
+
describe("formatCreateTrigger with quoted (dashed) names", () => {
|
|
5
|
+
// Regression: CLI-1820. A trigger whose name contains characters that force
|
|
6
|
+
// PostgreSQL to double-quote it (e.g. a dash) used to lose its event/table
|
|
7
|
+
// clause ("AFTER INSERT ON public.t1"). The tokenizer emitted no token for
|
|
8
|
+
// the quoted identifier, so the formatter mistook the next keyword for the
|
|
9
|
+
// name and sliced away everything before the first recognized clause.
|
|
10
|
+
it("preserves the event/table clause for a dashed trigger name", () => {
|
|
11
|
+
const sql = `CREATE TRIGGER "new-webhook-with-dashed-name" AFTER INSERT ON public.t1 FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://example.com/x', 'POST', '{}', '{}', '5000')`;
|
|
12
|
+
|
|
13
|
+
const [formatted] = formatSqlStatements([sql], { keywordCase: "upper" });
|
|
14
|
+
|
|
15
|
+
expect(formatted).toContain("AFTER INSERT ON public.t1");
|
|
16
|
+
expect(formatted).toMatchInlineSnapshot(`
|
|
17
|
+
"CREATE TRIGGER "new-webhook-with-dashed-name"
|
|
18
|
+
AFTER INSERT ON public.t1
|
|
19
|
+
FOR EACH ROW
|
|
20
|
+
EXECUTE FUNCTION
|
|
21
|
+
supabase_functions.http_request('https://example.com/x', 'POST', '{}', '{}', '5000')"
|
|
22
|
+
`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("formats a dashed trigger name the same way as an unquoted one", () => {
|
|
26
|
+
const dashed = `CREATE TRIGGER "send-chat-push" AFTER INSERT ON public.chat_message FOR EACH ROW EXECUTE FUNCTION public.notify()`;
|
|
27
|
+
const plain = `CREATE TRIGGER send_chat_push AFTER INSERT ON public.chat_message FOR EACH ROW EXECUTE FUNCTION public.notify()`;
|
|
28
|
+
|
|
29
|
+
const [dashedOut] = formatSqlStatements([dashed], { keywordCase: "upper" });
|
|
30
|
+
const [plainOut] = formatSqlStatements([plain], { keywordCase: "upper" });
|
|
31
|
+
|
|
32
|
+
expect(dashedOut.replace('"send-chat-push"', "send_chat_push")).toBe(
|
|
33
|
+
plainOut,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -29,6 +29,16 @@ type WalkSqlOptions = {
|
|
|
29
29
|
* called once with the full sequence.
|
|
30
30
|
*/
|
|
31
31
|
onSkipped?: (chunk: string) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Called once for each top-level double-quoted identifier, after its closing
|
|
34
|
+
* quote, with the index of the opening quote (`start`), the index just past
|
|
35
|
+
* the closing quote (`end`), and the parenthesis depth at the quote.
|
|
36
|
+
*
|
|
37
|
+
* Lets callers treat a quoted identifier (e.g. `"my-trigger"`) as an atomic
|
|
38
|
+
* token even though its interior is reported via `onSkipped`. Returning
|
|
39
|
+
* `false` stops the walk early, like `onTopLevel`.
|
|
40
|
+
*/
|
|
41
|
+
onQuotedIdentifier?: (start: number, end: number, depth: number) => boolean;
|
|
32
42
|
};
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -104,10 +114,12 @@ export function walkSql(
|
|
|
104
114
|
const trackDepth = options?.trackDepth ?? false;
|
|
105
115
|
const startIndex = options?.startIndex ?? 0;
|
|
106
116
|
const onSkipped = options?.onSkipped;
|
|
117
|
+
const onQuotedIdentifier = options?.onQuotedIdentifier;
|
|
107
118
|
|
|
108
119
|
let inSingleQuote = false;
|
|
109
120
|
let singleQuoteEscapeMode = false;
|
|
110
121
|
let inDoubleQuote = false;
|
|
122
|
+
let doubleQuoteStart = -1;
|
|
111
123
|
let inLineComment = false;
|
|
112
124
|
let inBlockComment = false;
|
|
113
125
|
let dollarTag: string | null = null;
|
|
@@ -187,6 +199,12 @@ export function walkSql(
|
|
|
187
199
|
continue;
|
|
188
200
|
}
|
|
189
201
|
inDoubleQuote = false;
|
|
202
|
+
onSkipped?.(char);
|
|
203
|
+
if (onQuotedIdentifier?.(doubleQuoteStart, i + 1, depth) === false) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
i += 1;
|
|
207
|
+
continue;
|
|
190
208
|
}
|
|
191
209
|
onSkipped?.(char);
|
|
192
210
|
i += 1;
|
|
@@ -215,6 +233,7 @@ export function walkSql(
|
|
|
215
233
|
}
|
|
216
234
|
if (char === '"') {
|
|
217
235
|
inDoubleQuote = true;
|
|
236
|
+
doubleQuoteStart = i;
|
|
218
237
|
onSkipped?.(char);
|
|
219
238
|
i += 1;
|
|
220
239
|
continue;
|
|
@@ -21,6 +21,22 @@ describe("scanTokens", () => {
|
|
|
21
21
|
expect(inner[1].depth).toBe(1);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
it("emits a single token for a double-quoted identifier", () => {
|
|
25
|
+
const tokens = scanTokens('CREATE TRIGGER "send-chat-push" AFTER');
|
|
26
|
+
expect(tokens).toEqual([
|
|
27
|
+
{ value: "CREATE", upper: "CREATE", start: 0, end: 6, depth: 0 },
|
|
28
|
+
{ value: "TRIGGER", upper: "TRIGGER", start: 7, end: 14, depth: 0 },
|
|
29
|
+
{
|
|
30
|
+
value: '"send-chat-push"',
|
|
31
|
+
upper: '"SEND-CHAT-PUSH"',
|
|
32
|
+
start: 15,
|
|
33
|
+
end: 31,
|
|
34
|
+
depth: 0,
|
|
35
|
+
},
|
|
36
|
+
{ value: "AFTER", upper: "AFTER", start: 32, end: 37, depth: 0 },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
24
40
|
it("ignores content in quotes, comments, and dollar-quotes", () => {
|
|
25
41
|
const tokens = scanTokens("SELECT 'hello' -- comment\n FROM $$body$$ tbl");
|
|
26
42
|
const uppers = tokens.map((t) => t.upper);
|
|
@@ -27,7 +27,26 @@ export function scanTokens(statement: string): Token[] {
|
|
|
27
27
|
}
|
|
28
28
|
return true;
|
|
29
29
|
},
|
|
30
|
-
{
|
|
30
|
+
{
|
|
31
|
+
trackDepth: true,
|
|
32
|
+
// Double-quoted identifiers (e.g. `"my-trigger"`) are atomic tokens too.
|
|
33
|
+
// Without this, the walker skips their interior entirely and positional
|
|
34
|
+
// token logic (e.g. "the name follows the TRIGGER keyword") lands on the
|
|
35
|
+
// wrong token. The value keeps the surrounding quotes; a quoted
|
|
36
|
+
// identifier never matches a keyword, which is correct.
|
|
37
|
+
onQuotedIdentifier: (start, end, depth) => {
|
|
38
|
+
const value = statement.slice(start, end);
|
|
39
|
+
tokens.push({
|
|
40
|
+
value,
|
|
41
|
+
upper: value.toUpperCase(),
|
|
42
|
+
start,
|
|
43
|
+
end,
|
|
44
|
+
depth,
|
|
45
|
+
});
|
|
46
|
+
skipUntil = end;
|
|
47
|
+
return true;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
31
50
|
);
|
|
32
51
|
|
|
33
52
|
return tokens;
|