diff --git a/.changeset/d1-crlf-remote-query.md b/.changeset/d1-crlf-remote-query.md new file mode 100644 index 00000000000..6e641c4946a --- /dev/null +++ b/.changeset/d1-crlf-remote-query.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Normalize CRLF line endings before sending D1 commands to the remote query API + +`wrangler d1 migrations apply --remote` and `wrangler d1 execute --remote --command` failed with `incomplete input: SQLITE_ERROR` when the SQL contained CRLF line endings inside a compound statement such as a `CREATE TRIGGER ... BEGIN ... END;` body. The command string is now normalized to LF before it is sent to the D1 query API. diff --git a/packages/wrangler/src/__tests__/d1/execute.test.ts b/packages/wrangler/src/__tests__/d1/execute.test.ts index 2ced79fc5e7..d4041276d90 100644 --- a/packages/wrangler/src/__tests__/d1/execute.test.ts +++ b/packages/wrangler/src/__tests__/d1/execute.test.ts @@ -302,6 +302,59 @@ To continue without logging in, rerun this command with \`--temporary\`. Wrangle expect(std.out).toMatch("🚣 Executed 1 command in 123.46ms"); }); + it("should normalize CRLF line endings in commands sent to the remote query API", async ({ + expect, + }) => { + setIsTTY(false); + writeWranglerConfig({ + d1_databases: [ + { binding: "DATABASE", database_name: "db", database_id: "xxxx" }, + ], + }); + + msw.use( + ...getMswSuccessMembershipHandlers([ + { + id: "some-account-id", + name: "test-account", + }, + ]) + ); + + let sentSql: string | undefined; + msw.use( + http.get("*/accounts/:accountId/d1/database", async () => { + return HttpResponse.json( + createFetchResult([ + { uuid: "xxxx", name: "db", created_at: "", version: "alpha" }, + ]) + ); + }), + http.post( + "*/accounts/:accountId/d1/database/:databaseId/query", + async ({ request }) => { + sentSql = ((await request.json()) as { sql: string }).sql; + return HttpResponse.json( + createFetchResult([ + { + results: [{ result: 1 }], + success: true, + meta: { duration: 100 }, + }, + ]) + ); + } + ) + ); + + await runWrangler( + "d1 execute db --remote --command 'CREATE TRIGGER trg BEFORE DELETE ON probe_z\r\nBEGIN\r\n SELECT RAISE(ABORT, '\''no'\'');\r\nEND;'" + ); + + expect(sentSql).toBeDefined(); + expect(sentSql).not.toContain("\r"); + }); + it("should format batch execution duration with 2 decimal places", async ({ expect, }) => { diff --git a/packages/wrangler/src/d1/execute.ts b/packages/wrangler/src/d1/execute.ts index 290a0a64fbb..f1dc9d450a6 100644 --- a/packages/wrangler/src/d1/execute.ts +++ b/packages/wrangler/src/d1/execute.ts @@ -503,14 +503,17 @@ async function executeRemotely({ }, ]; } else { + // The D1 query API splits multi-statement SQL on `;` server-side, and + // mishandles CRLF line endings inside compound statements such as a + // `CREATE TRIGGER ... BEGIN ... END;` body (issue #14991). Normalize + // to LF so the server receives the same input that works for LF files. + const sql = input.command?.replace(/\r\n/g, "\n"); const result = await d1ApiPost( config, accountId, db, "query", - { - sql: input.command, - } + { sql } ); logResult(result); return result;