Skip to content

Commit 2746b5a

Browse files
committed
query command
1 parent 41be5b5 commit 2746b5a

File tree

7 files changed

+89
-43
lines changed

7 files changed

+89
-43
lines changed

bin/notebooks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ switch (command) {
1818
await run(process.argv.slice(3));
1919
break;
2020
}
21+
case "query": {
22+
const {default: run} = await import("./query.js");
23+
await run(process.argv.slice(3));
24+
break;
25+
}
2126
default: {
2227
console.log(
2328
`usage: notebooks <command>
2429
2530
preview start the preview server
2631
build generate a static site
2732
download download an Observable Notebook as HTML
33+
query run a database query
2834
help print usage information
2935
version print the version
3036
`

bin/query.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/env node
2+
3+
import {parseArgs} from "node:util";
4+
import {getDatabase, getDatabaseConfig, getQueryCachePath} from "../src/databases/index.js";
5+
import {dirname} from "node:path/win32";
6+
import {mkdir, writeFile} from "node:fs/promises";
7+
import {join} from "node:path";
8+
9+
if (process.argv[1] === import.meta.filename) run();
10+
11+
export default async function run(args?: string[]): Promise<void> {
12+
const {values, positionals} = parseArgs({
13+
args,
14+
allowPositionals: true,
15+
allowNegative: true,
16+
options: {
17+
root: {
18+
type: "string",
19+
default: "."
20+
},
21+
database: {
22+
type: "string"
23+
},
24+
help: {
25+
type: "boolean",
26+
short: "h"
27+
}
28+
}
29+
});
30+
31+
if (values.help || !values.database) {
32+
console.log(`usage: notebooks query <...query>
33+
34+
--database <name> name of the database
35+
--root <dir> path to the root directory; defaults to cwd
36+
-h, --help show this message
37+
`);
38+
return;
39+
}
40+
41+
// Parse positionals into query template arguments.
42+
const strings: string[] = [];
43+
const params: unknown[] = [];
44+
for (let i = 0; i < positionals.length; ++i) {
45+
if (i & 1) params.push(JSON.parse(positionals[i]));
46+
else strings.push(positionals[i]);
47+
}
48+
49+
process.chdir(values.root);
50+
const config = await getDatabaseConfig(".", values.database);
51+
const database = await getDatabase(config);
52+
const results = await database.call(null, strings, ...params);
53+
const cachePath = await getQueryCachePath(".", values.database, strings, ...params);
54+
await mkdir(dirname(cachePath), {recursive: true});
55+
await writeFile(cachePath, JSON.stringify(results, replace));
56+
console.log(join(values.root, cachePath));
57+
}
58+
59+
// Force dates to be serialized as ISO 8601 UTC, undoing this:
60+
// https://github.com/snowflakedb/snowflake-connector-nodejs/blob/a9174fb7/lib/connection/result/sf_timestamp.js#L177-L179
61+
function replace(this: {[key: string]: unknown}, key: string, value: unknown): unknown {
62+
return this[key] instanceof Date ? Date.prototype.toJSON.call(this[key]) : value;
63+
}

src/databases/duckdb.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
11
import type {DuckDBResult, DuckDBType, Json} from "@duckdb/node-api";
22
import {DuckDBConnection, DuckDBInstance} from "@duckdb/node-api";
33
import {BIGINT, BIT, BLOB, BOOLEAN, DATE, DOUBLE, FLOAT, HUGEINT, INTEGER, INTERVAL, SMALLINT, TIME, TIMESTAMP, TIMESTAMP_MS, TIMESTAMP_NS, TIMESTAMP_S, TIMESTAMPTZ, TINYINT, UBIGINT, UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, VARCHAR, VARINT} from "@duckdb/node-api"; // prettier-ignore
4-
import {join} from "node:path";
5-
import type {DatabaseContext, DuckDBConfig, QueryTemplateFunction} from "./index.js";
4+
import type {DuckDBConfig, QueryTemplateFunction} from "./index.js";
65
import type {ColumnSchema} from "../runtime/index.js";
76

8-
export default function duckdb(
9-
{path, options}: DuckDBConfig,
10-
context: DatabaseContext
11-
): QueryTemplateFunction {
12-
if (path !== undefined) path = join(context.cwd, path);
7+
export default function duckdb({path, options}: DuckDBConfig): QueryTemplateFunction {
138
return async (strings, ...params) => {
149
const instance = await DuckDBInstance.create(path, options);
1510
const connection = await DuckDBConnection.create(instance);
16-
await connection.run(`SET file_search_path=$0`, [context.cwd]);
1711
const date = new Date();
1812
let result: DuckDBResult;
1913
let rows: Record<string, Json>[];

src/databases/index.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ export type PostgresConfig = {
4141
ssl?: boolean;
4242
};
4343

44-
export type DatabaseContext = {
45-
cwd: string;
46-
};
47-
4844
export type QueryTemplateFunction = (
4945
strings: readonly string[],
5046
...params: QueryParam[]
@@ -82,15 +78,12 @@ export async function getDatabaseConfig(
8278
return config;
8379
}
8480

85-
export async function getDatabase(
86-
config: DatabaseConfig,
87-
context: DatabaseContext
88-
): Promise<QueryTemplateFunction> {
81+
export async function getDatabase(config: DatabaseConfig): Promise<QueryTemplateFunction> {
8982
switch (config.type) {
9083
case "duckdb":
91-
return (await import("./duckdb.js")).default(config, context);
84+
return (await import("./duckdb.js")).default(config);
9285
case "sqlite":
93-
return (await import(process.versions.bun ? "./sqlite-bun.js" : "./sqlite-node.js")).default(config, context);
86+
return (await import(process.versions.bun ? "./sqlite-bun.js" : "./sqlite-node.js")).default(config);
9487
case "snowflake":
9588
return (await import("./snowflake.js")).default(config);
9689
case "postgres":

src/databases/sqlite-bun.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import {join} from "node:path";
21
import type {Statement} from "bun:sqlite";
32
import {Database} from "bun:sqlite";
4-
import type {DatabaseContext, SQLiteConfig, QueryTemplateFunction} from "./index.js";
3+
import type {SQLiteConfig, QueryTemplateFunction} from "./index.js";
54
import type {ColumnSchema} from "../runtime/index.js";
65
import {getColumnType} from "./sqlite.js";
76

8-
export default function sqlite(
9-
{path}: SQLiteConfig,
10-
context: DatabaseContext
11-
): QueryTemplateFunction {
7+
export default function sqlite({path = ":memory:"}: SQLiteConfig): QueryTemplateFunction {
128
return async (strings, ...params) => {
139
const date = new Date();
14-
const database = new Database(path === undefined ? ":memory:" : join(context.cwd, path));
10+
const database = new Database(path);
1511
try {
1612
const statement = database.prepare(strings.join("?"));
1713
const rows = statement.all(...params) as Record<string, unknown>[];

src/databases/sqlite-node.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
1-
import {join} from "node:path";
21
import type {StatementSync} from "node:sqlite";
32
import {DatabaseSync} from "node:sqlite";
4-
import type {DatabaseContext, SQLiteConfig, QueryTemplateFunction} from "./index.js";
3+
import type {SQLiteConfig, QueryTemplateFunction} from "./index.js";
54
import type {ColumnSchema} from "../runtime/index.js";
65
import {getColumnType} from "./sqlite.js";
76

8-
export default function sqlite(
9-
{path}: SQLiteConfig,
10-
context: DatabaseContext
11-
): QueryTemplateFunction {
7+
export default function sqlite({path = ":memory:"}: SQLiteConfig): QueryTemplateFunction {
128
return async (strings, ...params) => {
139
const date = new Date();
14-
const database = new DatabaseSync(path === undefined ? ":memory:" : join(context.cwd, path));
10+
const database = new DatabaseSync(path);
1511
try {
1612
const statement = database.prepare(strings.join("?"));
1713
const rows = statement.all(...params) as Record<string, unknown>[];

src/vite/observable.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import {fork} from "node:child_process";
12
import {existsSync} from "node:fs";
2-
import {mkdir, readFile, writeFile} from "node:fs/promises";
3+
import {readFile} from "node:fs/promises";
34
import {dirname, join, resolve} from "node:path";
45
import {relative} from "node:path/posix";
56
import {fileURLToPath} from "node:url";
67
import type {TemplateLiteral} from "acorn";
78
import {JSDOM} from "jsdom";
89
import type {PluginOption, IndexHtmlTransformContext} from "vite";
9-
import {getDatabase, getDatabaseConfig, getQueryCachePath} from "../databases/index.js";
10+
import {getQueryCachePath} from "../databases/index.js";
1011
import type {Cell, Notebook} from "../lib/notebook.js";
1112
import {deserialize} from "../lib/serialize.js";
1213
import {Sourcemap} from "../javascript/sourcemap.js";
@@ -115,15 +116,12 @@ export function observable({
115116
const dir = dirname(context.filename);
116117
const cachePath = await getQueryCachePath(context.filename, cell.database, [value]);
117118
if (!existsSync(cachePath)) {
118-
const config = await getDatabaseConfig(context.filename, cell.database);
119-
try {
120-
const database = await getDatabase(config, {cwd: dir});
121-
const results = await database.call(null, [value]);
122-
await mkdir(dirname(cachePath), {recursive: true});
123-
await writeFile(cachePath, JSON.stringify(results));
124-
} catch (error) {
125-
console.error(error);
126-
}
119+
const args = ["--root", dir, "--database", cell.database, value];
120+
const child = fork(fileURLToPath(import.meta.resolve("../../bin/query.ts")), args);
121+
await new Promise((resolve, reject) => {
122+
child.on("error", reject);
123+
child.on("exit", resolve);
124+
});
127125
}
128126
cell.mode = "js";
129127
cell.value = `FileAttachment(${JSON.stringify(relative(dir, cachePath))}).json().then(DatabaseClient.revive)${hidden ? "" : `.then(Inputs.table)${cell.output ? ".then(view)" : ""}`}`;

0 commit comments

Comments
 (0)