@observablehq/notebook-kit 1.1.0-rc.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,12 +16,18 @@ switch (command) {
16
16
  await run(process.argv.slice(3));
17
17
  break;
18
18
  }
19
+ case "query": {
20
+ const { default: run } = await import("./query.js");
21
+ await run(process.argv.slice(3));
22
+ break;
23
+ }
19
24
  default: {
20
25
  console.log(`usage: notebooks <command>
21
26
 
22
27
  preview start the preview server
23
28
  build generate a static site
24
29
  download download an Observable Notebook as HTML
30
+ query run a database query
25
31
  help print usage information
26
32
  version print the version
27
33
  `);
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export default function run(args?: string[]): Promise<void>;
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { getDatabase, getDatabaseConfig, getQueryCachePath } from "../src/databases/index.js";
4
+ import { dirname } from "node:path/win32";
5
+ import { mkdir, writeFile } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ if (process.argv[1] === import.meta.filename)
8
+ run();
9
+ export default async function run(args) {
10
+ const { values, positionals } = parseArgs({
11
+ args,
12
+ allowPositionals: true,
13
+ allowNegative: true,
14
+ options: {
15
+ root: {
16
+ type: "string",
17
+ default: "."
18
+ },
19
+ database: {
20
+ type: "string"
21
+ },
22
+ help: {
23
+ type: "boolean",
24
+ short: "h"
25
+ }
26
+ }
27
+ });
28
+ if (values.help || !values.database) {
29
+ console.log(`usage: notebooks query <...query>
30
+
31
+ --database <name> name of the database
32
+ --root <dir> path to the root directory; defaults to cwd
33
+ -h, --help show this message
34
+ `);
35
+ return;
36
+ }
37
+ // Parse positionals into query template arguments.
38
+ const strings = [];
39
+ const params = [];
40
+ for (let i = 0; i < positionals.length; ++i) {
41
+ if (i & 1)
42
+ params.push(JSON.parse(positionals[i]));
43
+ else
44
+ strings.push(positionals[i]);
45
+ }
46
+ process.chdir(values.root);
47
+ const config = await getDatabaseConfig(".", values.database);
48
+ const database = await getDatabase(config);
49
+ const results = await database.call(null, strings, ...params);
50
+ const cachePath = await getQueryCachePath(".", values.database, strings, ...params);
51
+ await mkdir(dirname(cachePath), { recursive: true });
52
+ await writeFile(cachePath, JSON.stringify(results, replace));
53
+ console.log(join(values.root, cachePath));
54
+ }
55
+ // Force dates to be serialized as ISO 8601 UTC, undoing this:
56
+ // https://github.com/snowflakedb/snowflake-connector-nodejs/blob/a9174fb7/lib/connection/result/sf_timestamp.js#L177-L179
57
+ function replace(key, value) {
58
+ return this[key] instanceof Date ? Date.prototype.toJSON.call(this[key]) : value;
59
+ }
package/dist/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/observablehq/notebook-kit.git"
7
7
  },
8
- "version": "1.1.0-rc.8",
8
+ "version": "1.1.0",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "vitest",
@@ -67,6 +67,7 @@
67
67
  "@eslint/js": "^9.29.0",
68
68
  "@types/jsdom": "^21.1.7",
69
69
  "@types/markdown-it": "^14.1.2",
70
+ "bun-types": "^1.2.20",
70
71
  "eslint": "^9.29.0",
71
72
  "globals": "^16.2.0",
72
73
  "htl": "^0.3.1",
@@ -1,2 +1,2 @@
1
- import type { DatabaseContext, DuckDBConfig, QueryTemplateFunction } from "./index.js";
2
- export default function duckdb({ path, options }: DuckDBConfig, context: DatabaseContext): QueryTemplateFunction;
1
+ import type { DuckDBConfig, QueryTemplateFunction } from "./index.js";
2
+ export default function duckdb({ path, options }: DuckDBConfig): QueryTemplateFunction;
@@ -1,13 +1,9 @@
1
1
  import { DuckDBConnection, DuckDBInstance } from "@duckdb/node-api";
