Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[D1] add a json option to execute #2627

Merged
merged 17 commits into from
Jan 27, 2023
Merged
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
82 changes: 82 additions & 0 deletions .changeset/wicked-lies-itch.md
Original file line number Diff line number Diff line change
@@ -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 <YOUR COMMAND HERE>`


--------------------
🚧 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
}
}
]
```
2 changes: 2 additions & 0 deletions fixtures/d1-worker-app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
.wrangler
3 changes: 3 additions & 0 deletions fixtures/d1-worker-app/README.md
Original file line number Diff line number Diff line change
@@ -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`
16 changes: 16 additions & 0 deletions fixtures/d1-worker-app/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions fixtures/d1-worker-app/schema.sql
Original file line number Diff line number Diff line change
@@ -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');
5 changes: 5 additions & 0 deletions fixtures/d1-worker-app/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
async fetch(request) {
return new Response(`Hello world!`);
},
};
12 changes: 12 additions & 0 deletions fixtures/d1-worker-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
9 changes: 9 additions & 0 deletions fixtures/d1-worker-app/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"
134 changes: 71 additions & 63 deletions packages/wrangler/src/d1/execute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
});
}

Expand All @@ -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);

Expand All @@ -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<typeof Options>;
export const Handler = withConfig<HandlerOptions>(
async ({

export const Handler = async (args: HandlerOptions): Promise<void> => {
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<void> => {
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(
<Static items={response}>
{(result) => {
// batch results
if (!Array.isArray(result)) {
const { results, query } = result;
if (isInteractive && !json) {
// Render table if single result
render(
<Static items={response}>
{(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 ? <Text dimColor>{shortQuery}</Text> : null}
<Table data={results}></Table>
</>
);
}
if (Array.isArray(results) && results.length > 0) {
const shortQuery = shorten(query, 48);
return (
<>
{shortQuery ? <Text dimColor>{shortQuery}</Text> : null}
<Table data={results}></Table>
</>
);
}
}}
</Static>
);
} else {
logger.log(JSON.stringify(response, null, 2));
}
}
}}
</Static>
);
} 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) {
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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) {
rozenmd marked this conversation as resolved.
Show resolved Hide resolved
// 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) {
Expand Down