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/.changeset/olive-donuts-battle.md b/.changeset/olive-donuts-battle.md new file mode 100644 index 00000000000..45c6d5a3737 --- /dev/null +++ b/.changeset/olive-donuts-battle.md @@ -0,0 +1,9 @@ +--- +"wrangler": patch +--- + +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..15bb3058552 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( @@ -277,6 +288,114 @@ 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 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, + }) => { + 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 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, + }) => { + 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 026d3aad620..a59f9f5025f 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); @@ -247,16 +251,27 @@ 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; identifier characters + * include letters with diacritical marks, as in `isDollarQuoteIdentifier()`, + * so a `néend` column does not match either. + */ +const COMPOUND_STATEMENT_START = /(?