2
2
  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
3
- import { join } from "node:path";
4
- export default function duckdb({ path, options }, context) {
5
- if (path !== undefined)
6
- path = join(context.cwd, path);
3
+ export default function duckdb({ path, options }) {
7
4
  return async (strings, ...params) => {
8
5
  const instance = await DuckDBInstance.create(path, options);
9
6
  const connection = await DuckDBConnection.create(instance);
10
- await connection.run(`SET file_search_path=$0`, [context.cwd]);
11
7
  const date = new Date();
12
8
  let result;
13
9
  let rows;
@@ -1,6 +1,5 @@
1
1
  import type { ColumnSchema, QueryParam } from "../runtime/index.js";
2
- export { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
3
- export type DatabaseConfig = DuckDBConfig | SnowflakeConfig | PostgresConfig;
2
+ export type DatabaseConfig = DuckDBConfig | SQLiteConfig | SnowflakeConfig | PostgresConfig;
4
3
  export type DuckDBConfig = {
5
4
  type: "duckdb";
6
5
  path?: string;
@@ -8,6 +7,10 @@ export type DuckDBConfig = {
8
7
  [key: string]: string;
9
8
  };
10
9
  };
10
+ export type SQLiteConfig = {
11
+ type: "sqlite";
12
+ path?: string;
13
+ };
11
14
  export type SnowflakeConfig = {
12
15
  type: "snowflake";
13
16
  account: string;
@@ -27,9 +30,6 @@ export type PostgresConfig = {
27
30
  database?: string;
28
31
  ssl?: boolean;
29
32
  };
30
- export type DatabaseContext = {
31
- cwd: string;
32
- };
33
33
  export type QueryTemplateFunction = (strings: readonly string[], ...params: QueryParam[]) => Promise<SerializableQueryResult>;
34
34
  export type SerializableQueryResult = {
35
35
  rows: Record<string, unknown>[];
@@ -38,5 +38,5 @@ export type SerializableQueryResult = {
38
38
  date: Date;
39
39
  };
40
40
  export declare function getDatabaseConfig(sourcePath: string, databaseName: string): Promise<DatabaseConfig>;
41
- export declare function getDatabase(config: DatabaseConfig, context: DatabaseContext): Promise<QueryTemplateFunction>;
41
+ export declare function getDatabase(config: DatabaseConfig): Promise<QueryTemplateFunction>;
42
42
  export declare function getQueryCachePath(sourcePath: string, databaseName: string, strings: readonly string[], ...params: unknown[]): Promise<string>;
@@ -3,7 +3,6 @@ import { dirname, join } from "node:path";
3
3
  import { json } from "node:stream/consumers";
4
4
  import { isEnoent } from "../lib/error.js";
5
5
  import { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
6
- export { hash as getQueryHash, nameHash as getNameHash } from "../lib/hash.js";
7
6
  export async function getDatabaseConfig(sourcePath, databaseName) {
8
7
  const sourceDir = dirname(sourcePath);
9
8
  const configPath = join(sourceDir, ".observable", "databases.json");
@@ -22,17 +21,23 @@ export async function getDatabaseConfig(sourcePath, databaseName) {
22
21
  config = { type: "postgres" };
23
22
  else if (databaseName === "duckdb")
24
23
  config = { type: "duckdb" };
25
- else if (/\.(duck)?db$/i.test(databaseName))
24
+ else if (databaseName === "sqlite")
25
+ config = { type: "sqlite" };
26
+ else if (/\.duckdb$/i.test(databaseName))
26
27
  config = { type: "duckdb", path: databaseName };
28
+ else if (/\.db$/i.test(databaseName))
29
+ config = { type: "sqlite", path: databaseName }; // TODO disambiguate
27
30
  else
28
31
  throw new Error(`database not found: ${databaseName}`);
29
32
  }
30
33
  return config;
31
34
  }
32
- export async function getDatabase(config, context) {
35
+ export async function getDatabase(config) {
33
36
  switch (config.type) {
34
37
  case "duckdb":
35
- return (await import("./duckdb.js")).default(config, context);
38
+ return (await import("./duckdb.js")).default(config);
39
+ case "sqlite":
40
+ return (await import(process.versions.bun ? "./sqlite-bun.js" : "./sqlite-node.js")).default(config);
36
41
  case "snowflake":
37
42
  return (await import("./snowflake.js")).default(config);
38
43
  case "postgres":
@@ -0,0 +1,2 @@
1
+ import type { SQLiteConfig, QueryTemplateFunction } from "./index.js";
2
+ export default function sqlite({ path }: SQLiteConfig): QueryTemplateFunction;
@@ -0,0 +1,27 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { getColumnType } from "./sqlite.js";
3
+ export default function sqlite({ path = ":memory:" }) {
4
+ return async (strings, ...params) => {
5
+ const date = new Date();
6
+ const database = new Database(path);
7
+ try {
8
+ const statement = database.prepare(strings.join("?"));
9
+ const rows = statement.all(...params);
10
+ return {
11
+ rows,
12
+ schema: getStatementSchema(statement),
13
+ duration: Date.now() - +date,
14
+ date
15
+ };
16
+ }
17
+ finally {
18
+ database.close();
19
+ }
20
+ };
21
+ }
22
+ function getStatementSchema(statement) {
23
+ return statement.columnNames.map((name, i) => ({
24
+ name,
25
+ type: getColumnType(statement.columnTypes[i])
26
+ }));
27
+ }
@@ -0,0 +1,2 @@
1
+ import type { SQLiteConfig, QueryTemplateFunction } from "./index.js";
2
+ export default function sqlite({ path }: SQLiteConfig): QueryTemplateFunction;
@@ -0,0 +1,26 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { getColumnType } from "./sqlite.js";
3
+ export default function sqlite({ path = ":memory:" }) {
4
+ return async (strings, ...params) => {
5
+ const date = new Date();
6
+ const database = new DatabaseSync(path);
7
+ try {
8
+ const statement = database.prepare(strings.join("?"));
9
+ const rows = statement.all(...params);
10
+ return {
11
+ rows,
12
+ schema: getStatementSchema(statement),
13
+ duration: Date.now() - +date,
14
+ date
15
+ };
16
+ }
17
+ finally {
18
+ database.close();
19
+ }
20
+ };
21
+ }
22
+ function getStatementSchema(statement) {
23
+ return statement
24
+ .columns()
25
+ .map((column) => ({ name: column.name, type: getColumnType(column.type) }));
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { ColumnSchema } from "../runtime/index.js";
2
+ export declare function getColumnType(type: string | null): ColumnSchema["type"];
@@ -0,0 +1,36 @@
1
+ export function getColumnType(type) {
2
+ switch (type) {
3
+ case "INT":
4
+ case "INTEGER":
5
+ case "TINYINT":
6
+ case "SMALLINT":
7
+ case "MEDIUMINT":
8
+ case "BIGINT":
9
+ case "UNSIGNED BIG INT":
10
+ case "INT2":
11
+ case "INT8":
12
+ return "integer";
13
+ case "TEXT":
14
+ case "CLOB":
15
+ return "string";
16
+ case "REAL":
17
+ case "DOUBLE":
18
+ case "DOUBLE PRECISION":
19
+ case "FLOAT":
20
+ case "NUMERIC":
21
+ return "number";
22
+ case "BLOB":
23
+ return "buffer";
24
+ case "DATE":
25
+ case "DATETIME":
26
+ return "string"; // TODO convert strings to Date instances in sql.js
27
+ case null:
28
+ return "other";
29
+ default:
30
+ return /^(?:(?:(?:VARYING|NATIVE) )?CHARACTER|(?:N|VAR|NVAR)CHAR)\(/.test(type)
31
+ ? "string"
32
+ : /^(?:DECIMAL|NUMERIC)\(/.test(type)
33
+ ? "number"
34
+ : "other";
35
+ }
36
+ }
@@ -58,12 +58,12 @@ export declare class NotebookRuntime {
58
58
  width: () => AsyncGenerator<number, void, unknown>;
59
59
  DatabaseClient: () => {
60
60
  (name: string, options?: import("./stdlib/databaseClient.js").QueryOptionsSpec): import("./stdlib/databaseClient.js").DatabaseClient;
61
- hash: typeof import("../lib/hash.js").hash;
62
61
  revive: ({ rows, schema, date, ...meta }: import("../databases/index.js").SerializableQueryResult) => import("./stdlib/databaseClient.js").QueryResult;
63
62
  prototype: {
64
63
  readonly name: string;
65
64
  readonly options: import("./stdlib/databaseClient.js").QueryOptions;
66
65
  sql(strings: readonly string[], ...params: import("./stdlib/databaseClient.js").QueryParam[]): Promise<import("./stdlib/databaseClient.js").QueryResult>;
66
+ cachePath(strings: readonly string[], ...params: import("./stdlib/databaseClient.js").QueryParam[]): Promise<string>;
67
67
  };
68
68
  };
69
69
  FileAttachment: () => {
@@ -1,5 +1,4 @@
1
1
  import type { SerializableQueryResult } from "../../databases/index.js";
2
- import { hash } from "../../lib/hash.js";
3
2
  /** A serializable value that can be interpolated into a query. */
4
3
  export type QueryParam = any;
5
4
  /** @see https://observablehq.com/@observablehq/database-client-specification#%C2%A71 */
@@ -32,7 +31,6 @@ export interface DatabaseClient {
32
31
  }
33
32
  export declare const DatabaseClient: {
34
33
  (name: string, options?: QueryOptionsSpec): DatabaseClient;
35
- hash: typeof hash;
36
34
  revive: typeof revive;
37
35
  prototype: DatabaseClientImpl;
38
36
  };
@@ -41,6 +39,7 @@ declare class DatabaseClientImpl implements DatabaseClient {
41
39
  readonly options: QueryOptions;
42
40
  constructor(name: string, options: QueryOptions);
43
41
  sql(strings: readonly string[], ...params: QueryParam[]): Promise<QueryResult>;
42
+ cachePath(strings: readonly string[], ...params: QueryParam[]): Promise<string>;
44
43
  }
45
44
  declare function revive({ rows, schema, date, ...meta }: SerializableQueryResult): QueryResult;
46
45
  export {};
@@ -30,12 +30,15 @@ class DatabaseClientImpl {
30
30
  });
31
31
  }
32
32
  async sql(strings, ...params) {
33
- const path = `.observable/cache/${await nameHash(this.name)}-${await hash(strings, ...params)}.json`;
33
+ const path = await this.cachePath(strings, ...params);
34
34
  const response = await fetch(path);
35
35
  if (!response.ok)
36
36
  throw new Error(`failed to fetch: ${path}`);
37
37
  return await response.json().then(revive);
38
38
  }
39
+ async cachePath(strings, ...params) {
40
+ return `.observable/cache/${await nameHash(this.name)}-${await hash(strings, ...params)}.json`;
41
+ }
39
42
  }
40
43
  function revive({ rows, schema, date, ...meta }) {
41
44
  for (const column of schema) {
@@ -69,7 +72,6 @@ function revive({ rows, schema, date, ...meta }) {
69
72
  function asDate(value) {
70
73
  return new Date(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?$/.test(value) ? value + "Z" : value);
71
74
  }
72
- DatabaseClient.hash = hash;
73
75
  DatabaseClient.revive = revive;
74
76
  DatabaseClient.prototype = DatabaseClientImpl.prototype; // instanceof
75
77
  Object.defineProperty(DatabaseClientImpl, "name", { value: "DatabaseClient" }); // prevent mangling
@@ -48,12 +48,12 @@ export declare const library: {
48
48
  width: () => AsyncGenerator<number, void, unknown>;
49
49
  DatabaseClient: () => {
50
50
  (name: string, options?: import("./databaseClient.js").QueryOptionsSpec): DatabaseClient;
51
- hash: typeof import("../../lib/hash.js").hash;
52
51
  revive: ({ rows, schema, date, ...meta }: import("../../databases/index.js").SerializableQueryResult) => import("./databaseClient.js").QueryResult;
53
52
  prototype: {
54
53
  readonly name: string;
55
54
  readonly options: import("./databaseClient.js").QueryOptions;
56
55
  sql(strings: readonly string[], ...params: import("./databaseClient.js").QueryParam[]): Promise<import("./databaseClient.js").QueryResult>;
56
+ cachePath(strings: readonly string[], ...params: import("./databaseClient.js").QueryParam[]): Promise<string>;
57
57
  };
58
58
  };
59
59
  FileAttachment: () => {
@@ -1,10 +1,11 @@
1
+ import { fork } from "node:child_process";
1
2
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { readFile } from "node:fs/promises";
3
4
  import { dirname, join, resolve } from "node:path";
4
5
  import { relative } from "node:path/posix";
5
6
  import { fileURLToPath } from "node:url";
6
7
  import { JSDOM } from "jsdom";
7
- import { getDatabase, getDatabaseConfig, getQueryCachePath } from "../databases/index.js";
8
+ import { getQueryCachePath } from "../databases/index.js";
8
9
  import { deserialize } from "../lib/serialize.js";
9
10
  import { Sourcemap } from "../javascript/sourcemap.js";
10
11
  import { transpile } from "../javascript/transpile.js";
@@ -69,16 +70,12 @@ export function observable({ window = new JSDOM().window, parser = new window.DO
69
70
  const dir = dirname(context.filename);
70
71
  const cachePath = await getQueryCachePath(context.filename, cell.database, [value]);
71
72
  if (!existsSync(cachePath)) {
72
- const config = await getDatabaseConfig(context.filename, cell.database);
73
- try {
74
- const database = await getDatabase(config, { cwd: dir });
75
- const results = await database.call(null, [value]);
76
- await mkdir(dirname(cachePath), { recursive: true });
77
- await writeFile(cachePath, JSON.stringify(results));
78
- }
79
- catch (error) {
80
- console.error(error);
81
- }
73
+ const args = ["--root", dir, "--database", cell.database, value];
74
+ const child = fork(fileURLToPath(import.meta.resolve("../../bin/query.js")), args);
75
+ await new Promise((resolve, reject) => {
76
+ child.on("error", reject);
77
+ child.on("exit", resolve);
78
+ });
82
79
  }
83
80
  cell.mode = "js";
84
81
  cell.value = `FileAttachment(${JSON.stringify(relative(dir, cachePath))}).json().then(DatabaseClient.revive)${hidden ? "" : `.then(Inputs.table)${cell.output ? ".then(view)" : ""}`}`;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/observablehq/notebook-kit.git"
7
7
  },
8
- "version": "1.1.0-rc.8",
8
+ "version": "1.1.0",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "test": "vitest",
@@ -67,6 +67,7 @@
67
67
  "@eslint/js": "^9.29.0",
68
68
  "@types/jsdom": "^21.1.7",
69
69
  "@types/markdown-it": "^14.1.2",
70
+ "bun-types": "^1.2.20",
70
71
  "eslint": "^9.29.0",
71
72
  "globals": "^16.2.0",
72
73
  "htl": "^0.3.1",