From 50bc2eedb46a40e2b9ee351408f805673652f8e4 Mon Sep 17 00:00:00 2001 From: MatheusMartinho Date: Sun, 16 Aug 2026 13:58:25 -0300 Subject: [PATCH 1/4] [wrangler] Recognise compound statement markers without surrounding whitespace The D1 SQL splitter only treated BEGIN, CASE and END as compound statement markers when they were surrounded by whitespace. SQLite also accepts them delimited by punctuation, so a trigger body ending in `INSERT ...;END;` never terminated and every following statement was swallowed into it and sent as one statement, and a trigger declared with `WHEN (1=1)BEGIN` was split in the middle of its body. Match the markers when they are delimited by any non-identifier character, keeping identifiers that merely end in a keyword, such as a `weekend` table, from matching. --- .changeset/olive-donuts-battle.md | 9 ++++ .../src/__tests__/d1/splitter.test.ts | 51 +++++++++++++++++++ packages/wrangler/src/d1/splitter.ts | 13 ++++- 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 .changeset/olive-donuts-battle.md diff --git a/.changeset/olive-donuts-battle.md b/.changeset/olive-donuts-battle.md new file mode 100644 index 00000000000..7f84a46c774 --- /dev/null +++ b/.changeset/olive-donuts-battle.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +fix: recognise compound statement markers that are not padded with whitespace + +`wrangler d1 execute --file` and `wrangler d1 migrations apply` split a SQL file into statements before sending them to D1. The splitter only recognised `BEGIN`, `CASE` and `END` when they were surrounded by whitespace, so SQL that SQLite accepts, such as a trigger body ending in `INSERT ...;END;` or a trigger declared with `WHEN (1=1)BEGIN`, was split incorrectly: statements after the trigger were swallowed into it and silently sent as a single statement. + +Markers are now matched when delimited by punctuation as well, while identifiers that merely end in a keyword, such as a `weekend` table, are still left alone. diff --git a/packages/wrangler/src/__tests__/d1/splitter.test.ts b/packages/wrangler/src/__tests__/d1/splitter.test.ts index 60e0e3866b5..cefa7e60e9f 100644 --- a/packages/wrangler/src/__tests__/d1/splitter.test.ts +++ b/packages/wrangler/src/__tests__/d1/splitter.test.ts @@ -277,6 +277,57 @@ describe("splitSqlQuery()", () => { `); }); + it("should end a compound statement when END follows a semicolon", ({ + expect, + }) => { + expect( + splitSqlQuery(` + CREATE TRIGGER audit_trigger AFTER INSERT ON items + BEGIN + INSERT INTO audit (item_id) VALUES (new.id);END; + INSERT INTO items (id) VALUES (1); + SELECT * FROM items;`) + ).toEqual([ + `CREATE TRIGGER audit_trigger AFTER INSERT ON items + BEGIN + INSERT INTO audit (item_id) VALUES (new.id);END`, + "INSERT INTO items (id) VALUES (1)", + "SELECT * FROM items", + ]); + }); + + it("should start a compound statement when BEGIN follows a parenthesis", ({ + expect, + }) => { + expect( + splitSqlQuery(` + CREATE TRIGGER audit_trigger AFTER INSERT ON items FOR EACH ROW WHEN (1=1)BEGIN + INSERT INTO audit (item_id) VALUES (new.id); + END; + SELECT * FROM items;`) + ).toEqual([ + `CREATE TRIGGER audit_trigger AFTER INSERT ON items FOR EACH ROW WHEN (1=1)BEGIN + INSERT INTO audit (item_id) VALUES (new.id); + END`, + "SELECT * FROM items", + ]); + }); + + it("should not treat an identifier ending in END as a compound statement end", ({ + expect, + }) => { + expect( + splitSqlQuery(` + CREATE TABLE weekend (id INTEGER PRIMARY KEY); + INSERT INTO weekend (id) VALUES (1); + SELECT * FROM weekend;`) + ).toEqual([ + "CREATE TABLE weekend (id INTEGER PRIMARY KEY)", + "INSERT INTO weekend (id) VALUES (1)", + "SELECT * FROM weekend", + ]); + }); + it("should handle compound statements for BEGINs", ({ expect }) => { expect( splitSqlQuery(` diff --git a/packages/wrangler/src/d1/splitter.ts b/packages/wrangler/src/d1/splitter.ts index 026d3aad620..9f98778d89b 100644 --- a/packages/wrangler/src/d1/splitter.ts +++ b/packages/wrangler/src/d1/splitter.ts @@ -247,16 +247,25 @@ function isDollarQuoteIdentifier(str: string) { ); } +/** + * Compound statement markers only need to be delimited from surrounding + * identifiers, not padded with whitespace: SQLite accepts `WHEN (1=1)BEGIN` and + * `INSERT ...;END;`. The lookbehind keeps identifiers that merely end in the + * keyword, such as a `weekend` column, from matching. + */ +const COMPOUND_STATEMENT_START = /(? Date: Sun, 16 Aug 2026 14:09:14 -0300 Subject: [PATCH 2/4] [wrangler] Match compound statement END before any non-identifier character Relaxing only the character before the marker made a parenthesised CASE expression, such as SUM(CASE WHEN a THEN 1 ELSE 0 END), open a compound statement that END) never closed, so the rest of the file was sent as a single statement. Accept any non-identifier character after END too, and cover the case in the tests. --- .changeset/olive-donuts-battle.md | 2 +- .../wrangler/src/__tests__/d1/splitter.test.ts | 15 +++++++++++++++ packages/wrangler/src/d1/splitter.ts | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.changeset/olive-donuts-battle.md b/.changeset/olive-donuts-battle.md index 7f84a46c774..45c6d5a3737 100644 --- a/.changeset/olive-donuts-battle.md +++ b/.changeset/olive-donuts-battle.md @@ -2,7 +2,7 @@ "wrangler": patch --- -fix: recognise compound statement markers that are not padded with whitespace +Recognise compound statement markers that are not padded with whitespace `wrangler d1 execute --file` and `wrangler d1 migrations apply` split a SQL file into statements before sending them to D1. The splitter only recognised `BEGIN`, `CASE` and `END` when they were surrounded by whitespace, so SQL that SQLite accepts, such as a trigger body ending in `INSERT ...;END;` or a trigger declared with `WHEN (1=1)BEGIN`, was split incorrectly: statements after the trigger were swallowed into it and silently sent as a single statement. diff --git a/packages/wrangler/src/__tests__/d1/splitter.test.ts b/packages/wrangler/src/__tests__/d1/splitter.test.ts index cefa7e60e9f..c3c23116833 100644 --- a/packages/wrangler/src/__tests__/d1/splitter.test.ts +++ b/packages/wrangler/src/__tests__/d1/splitter.test.ts @@ -313,6 +313,21 @@ describe("splitSqlQuery()", () => { ]); }); + it("should end a CASE expression closed by a parenthesis or comma", ({ + expect, + }) => { + expect( + splitSqlQuery(` + SELECT SUM(CASE WHEN a THEN 1 ELSE 0 END) FROM t; + SELECT CASE WHEN a THEN 1 ELSE 0 END, b FROM t; + SELECT * FROM t;`) + ).toEqual([ + "SELECT SUM(CASE WHEN a THEN 1 ELSE 0 END) FROM t", + "SELECT CASE WHEN a THEN 1 ELSE 0 END, b FROM t", + "SELECT * FROM t", + ]); + }); + it("should not treat an identifier ending in END as a compound statement end", ({ expect, }) => { diff --git a/packages/wrangler/src/d1/splitter.ts b/packages/wrangler/src/d1/splitter.ts index 9f98778d89b..0d368da2da1 100644 --- a/packages/wrangler/src/d1/splitter.ts +++ b/packages/wrangler/src/d1/splitter.ts @@ -254,7 +254,7 @@ function isDollarQuoteIdentifier(str: string) { * keyword, such as a `weekend` column, from matching. */ const COMPOUND_STATEMENT_START = /(? Date: Mon, 17 Aug 2026 11:46:39 -0300 Subject: [PATCH 3/4] [wrangler] Treat bracket-quoted identifiers as quoted in the D1 splitter The statement scanner handled ', " and backtick quoting but not SQLite's bracket-quoted identifiers, while normalizeSqlLineEndings() in the same file already did. A ; inside such an identifier, as in CREATE TABLE metrics ([value;unit] TEXT), was treated as a statement boundary and broken fragments were sent to D1 (pre-existing, #15228). With the punctuation-delimited markers from this branch the gap also made a [end] column inside a trigger body pop the compound statement early and chop the trigger apart. Consume [ ... ] like the other quote styles, and cover both cases in the tests. --- .changeset/brave-brackets-split.md | 7 ++++ .../src/__tests__/d1/splitter.test.ts | 32 +++++++++++++++++++ packages/wrangler/src/d1/splitter.ts | 4 +++ 3 files changed, 43 insertions(+) create mode 100644 .changeset/brave-brackets-split.md diff --git a/.changeset/brave-brackets-split.md b/.changeset/brave-brackets-split.md new file mode 100644 index 00000000000..2c73aeab5fe --- /dev/null +++ b/.changeset/brave-brackets-split.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Treat bracket-quoted identifiers as quoted when splitting D1 SQL files + +The D1 SQL splitter handled `'`, `"` and backtick quoting but not SQLite's bracket-quoted identifiers (`[name]`), while `normalizeSqlLineEndings()` in the same file already did. A `;` inside such an identifier, e.g. `CREATE TABLE metrics ([value;unit] TEXT);`, was treated as a statement boundary, so `wrangler d1 execute --file` and `wrangler d1 migrations apply` sent broken fragments to D1. The scanner now skips over `[` … `]` like the other quote styles, which also keeps the new punctuation-delimited compound markers from matching keywords inside brackets, such as a `[end]` column in a trigger body. diff --git a/packages/wrangler/src/__tests__/d1/splitter.test.ts b/packages/wrangler/src/__tests__/d1/splitter.test.ts index c3c23116833..52a99237f5c 100644 --- a/packages/wrangler/src/__tests__/d1/splitter.test.ts +++ b/packages/wrangler/src/__tests__/d1/splitter.test.ts @@ -128,6 +128,17 @@ describe("splitSqlQuery()", () => { `); }); + it("should handle bracket-quoted identifiers", ({ expect }) => { + expect( + splitSqlQuery(` + CREATE TABLE metrics ([value;unit] TEXT); + SELECT [value;unit] FROM metrics;`) + ).toEqual([ + "CREATE TABLE metrics ([value;unit] TEXT)", + "SELECT [value;unit] FROM metrics", + ]); + }); + it("should handle inline comments", ({ expect }) => { expect( splitSqlQuery( @@ -343,6 +354,27 @@ describe("splitSqlQuery()", () => { ]); }); + it("should not treat a bracket-quoted identifier as a compound statement marker", ({ + expect, + }) => { + expect( + splitSqlQuery(` + CREATE TRIGGER t AFTER INSERT ON items + BEGIN + UPDATE x SET [end] = 1; + UPDATE y SET z = 2; + END; + SELECT 1;`) + ).toEqual([ + `CREATE TRIGGER t AFTER INSERT ON items + BEGIN + UPDATE x SET [end] = 1; + UPDATE y SET z = 2; + END`, + "SELECT 1", + ]); + }); + it("should handle compound statements for BEGINs", ({ expect }) => { expect( splitSqlQuery(` diff --git a/packages/wrangler/src/d1/splitter.ts b/packages/wrangler/src/d1/splitter.ts index 0d368da2da1..803f404e932 100644 --- a/packages/wrangler/src/d1/splitter.ts +++ b/packages/wrangler/src/d1/splitter.ts @@ -149,6 +149,10 @@ function splitSqlIntoStatements(sql: string): string[] { case "`": str += char + consumeUntilMarker(iterator, char); break; + case `[`: + // SQLite bracket-quoted identifiers end at the first `]`; there is no escape sequence. + str += char + consumeUntilMarker(iterator, `]`); + break; case `$`: { const dollarQuote = "$" + consumeWhile(iterator, isDollarQuoteIdentifier); From d543f3a9d817983574677c7fba40f29bc8ddbef9 Mon Sep 17 00:00:00 2001 From: MatheusMartinho Date: Mon, 17 Aug 2026 11:46:59 -0300 Subject: [PATCH 4/4] [wrangler] Use Unicode identifier classes for compound statement markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ASCII-only class treated a letter with a diacritical mark as a delimiter, so a column such as néend matched the END marker and closed a trigger body early. Match identifier characters with \p{L}\p{N} instead, consistent with how isDollarQuoteIdentifier() in the same file treats letters with diacritics. --- .../src/__tests__/d1/splitter.test.ts | 21 +++++++++++++++++++ packages/wrangler/src/d1/splitter.ts | 8 ++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/wrangler/src/__tests__/d1/splitter.test.ts b/packages/wrangler/src/__tests__/d1/splitter.test.ts index 52a99237f5c..15bb3058552 100644 --- a/packages/wrangler/src/__tests__/d1/splitter.test.ts +++ b/packages/wrangler/src/__tests__/d1/splitter.test.ts @@ -354,6 +354,27 @@ describe("splitSqlQuery()", () => { ]); }); + it("should not treat an accented identifier ending in END as a compound statement end", ({ + expect, + }) => { + expect( + splitSqlQuery(` + CREATE TRIGGER t AFTER INSERT ON x + BEGIN + UPDATE y SET a = 1 WHERE b = néend; + UPDATE z SET c = 2; + END; + SELECT 1;`) + ).toEqual([ + `CREATE TRIGGER t AFTER INSERT ON x + BEGIN + UPDATE y SET a = 1 WHERE b = néend; + UPDATE z SET c = 2; + END`, + "SELECT 1", + ]); + }); + it("should not treat a bracket-quoted identifier as a compound statement marker", ({ expect, }) => { diff --git a/packages/wrangler/src/d1/splitter.ts b/packages/wrangler/src/d1/splitter.ts index 803f404e932..a59f9f5025f 100644 --- a/packages/wrangler/src/d1/splitter.ts +++ b/packages/wrangler/src/d1/splitter.ts @@ -255,10 +255,12 @@ function isDollarQuoteIdentifier(str: string) { * Compound statement markers only need to be delimited from surrounding * identifiers, not padded with whitespace: SQLite accepts `WHEN (1=1)BEGIN` and * `INSERT ...;END;`. The lookbehind keeps identifiers that merely end in the - * keyword, such as a `weekend` column, from matching. + * keyword, such as a `weekend` column, from matching; identifier characters + * include letters with diacritical marks, as in `isDollarQuoteIdentifier()`, + * so a `néend` column does not match either. */ -const COMPOUND_STATEMENT_START = /(?