diff --git a/.changeset/wicked-lies-itch.md b/.changeset/wicked-lies-itch.md new file mode 100644 index 000000000000..610e63584c29 --- /dev/null +++ b/.changeset/wicked-lies-itch.md @@ -0,0 +1,82 @@ +--- +"wrangler": patch +--- + +fix: implement `d1 execute --json` with clean output for piping into other commands + +**Before:** + +```bash +rozenmd@cflaptop test1 % npx wrangler d1 execute test --command="select * from customers" +▲ [WARNING] Processing wrangler.toml configuration: + + - D1 Bindings are currently in alpha to allow the API to evolve before general availability. + Please report any issues to https://github.com/cloudflare/wrangler2/issues/new/choose + Note: Run this command with the environment variable NO_D1_WARNING=true to hide this message + + For example: `export NO_D1_WARNING=true && wrangler ` + + +-------------------- +🚧 D1 is currently in open alpha and is not recommended for production data and traffic +🚧 Please report any bugs to https://github.com/cloudflare/wrangler2/issues/new/choose +🚧 To request features, visit https://community.cloudflare.com/c/developers/d1 +🚧 To give feedback, visit https://discord.gg/cloudflaredev +-------------------- + +🌀 Mapping SQL input into an array of statements +🌀 Parsing 1 statements +🌀 Executing on test (xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx): +🚣 Executed 1 command in 11.846710999961942ms +┌────────────┬─────────────────────┬───────────────────┐ +│ CustomerID │ CompanyName │ ContactName │ +├────────────┼─────────────────────┼───────────────────┤ +│ 1 │ Alfreds Futterkiste │ Maria Anders │ +├────────────┼─────────────────────┼───────────────────┤ +│ 4 │ Around the Horn │ Thomas Hardy │ +├────────────┼─────────────────────┼───────────────────┤ +│ 11 │ Bs Beverages │ Victoria Ashworth │ +├────────────┼─────────────────────┼───────────────────┤ +│ 13 │ Bs Beverages │ Random Name │ +└────────────┴─────────────────────┴───────────────────┘ +``` + +**After:** + +```bash +rozenmd@cflaptop test1 % npx wrangler d1 execute test --command="select * from customers" --json +[ + { + "results": [ + { + "CustomerID": 1, + "CompanyName": "Alfreds Futterkiste", + "ContactName": "Maria Anders" + }, + { + "CustomerID": 4, + "CompanyName": "Around the Horn", + "ContactName": "Thomas Hardy" + }, + { + "CustomerID": 11, + "CompanyName": "Bs Beverages", + "ContactName": "Victoria Ashworth" + }, + { + "CustomerID": 13, + "CompanyName": "Bs Beverages", + "ContactName": "Random Name" + } + ], + "success": true, + "meta": { + "duration": 1.662519000004977, + "last_row_id": null, + "changes": null, + "served_by": "primary-xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.db3", + "internal_stats": null + } + } +] +``` diff --git a/fixtures/d1-worker-app/.gitignore b/fixtures/d1-worker-app/.gitignore new file mode 100644 index 000000000000..37cbd6339404 --- /dev/null +++ b/fixtures/d1-worker-app/.gitignore @@ -0,0 +1,2 @@ +dist +.wrangler diff --git a/fixtures/d1-worker-app/README.md b/fixtures/d1-worker-app/README.md new file mode 100644 index 000000000000..692820c9f961 --- /dev/null +++ b/fixtures/d1-worker-app/README.md @@ -0,0 +1,3 @@ +# Notes about testing with this fixture + +- For some reason, miniflare requires exactly node version 16 to be able to run `wrangler dev --local` diff --git a/fixtures/d1-worker-app/package.json b/fixtures/d1-worker-app/package.json new file mode 100644 index 000000000000..c2d02fd7363e --- /dev/null +++ b/fixtures/d1-worker-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "d1-worker-app", + "version": "1.0.0", + "private": true, + "description": "", + "license": "ISC", + "author": "", + "main": "src/index.js", + "scripts": { + "check:type": "tsc", + "db:query": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --command='SELECT * FROM Customers'", + "db:query-json": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --command='SELECT * FROM Customers' --json", + "db:reset": "wrangler d1 execute UPDATE_THIS_FOR_REMOTE_USE --local --file=./schema.sql", + "start": "wrangler dev --local" + } +} diff --git a/fixtures/d1-worker-app/schema.sql b/fixtures/d1-worker-app/schema.sql new file mode 100644 index 000000000000..3d152acfa0d9 --- /dev/null +++ b/fixtures/d1-worker-app/schema.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS Customers; +CREATE TABLE Customers (CustomerID INT, CompanyName TEXT, ContactName TEXT, PRIMARY KEY (`CustomerID`)); +INSERT INTO Customers (CustomerID, CompanyName, ContactName) VALUES (1, 'Alfreds Futterkiste', 'Maria Anders'), (4, 'Around the Horn', 'Thomas Hardy'), (11, 'Bs Beverages', 'Victoria Ashworth'), (13, 'Bs Beverages', 'Random Name'); diff --git a/fixtures/d1-worker-app/src/index.js b/fixtures/d1-worker-app/src/index.js new file mode 100644 index 000000000000..473e10a632f7 --- /dev/null +++ b/fixtures/d1-worker-app/src/index.js @@ -0,0 +1,5 @@ +export default { + async fetch(request) { + return new Response(`Hello world!`); + }, +}; diff --git a/fixtures/d1-worker-app/tsconfig.json b/fixtures/d1-worker-app/tsconfig.json new file mode 100644 index 000000000000..6eb14e3584b7 --- /dev/null +++ b/fixtures/d1-worker-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "esModuleInterop": true, + "module": "CommonJS", + "lib": ["ES2020"], + "types": ["node"], + "moduleResolution": "node", + "noEmit": true + }, + "include": ["tests", "../../node-types.d.ts"] +} diff --git a/fixtures/d1-worker-app/wrangler.toml b/fixtures/d1-worker-app/wrangler.toml new file mode 100644 index 000000000000..ce37a2f1b4df --- /dev/null +++ b/fixtures/d1-worker-app/wrangler.toml @@ -0,0 +1,9 @@ +name = "worker-app" +main = "src/index.js" +compatibility_date = "2022-03-31" + +[[d1_databases]] +binding = "DB" # i.e. available in your Worker on env.DB +database_name = "UPDATE_THIS_FOR_REMOTE_USE" +preview_database_id = "UPDATE_THIS_FOR_REMOTE_USE" +database_id = "UPDATE_THIS_FOR_REMOTE_USE" diff --git a/packages/wrangler/src/d1/execute.tsx b/packages/wrangler/src/d1/execute.tsx index 18aac181283e..7c138352428d 100644 --- a/packages/wrangler/src/d1/execute.tsx +++ b/packages/wrangler/src/d1/execute.tsx @@ -7,7 +7,7 @@ import Table from "ink-table"; import { npxImport } from "npx-import"; import React from "react"; import { fetchResult } from "../cfetch"; -import { withConfig } from "../config"; +import { readConfig } from "../config"; import { getLocalPersistencePath } from "../dev/get-local-persistence-path"; import { confirm } from "../dialogs"; import { logger } from "../logger"; @@ -75,6 +75,11 @@ export function Options(yargs: CommonYargsArgv) { describe: "Specify directory to use for local persistence (for --local)", type: "string", requiresArg: true, + }) + .option("json", { + describe: "Return output as clean JSON", + type: "boolean", + default: false, }); } @@ -91,14 +96,13 @@ export async function executeSql( shouldPrompt: boolean | undefined, persistTo: undefined | string, file?: string, - command?: string + command?: string, + json?: boolean ) { const sql = file ? readFileSync(file) : command; - if (!sql) throw new Error(`Error: must provide --command or --file.`); if (persistTo && !local) throw new Error(`Error: can't use --persist-to without --local`); - logger.log(`🌀 Mapping SQL input into an array of statements`); const queries = splitSqlQuery(sql); @@ -112,72 +116,80 @@ export async function executeSql( } return local - ? await executeLocally(config, name, shouldPrompt, queries, persistTo) - : await executeRemotely(config, name, shouldPrompt, batchSplit(queries)); + ? await executeLocally(config, name, shouldPrompt, queries, persistTo, json) + : await executeRemotely( + config, + name, + shouldPrompt, + batchSplit(queries), + json + ); } type HandlerOptions = StrictYargsOptionsToInterface; -export const Handler = withConfig( - async ({ + +export const Handler = async (args: HandlerOptions): Promise => { + const { local, database, yes, persistTo, file, command, json } = args; + const existingLogLevel = logger.loggerLevel; + if (json) { + // set loggerLevel to error to avoid readConfig warnings appearing in JSON output + logger.loggerLevel = "error"; + } + const config = readConfig(args.config, args); + logger.log(d1BetaWarning); + if (file && command) + return logger.error(`Error: can't provide both --command and --file.`); + + const isInteractive = process.stdout.isTTY; + const response: QueryResult[] | null = await executeSql( + local, config, database, + isInteractive && !yes, + persistTo, file, command, - local, - persistTo, - yes, - }): Promise => { - logger.log(d1BetaWarning); - if (file && command) - return logger.error(`Error: can't provide both --command and --file.`); - - const isInteractive = process.stdout.isTTY; - const response: QueryResult[] | null = await executeSql( - local, - config, - database, - isInteractive && !yes, - persistTo, - file, - command - ); + json + ); - // Early exit if prompt rejected - if (!response) return; + // Early exit if prompt rejected + if (!response) return; - if (isInteractive) { - // Render table if single result - render( - - {(result) => { - // batch results - if (!Array.isArray(result)) { - const { results, query } = result; + if (isInteractive && !json) { + // Render table if single result + render( + + {(result) => { + // batch results + if (!Array.isArray(result)) { + const { results, query } = result; - if (Array.isArray(results) && results.length > 0) { - const shortQuery = shorten(query, 48); - return ( - <> - {shortQuery ? {shortQuery} : null} -
- - ); - } + if (Array.isArray(results) && results.length > 0) { + const shortQuery = shorten(query, 48); + return ( + <> + {shortQuery ? {shortQuery} : null} +
+ + ); } - }} -
- ); - } else { - logger.log(JSON.stringify(response, null, 2)); - } + } + }} +
+ ); + } else { + // set loggerLevel back to what it was before to actually output the JSON in stdout + logger.loggerLevel = existingLogLevel; + logger.log(JSON.stringify(response, null, 2)); } -); +}; async function executeLocally( config: Config, name: string, shouldPrompt: boolean | undefined, queries: string[], - persistTo: string | undefined + persistTo: string | undefined, + json?: boolean ) { const localDB = getDatabaseInfoFromConfig(config, name); if (!localDB) { @@ -202,6 +214,7 @@ async function executeLocally( if (!existsSync(dbDir)) { const ok = + json || !shouldPrompt || (await confirm(`About to create ${readableRelative(dbPath)}, ok?`)); if (!ok) return null; @@ -224,10 +237,12 @@ async function executeRemotely( config: Config, name: string, shouldPrompt: boolean | undefined, - batches: string[] + batches: string[], + json?: boolean ) { const multiple_batches = batches.length > 1; - if (multiple_batches) { + // in JSON mode, we don't want a prompt here + if (multiple_batches && !json) { const warning = `⚠️ Too much SQL to send at once, this execution will be sent as ${batches.length} batches.`; if (shouldPrompt) { @@ -248,14 +263,7 @@ async function executeRemotely( name ); - if (shouldPrompt) { - logger.log(`🌀 Executing on ${name} (${db.uuid}):`); - - // Don't output if shouldPrompt is undefined - } else if (shouldPrompt !== undefined) { - // Pipe to error so we don't break jq - logger.error(`Executing on ${name} (${db.uuid}):`); - } + logger.log(`🌀 Executing on ${name} (${db.uuid}):`); const results: QueryResult[] = []; for (const sql of batches) {