@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.
@@ -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
- }, { trackDepth: true });
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.30",
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.2",
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
- { trackDepth: true },
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;