Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/d1-crlf-remote-query.md
Original file line number Diff line number Diff line change
@@ -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.
53 changes: 53 additions & 0 deletions packages/wrangler/src/__tests__/d1/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
9 changes: 6 additions & 3 deletions packages/wrangler/src/d1/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Windows-style line breaks inside quoted text values are silently changed when running against the remote database

Every carriage return in the SQL is stripped out (input.command?.replace(/\r\n/g, "\n") at packages/wrangler/src/d1/execute.ts:510) before the statement is sent, so text values that intentionally contain Windows line breaks are stored differently on the remote database than locally.
Impact: Data inserted or updated through remote commands or migrations loses its carriage returns, producing values that differ from the same command run locally.

Blanket replacement also rewrites string literals, not just statement separators

The replacement is applied to the whole command text, including the inside of quoted SQL string literals. For example wrangler d1 execute db --remote --command "INSERT INTO t(msg) VALUES ('a\r\nb')" (and any migration file with CRLF-containing literals applied via packages/wrangler/src/d1/migrations/apply.ts:158-165) stores a\nb remotely. The local path at packages/wrangler/src/d1/execute.ts:340 does no such normalization, so local and remote execution of identical SQL now diverge. A narrower fix would normalize only outside of quoted literals (e.g. reuse the existing tokenizer in packages/wrangler/src/d1/splitter.ts).

Prompt for agents
The remote query path in packages/wrangler/src/d1/execute.ts normalizes CRLF to LF across the entire command string. This also rewrites the contents of quoted SQL string literals, so text values containing Windows line breaks are silently stored with LF on the remote database, while the local path (same file, around line 340) leaves them untouched — local and remote execution of the same SQL now produce different stored data. Consider normalizing only line endings that are outside of string literals/comments, e.g. by reusing the SQL tokenizer in packages/wrangler/src/d1/splitter.ts, or otherwise limiting the normalization to statement whitespace.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const result = await d1ApiPost<QueryResult[]>(
config,
accountId,
db,
"query",
{
sql: input.command,
}
{ sql }
);
logResult(result);
return result;
Expand Down