@observablehq/notebook-kit 2.2.2 → 2.4.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.
- package/dist/package.json +1 -1
- package/dist/src/databases/duckdb.js +3 -1
- package/dist/src/databases/index.d.ts +2 -2
- package/dist/src/databases/postgres.js +3 -1
- package/dist/src/databases/snowflake.js +1 -0
- package/dist/src/databases/sqlite-bun.js +1 -0
- package/dist/src/databases/sqlite-node.js +1 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/javascript/imports.d.ts +6 -3
- package/dist/src/javascript/imports.js +3 -3
- package/dist/src/javascript/template.js +19 -8
- package/dist/src/javascript/transpile.d.ts +3 -4
- package/dist/src/lib/notebook.d.ts +5 -2
- package/dist/src/lib/notebook.js +9 -3
- package/dist/src/lib/serialize.js +4 -0
- package/dist/src/runtime/stdlib/databaseClient.d.ts +6 -5
- package/dist/src/runtime/stdlib/databaseClient.js +1 -1
- package/dist/src/runtime/stdlib/duckdb.js +1 -1
- package/dist/src/runtime/stdlib/index.d.ts +4 -2
- package/dist/src/runtime/stdlib/recommendedLibraries.d.ts +2 -0
- package/dist/src/runtime/stdlib/recommendedLibraries.js +2 -1
- package/dist/src/runtime/stdlib/sql.d.ts +42 -0
- package/dist/src/runtime/stdlib/sql.js +294 -0
- package/dist/src/vite/observable.js +2 -1
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -8,7 +8,9 @@ export default function duckdb({ path, options }) {
|
|
|
8
8
|
let result;
|
|
9
9
|
let rows;
|
|
10
10
|
try {
|
|
11
|
-
result = await connection.run(strings.reduce((p, c, i) => `${p}$${i}${c}`),
|
|
11
|
+
result = await connection.run(strings.reduce((p, c, i) => `${p}$${i}${c}`),
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
params);
|
|
12
14
|
rows = await result.getRowObjectsJson();
|
|
13
15
|
}
|
|
14
16
|
finally {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ColumnSchema
|
|
1
|
+
import type { ColumnSchema } from "../runtime/index.js";
|
|
2
2
|
import type { BigQueryConfig } from "./bigquery.js";
|
|
3
3
|
import type { DatabricksConfig } from "./databricks.js";
|
|
4
4
|
import type { DuckDBConfig } from "./duckdb.js";
|
|
@@ -6,7 +6,7 @@ import type { SQLiteConfig } from "./sqlite.js";
|
|
|
6
6
|
import type { SnowflakeConfig } from "./snowflake.js";
|
|
7
7
|
import type { PostgresConfig } from "./postgres.js";
|
|
8
8
|
export type DatabaseConfig = BigQueryConfig | DatabricksConfig | DuckDBConfig | SQLiteConfig | SnowflakeConfig | PostgresConfig;
|
|
9
|
-
export type QueryTemplateFunction = (strings: readonly string[], ...params:
|
|
9
|
+
export type QueryTemplateFunction = (strings: readonly string[], ...params: unknown[]) => Promise<SerializableQueryResult>;
|
|
10
10
|
export type SerializableQueryResult = {
|
|
11
11
|
rows: Record<string, unknown>[];
|
|
12
12
|
schema: ColumnSchema[];
|
|
@@ -13,7 +13,9 @@ export default function postgres(options) {
|
|
|
13
13
|
const date = new Date();
|
|
14
14
|
let rows;
|
|
15
15
|
try {
|
|
16
|
-
rows = await sql.unsafe(strings.reduce((p, c, i) => `${p}$${i}${c}`),
|
|
16
|
+
rows = await sql.unsafe(strings.reduce((p, c, i) => `${p}$${i}${c}`),
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
params);
|
|
17
19
|
}
|
|
18
20
|
finally {
|
|
19
21
|
await sql.end();
|
|
@@ -6,6 +6,7 @@ export default function sqlite({ path = ":memory:" }) {
|
|
|
6
6
|
const database = new Database(path);
|
|
7
7
|
try {
|
|
8
8
|
const statement = database.prepare(strings.join("?"));
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
10
|
const rows = statement.all(...params);
|
|
10
11
|
return {
|
|
11
12
|
rows,
|
|
@@ -6,6 +6,7 @@ export default function sqlite({ path = ":memory:" }) {
|
|
|
6
6
|
const database = new DatabaseSync(path);
|
|
7
7
|
try {
|
|
8
8
|
const statement = database.prepare(strings.join("?"));
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
10
|
const rows = statement.all(...params);
|
|
10
11
|
return {
|
|
11
12
|
rows,
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -14,13 +14,16 @@ export declare function isImportMetaResolve(node: CallExpression): boolean;
|
|
|
14
14
|
/** Returns true if the given node is an import.meta.url expression. */
|
|
15
15
|
export declare function isImportMetaUrl(node: MemberExpression): boolean;
|
|
16
16
|
export declare function isTypeImport(node: AnyImportSpecifier | ImportDeclaration): boolean;
|
|
17
|
-
export
|
|
17
|
+
export declare function resolveImportDefault(specifier: string): string;
|
|
18
|
+
export type ImportOptions = {
|
|
19
|
+
/** Returns the URL for the given import specifier; defaults to resolveImportDefault. */
|
|
20
|
+
resolveImport?: (specifier: string) => string;
|
|
18
21
|
/** If true, resolve local imports relative to document.baseURI. */
|
|
19
22
|
resolveLocalImports?: boolean;
|
|
20
23
|
};
|
|
21
|
-
export declare function rewriteImportExpressions(output: Sourcemap, body: Node, { resolveLocalImports }?:
|
|
24
|
+
export declare function rewriteImportExpressions(output: Sourcemap, body: Node, { resolveImport, resolveLocalImports }?: ImportOptions): void;
|
|
22
25
|
/** Note: mutates inputs! */
|
|
23
|
-
export declare function rewriteImportDeclarations(output: Sourcemap, body: Node, inputs: string[], { resolveLocalImports }?:
|
|
26
|
+
export declare function rewriteImportDeclarations(output: Sourcemap, body: Node, inputs: string[], { resolveImport, resolveLocalImports }?: ImportOptions): void;
|
|
24
27
|
export declare function getLocalName(node: NamedImportSpecifier): string;
|
|
25
28
|
export declare function getImportedName(node: NamedImportSpecifier): string;
|
|
26
29
|
export declare function flatMapImportSpecifiers<T>(node: ImportDeclaration, f: (node: NamedImportSpecifier) => T): T[];
|
|
@@ -47,10 +47,10 @@ export function isImportMetaUrl(node) {
|
|
|
47
47
|
export function isTypeImport(node) {
|
|
48
48
|
return node.importKind === "type";
|
|
49
49
|
}
|
|
50
|
-
function
|
|
50
|
+
export function resolveImportDefault(specifier) {
|
|
51
51
|
return resolveObservableImport(resolveJsrImport(resolveNpmImport(specifier)));
|
|
52
52
|
}
|
|
53
|
-
export function rewriteImportExpressions(output, body, { resolveLocalImports } = {}) {
|
|
53
|
+
export function rewriteImportExpressions(output, body, { resolveImport = resolveImportDefault, resolveLocalImports } = {}) {
|
|
54
54
|
function rewriteImportSource(source, node = source) {
|
|
55
55
|
const value = getStringLiteralValue(source);
|
|
56
56
|
const resolution = resolveImport(value);
|
|
@@ -79,7 +79,7 @@ export function rewriteImportExpressions(output, body, { resolveLocalImports } =
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
/** Note: mutates inputs! */
|
|
82
|
-
export function rewriteImportDeclarations(output, body, inputs, { resolveLocalImports } = {}) {
|
|
82
|
+
export function rewriteImportDeclarations(output, body, inputs, { resolveImport = resolveImportDefault, resolveLocalImports } = {}) {
|
|
83
83
|
const declarations = [];
|
|
84
84
|
simple(body, {
|
|
85
85
|
ImportDeclaration(node) {
|
|
@@ -121,22 +121,33 @@ function getPrefix(cell) {
|
|
|
121
121
|
? "htl.html"
|
|
122
122
|
: cell.mode;
|
|
123
123
|
}
|
|
124
|
-
function
|
|
124
|
+
function getDatabase(cell) {
|
|
125
125
|
const { id, database = "var:db", since } = cell;
|
|
126
126
|
return database.startsWith("var:")
|
|
127
|
-
?
|
|
128
|
-
: `DatabaseClient(${JSON.stringify(database)}, {id: ${id}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}})
|
|
127
|
+
? database.slice("var:".length)
|
|
128
|
+
: `DatabaseClient(${JSON.stringify(database)}, {id: ${id}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}})`;
|
|
129
|
+
}
|
|
130
|
+
function getSqlPrefix(cell) {
|
|
131
|
+
return `${getDatabase(cell)}.sql`;
|
|
132
|
+
}
|
|
133
|
+
function getSqlSuffix(cell) {
|
|
134
|
+
return cell.hidden ? "" : ".then(Inputs.table)";
|
|
135
|
+
}
|
|
136
|
+
function getSqlViewSuffix(cell) {
|
|
137
|
+
return `.bind(${getDatabase(cell)})`;
|
|
129
138
|
}
|
|
130
139
|
function getInterpreterPrefix(cell) {
|
|
131
140
|
const { id, mode, format, since } = cell;
|
|
132
141
|
return `Interpreter(${JSON.stringify(mode)}, {id: ${id}${format === undefined ? "" : `, format: ${JSON.stringify(format)}`}${since === undefined ? "" : `, since: ${JSON.stringify(since)}`}}).run(`;
|
|
133
142
|
}
|
|
134
143
|
function getSuffix(cell) {
|
|
135
|
-
return cell.mode === "sql"
|
|
136
|
-
?
|
|
137
|
-
:
|
|
138
|
-
?
|
|
139
|
-
:
|
|
144
|
+
return cell.mode === "sql"
|
|
145
|
+
? getSqlSuffix(cell)
|
|
146
|
+
: cell.mode === "sql.view"
|
|
147
|
+
? getSqlViewSuffix(cell)
|
|
148
|
+
: isInterpreter(cell.mode)
|
|
149
|
+
? getInterpreterSuffix(cell)
|
|
150
|
+
: "";
|
|
140
151
|
}
|
|
141
152
|
function getInterpreterSuffix(cell) {
|
|
142
153
|
const method = getInterpreterMethod(cell.format);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Cell } from "../lib/notebook.js";
|
|
2
|
+
import type { ImportOptions } from "./imports.js";
|
|
2
3
|
export type TranspiledJavaScript = {
|
|
3
4
|
/** the source code of a JavaScript function defining the primary variable */
|
|
4
5
|
body: string;
|
|
@@ -21,12 +22,10 @@ export type TranspiledJavaScript = {
|
|
|
21
22
|
/** the names of any referenced secrets */
|
|
22
23
|
secrets?: Set<string>;
|
|
23
24
|
};
|
|
24
|
-
export
|
|
25
|
-
/** If true, resolve local imports paths relative to document.baseURI. */
|
|
26
|
-
resolveLocalImports?: boolean;
|
|
25
|
+
export interface TranspileOptions extends ImportOptions {
|
|
27
26
|
/** If true, resolve file using import.meta.url (so Vite treats it as an asset). */
|
|
28
27
|
resolveFiles?: boolean;
|
|
29
|
-
}
|
|
28
|
+
}
|
|
30
29
|
/** @deprecated */
|
|
31
30
|
export declare function transpile(input: string, mode: Cell["mode"], options?: TranspileOptions): TranspiledJavaScript;
|
|
32
31
|
export declare function transpile(input: Cell, options?: TranspileOptions): TranspiledJavaScript;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type NotebookTheme = "air" | "coffee" | "cotton" | "deep-space" | "glacier" | "ink" | "midnight" | "near-midnight" | "ocean-floor" | "parchment" | "slate" | "stark" | "sun-faded";
|
|
2
|
+
export type CellMode = "js" | "ts" | "ojs" | "md" | "html" | "tex" | "dot" | "sql" | "sql.view" | "node" | "python" | "r";
|
|
3
|
+
export type CellFormat = "text" | "blob" | "buffer" | "json" | "csv" | "tsv" | "jpeg" | "gif" | "webp" | "png" | "arrow" | "parquet" | "html" | "svg" | "xml";
|
|
2
4
|
export type LightDarkNotebookTheme = {
|
|
3
5
|
light: NotebookTheme;
|
|
4
6
|
dark: NotebookTheme;
|
|
@@ -25,7 +27,7 @@ export interface CellSpec {
|
|
|
25
27
|
/** the committed cell value; defaults to empty */
|
|
26
28
|
value?: string;
|
|
27
29
|
/** the mode; affects how the value is evaluated; defaults to js */
|
|
28
|
-
mode?:
|
|
30
|
+
mode?: CellMode;
|
|
29
31
|
/** if true, the editor will stay open when not focused; defaults to false */
|
|
30
32
|
pinned?: boolean;
|
|
31
33
|
/** if true, implicit display will be suppressed; defaults to false */
|
|
@@ -33,7 +35,7 @@ export interface CellSpec {
|
|
|
33
35
|
/** if present, exposes the cell’s value to the rest of the notebook */
|
|
34
36
|
output?: string;
|
|
35
37
|
/** for data loader cells, how the data is represented */
|
|
36
|
-
format?:
|
|
38
|
+
format?: CellFormat;
|
|
37
39
|
/** for SQL cells, the database to query; use var:<name> to refer to a variable */
|
|
38
40
|
database?: string;
|
|
39
41
|
/** for SQL cells, the oldest allowable age of the cached query result */
|
|
@@ -48,4 +50,5 @@ export interface Cell extends CellSpec {
|
|
|
48
50
|
}
|
|
49
51
|
export declare function toNotebook({ cells, title, theme, readOnly }: NotebookSpec): Notebook;
|
|
50
52
|
export declare function toCell({ id, value, mode, pinned, hidden, output, format, database, since }: CellSpec): Cell;
|
|
53
|
+
export declare function isDatabase(mode: Cell["mode"]): mode is "sql" | "sql.view";
|
|
51
54
|
export declare function defaultPinned(mode: Cell["mode"]): boolean;
|
package/dist/src/lib/notebook.js
CHANGED
|
@@ -7,7 +7,7 @@ export function toNotebook({ cells = [], title = "Untitled", theme = "air", read
|
|
|
7
7
|
readOnly
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
|
-
export function toCell({ id, value = "", mode = "js", pinned = defaultPinned(mode), hidden = false, output, format = isInterpreter(mode) ? "buffer" : undefined, database = mode
|
|
10
|
+
export function toCell({ id, value = "", mode = "js", pinned = defaultPinned(mode), hidden = false, output, format = isInterpreter(mode) ? "buffer" : undefined, database = isDatabase(mode) ? "var:db" : undefined, since }) {
|
|
11
11
|
return {
|
|
12
12
|
id,
|
|
13
13
|
value,
|
|
@@ -16,13 +16,19 @@ export function toCell({ id, value = "", mode = "js", pinned = defaultPinned(mod
|
|
|
16
16
|
hidden,
|
|
17
17
|
output,
|
|
18
18
|
format: isInterpreter(mode) ? format : undefined,
|
|
19
|
-
database: mode
|
|
19
|
+
database: isDatabase(mode) ? database : undefined,
|
|
20
20
|
since: since !== undefined ? asDate(since) : undefined
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
+
export function isDatabase(mode) {
|
|
24
|
+
return mode === "sql" || mode === "sql.view";
|
|
25
|
+
}
|
|
26
|
+
function isJavaScript(mode) {
|
|
27
|
+
return mode === "js" || mode === "ts" || mode === "ojs";
|
|
28
|
+
}
|
|
23
29
|
function asDate(date) {
|
|
24
30
|
return date instanceof Date ? date : new Date(date);
|
|
25
31
|
}
|
|
26
32
|
export function defaultPinned(mode) {
|
|
27
|
-
return mode
|
|
33
|
+
return isJavaScript(mode) || isDatabase(mode) || isInterpreter(mode);
|
|
28
34
|
}
|
|
@@ -67,6 +67,8 @@ function serializeMode(mode) {
|
|
|
67
67
|
return "application/x-tex";
|
|
68
68
|
case "sql":
|
|
69
69
|
return "application/sql";
|
|
70
|
+
case "sql.view":
|
|
71
|
+
return "application/sql+view";
|
|
70
72
|
case "dot":
|
|
71
73
|
return "text/vnd.graphviz";
|
|
72
74
|
case "node":
|
|
@@ -93,6 +95,8 @@ function deserializeMode(mode) {
|
|
|
93
95
|
return "tex";
|
|
94
96
|
case "application/sql":
|
|
95
97
|
return "sql";
|
|
98
|
+
case "application/sql+view":
|
|
99
|
+
return "sql.view";
|
|
96
100
|
case "text/vnd.graphviz":
|
|
97
101
|
return "dot";
|
|
98
102
|
case "application/vnd.node.javascript":
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SerializableQueryResult } from "../../databases/index.js";
|
|
2
|
-
/**
|
|
2
|
+
/** @deprecated Use unknown instead. */
|
|
3
3
|
export type QueryParam = any;
|
|
4
4
|
/** @see https://observablehq.com/@observablehq/database-client-specification#%C2%A71 */
|
|
5
5
|
export type QueryResult = Record<string, any>[] & {
|
|
@@ -24,10 +24,11 @@ export interface QueryOptionsSpec {
|
|
|
24
24
|
export interface QueryOptions extends QueryOptionsSpec {
|
|
25
25
|
since?: Date;
|
|
26
26
|
}
|
|
27
|
-
export interface DatabaseClient {
|
|
27
|
+
export interface DatabaseClient<T = QueryResult> {
|
|
28
28
|
readonly name: string;
|
|
29
29
|
readonly options: QueryOptions;
|
|
30
|
-
|
|
30
|
+
readonly dialect?: string;
|
|
31
|
+
sql<S extends T = T>(strings: readonly string[], ...params: unknown[]): Promise<S>;
|
|
31
32
|
}
|
|
32
33
|
export declare const DatabaseClient: {
|
|
33
34
|
(name: string, options?: QueryOptionsSpec): DatabaseClient;
|
|
@@ -38,8 +39,8 @@ declare class DatabaseClientImpl implements DatabaseClient {
|
|
|
38
39
|
readonly name: string;
|
|
39
40
|
readonly options: QueryOptions;
|
|
40
41
|
constructor(name: string, options: QueryOptions);
|
|
41
|
-
sql(strings: readonly string[], ...params:
|
|
42
|
-
cachePath(strings: readonly string[], ...params:
|
|
42
|
+
sql<T = QueryResult>(strings: readonly string[], ...params: unknown[]): Promise<T>;
|
|
43
|
+
cachePath(strings: readonly string[], ...params: unknown[]): Promise<string>;
|
|
43
44
|
}
|
|
44
45
|
declare function revive({ rows, schema, date, ...meta }: SerializableQueryResult): QueryResult;
|
|
45
46
|
export {};
|
|
@@ -34,7 +34,7 @@ class DatabaseClientImpl {
|
|
|
34
34
|
const response = await fetch(path);
|
|
35
35
|
if (!response.ok)
|
|
36
36
|
throw new Error(`failed to fetch: ${path}`);
|
|
37
|
-
return await response.json().then(revive);
|
|
37
|
+
return (await response.json().then(revive));
|
|
38
38
|
}
|
|
39
39
|
async cachePath(strings, ...params) {
|
|
40
40
|
return `.observable/cache/${await nameHash(this.name)}-${await hash(strings, ...params)}.json`;
|
|
@@ -321,7 +321,7 @@ async function insertArrowIpc(database, name, buffer, options) {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
async function insertFlechetteTable(database, name, table, options) {
|
|
324
|
-
const { tableToIPC } = await import("https://cdn.jsdelivr.net/npm/@uwdata/flechette
|
|
324
|
+
const { tableToIPC } = await import("https://cdn.jsdelivr.net/npm/@uwdata/flechette/+esm");
|
|
325
325
|
return await insertArrowIpc(database, name, tableToIPC(table, { format: "stream" }), options);
|
|
326
326
|
}
|
|
327
327
|
async function insertArqueroTable(database, name, source) {
|
|
@@ -28,6 +28,7 @@ export declare const library: {
|
|
|
28
28
|
duckdb: () => Promise<any>;
|
|
29
29
|
DuckDBClient: () => Promise<typeof import("./duckdb.js").DuckDBClient>;
|
|
30
30
|
echarts: () => Promise<any>;
|
|
31
|
+
Flechette: () => Promise<any>;
|
|
31
32
|
htl: () => Promise<any>;
|
|
32
33
|
html: () => Promise<any>;
|
|
33
34
|
svg: () => Promise<any>;
|
|
@@ -39,6 +40,7 @@ export declare const library: {
|
|
|
39
40
|
Plot: () => Promise<any>;
|
|
40
41
|
React: () => Promise<any>;
|
|
41
42
|
ReactDOM: () => Promise<any>;
|
|
43
|
+
sql: () => Promise<typeof import("./sql.js").sql>;
|
|
42
44
|
tex: () => Promise<import("./template.js").RawTemplateRenderer & {
|
|
43
45
|
options: (options?: any) => import("./template.js").RawTemplateRenderer;
|
|
44
46
|
block: import("./template.js").RawTemplateRenderer;
|
|
@@ -54,8 +56,8 @@ export declare const library: {
|
|
|
54
56
|
prototype: {
|
|
55
57
|
readonly name: string;
|
|
56
58
|
readonly options: import("./databaseClient.js").QueryOptions;
|
|
57
|
-
sql(strings: readonly string[], ...params:
|
|
58
|
-
cachePath(strings: readonly string[], ...params:
|
|
59
|
+
sql<T = import("./databaseClient.js").QueryResult>(strings: readonly string[], ...params: unknown[]): Promise<T>;
|
|
60
|
+
cachePath(strings: readonly string[], ...params: unknown[]): Promise<string>;
|
|
59
61
|
};
|
|
60
62
|
};
|
|
61
63
|
FileAttachment: () => {
|
|
@@ -6,6 +6,7 @@ export declare const dot: () => Promise<import("./template.js").RawTemplateRende
|
|
|
6
6
|
export declare const duckdb: () => Promise<any>;
|
|
7
7
|
export declare const DuckDBClient: () => Promise<typeof import("./duckdb.js").DuckDBClient>;
|
|
8
8
|
export declare const echarts: () => Promise<any>;
|
|
9
|
+
export declare const Flechette: () => Promise<any>;
|
|
9
10
|
export declare const htl: () => Promise<any>;
|
|
10
11
|
export declare const html: () => Promise<any>;
|
|
11
12
|
export declare const svg: () => Promise<any>;
|
|
@@ -17,6 +18,7 @@ export declare const mermaid: () => Promise<import("./template.js").AsyncRawTemp
|
|
|
17
18
|
export declare const Plot: () => Promise<any>;
|
|
18
19
|
export declare const React: () => Promise<any>;
|
|
19
20
|
export declare const ReactDOM: () => Promise<any>;
|
|
21
|
+
export declare const sql: () => Promise<typeof import("./sql.js").sql>;
|
|
20
22
|
export declare const tex: () => Promise<import("./template.js").RawTemplateRenderer & {
|
|
21
23
|
options: (options?: any) => import("./template.js").RawTemplateRenderer;
|
|
22
24
|
block: import("./template.js").RawTemplateRenderer;
|
|
@@ -6,6 +6,7 @@ export const dot = () => import("./dot.js").then((_) => _.dot);
|
|
|
6
6
|
export const duckdb = () => import("https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@1.32.0/+esm");
|
|
7
7
|
export const DuckDBClient = () => import("./duckdb.js").then((_) => _.DuckDBClient);
|
|
8
8
|
export const echarts = () => import("https://cdn.jsdelivr.net/npm/echarts/+esm");
|
|
9
|
+
export const Flechette = () => import("https://cdn.jsdelivr.net/npm/@uwdata/flechette/+esm");
|
|
9
10
|
export const htl = () => import("https://cdn.jsdelivr.net/npm/htl/+esm");
|
|
10
11
|
export const html = () => import("https://cdn.jsdelivr.net/npm/htl/+esm").then((_) => _.html);
|
|
11
12
|
export const svg = () => import("https://cdn.jsdelivr.net/npm/htl/+esm").then((_) => _.svg);
|
|
@@ -17,7 +18,7 @@ export const mermaid = () => import("./mermaid.js").then((_) => _.mermaid);
|
|
|
17
18
|
export const Plot = () => import("https://cdn.jsdelivr.net/npm/@observablehq/plot/+esm");
|
|
18
19
|
export const React = () => import("https://cdn.jsdelivr.net/npm/react/+esm");
|
|
19
20
|
export const ReactDOM = () => import("https://cdn.jsdelivr.net/npm/react-dom/+esm");
|
|
20
|
-
|
|
21
|
+
export const sql = () => import("./sql.js").then((_) => _.sql);
|
|
21
22
|
// export const SQLite = () => import("observablehq:stdlib/sqlite").then((_) => _.default);
|
|
22
23
|
// export const SQLiteDatabaseClient = () => import("observablehq:stdlib/sqlite").then((_) => _.SQLiteDatabaseClient);
|
|
23
24
|
export const tex = () => import("./tex.js").then((_) => _.tex);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
type SqlDialect = string;
|
|
2
|
+
interface DatabaseClient {
|
|
3
|
+
readonly dialect?: SqlDialect;
|
|
4
|
+
sql<T>(strings: readonly string[], ...params: unknown[]): Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
export declare function sql(strings: readonly string[], ...params: unknown[]): SqlFragment;
|
|
7
|
+
export declare namespace sql {
|
|
8
|
+
var view: (strings: readonly string[], ...params: unknown[]) => SqlView;
|
|
9
|
+
var variant: (variants: Partial<Record<SqlDialect | "default", unknown>>) => SqlVariant;
|
|
10
|
+
var ident: (name: string) => SqlVariant;
|
|
11
|
+
var text: (value: string) => SqlFragment;
|
|
12
|
+
var Fragment: typeof SqlFragment;
|
|
13
|
+
var View: typeof SqlView;
|
|
14
|
+
var Variant: typeof SqlVariant;
|
|
15
|
+
}
|
|
16
|
+
declare class SqlFragment {
|
|
17
|
+
readonly strings: readonly string[];
|
|
18
|
+
readonly params: unknown[];
|
|
19
|
+
constructor(strings: readonly string[], params: unknown[]);
|
|
20
|
+
flat(dialect?: SqlDialect): typeof this;
|
|
21
|
+
query<T = unknown>(database: DatabaseClient): Promise<T>;
|
|
22
|
+
toDialect(dialect?: SqlDialect): typeof this;
|
|
23
|
+
toString(): string;
|
|
24
|
+
}
|
|
25
|
+
declare class SqlView extends SqlFragment {
|
|
26
|
+
constructor(strings: readonly string[], params: unknown[]);
|
|
27
|
+
bind(database: DatabaseClient): BoundSqlView;
|
|
28
|
+
}
|
|
29
|
+
declare class BoundSqlView extends SqlView {
|
|
30
|
+
readonly database: DatabaseClient;
|
|
31
|
+
constructor(strings: readonly string[], params: unknown[], database: DatabaseClient);
|
|
32
|
+
flat(): typeof this;
|
|
33
|
+
query<T>(): Promise<T>;
|
|
34
|
+
bind(): BoundSqlView;
|
|
35
|
+
}
|
|
36
|
+
declare class SqlVariant {
|
|
37
|
+
readonly variants: Partial<Record<SqlDialect | "default", unknown>>;
|
|
38
|
+
constructor(variants: Partial<Record<SqlDialect | "default", unknown>>);
|
|
39
|
+
toDialect(dialect?: SqlDialect): unknown;
|
|
40
|
+
toString(): string;
|
|
41
|
+
}
|
|
42
|
+
export type { SqlFragment, SqlView, SqlVariant };
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
export function sql(strings, ...params) {
|
|
2
|
+
return new SqlFragment(strings, params);
|
|
3
|
+
}
|
|
4
|
+
sql.view = function view(strings, ...params) {
|
|
5
|
+
return new SqlView(strings, params);
|
|
6
|
+
};
|
|
7
|
+
sql.variant = function variant(variants) {
|
|
8
|
+
return new SqlVariant(variants);
|
|
9
|
+
};
|
|
10
|
+
sql.ident = function ident(name) {
|
|
11
|
+
return new SqlVariant({
|
|
12
|
+
get databricks() {
|
|
13
|
+
return sql([tquote(name)]);
|
|
14
|
+
},
|
|
15
|
+
get bigquery() {
|
|
16
|
+
return sql([tquote(name)]);
|
|
17
|
+
},
|
|
18
|
+
get default() {
|
|
19
|
+
return sql([dquote(name)]);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
sql.text = function text(value) {
|
|
24
|
+
return sql([squote(value)]);
|
|
25
|
+
};
|
|
26
|
+
class SqlFragment {
|
|
27
|
+
constructor(strings, params) {
|
|
28
|
+
Object.defineProperty(this, "strings", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: void 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "params", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
40
|
+
this.strings = strings;
|
|
41
|
+
this.params = params;
|
|
42
|
+
}
|
|
43
|
+
flat(dialect) {
|
|
44
|
+
const iquote = getIquote(dialect);
|
|
45
|
+
const that = this.toDialect(dialect);
|
|
46
|
+
const { strings, params } = that;
|
|
47
|
+
const names = findUndernames(strings, params); // in-use table names, including ctes
|
|
48
|
+
const map = new Map(); // from view to cte name
|
|
49
|
+
const views = {};
|
|
50
|
+
let cteIndex = 0;
|
|
51
|
+
// Bakes any SQL view or fragment params into the query, recursively.
|
|
52
|
+
// Has the side-effect of populating the views map (of CTEs).
|
|
53
|
+
function flatParams(istrings, iparams) {
|
|
54
|
+
let ostrings;
|
|
55
|
+
let oparams;
|
|
56
|
+
for (let i = 0; i < iparams.length; ++i) {
|
|
57
|
+
const param = iparams[i];
|
|
58
|
+
const string = istrings[i + 1];
|
|
59
|
+
if (param instanceof SqlView) {
|
|
60
|
+
if (ostrings === undefined) {
|
|
61
|
+
ostrings = istrings.slice(0, i + 1);
|
|
62
|
+
oparams = iparams.slice(0, i);
|
|
63
|
+
}
|
|
64
|
+
let key = map.get(param);
|
|
65
|
+
if (key == null) {
|
|
66
|
+
do
|
|
67
|
+
key = `_${++cteIndex}`;
|
|
68
|
+
while (names.has(key));
|
|
69
|
+
names.add(key);
|
|
70
|
+
map.set(param, key);
|
|
71
|
+
const [pstrings, pparams] = flatParams(param.strings, param.params);
|
|
72
|
+
views[key] = new SqlFragment(pstrings, pparams);
|
|
73
|
+
}
|
|
74
|
+
ostrings[ostrings.length - 1] += iquote(key) + string;
|
|
75
|
+
}
|
|
76
|
+
else if (param instanceof SqlFragment) {
|
|
77
|
+
if (ostrings === undefined) {
|
|
78
|
+
ostrings = istrings.slice(0, i + 1);
|
|
79
|
+
oparams = iparams.slice(0, i);
|
|
80
|
+
}
|
|
81
|
+
const [pstrings, pparams] = flatParams(param.strings, param.params);
|
|
82
|
+
ostrings[ostrings.length - 1] += pstrings[0];
|
|
83
|
+
ostrings.push(...pstrings.slice(1));
|
|
84
|
+
ostrings[ostrings.length - 1] += string;
|
|
85
|
+
oparams.push(...pparams);
|
|
86
|
+
}
|
|
87
|
+
else if (ostrings !== undefined) {
|
|
88
|
+
oparams.push(param);
|
|
89
|
+
ostrings.push(string);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return ostrings === undefined ? [istrings, iparams] : [ostrings, oparams];
|
|
93
|
+
}
|
|
94
|
+
const [fstrings, fparams] = flatParams(strings, params);
|
|
95
|
+
const [vstrings, vparams] = withViews(fstrings, fparams, views, iquote);
|
|
96
|
+
return vstrings === strings && vparams === params ? that : reconstruct(that, vstrings, vparams);
|
|
97
|
+
}
|
|
98
|
+
query(database) {
|
|
99
|
+
const { strings, params } = this.flat(database.dialect);
|
|
100
|
+
return database.sql(strings, ...params);
|
|
101
|
+
}
|
|
102
|
+
toDialect(dialect) {
|
|
103
|
+
return fragmentToDialect(this, dialect, new Map());
|
|
104
|
+
}
|
|
105
|
+
toString() {
|
|
106
|
+
return this.flat().strings.join("?");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
class SqlView extends SqlFragment {
|
|
110
|
+
constructor(strings, params) {
|
|
111
|
+
super(strings, params);
|
|
112
|
+
}
|
|
113
|
+
bind(database) {
|
|
114
|
+
return new BoundSqlView(this.strings, this.params, database);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
class BoundSqlView extends SqlView {
|
|
118
|
+
constructor(strings, params, database) {
|
|
119
|
+
super(strings, params);
|
|
120
|
+
Object.defineProperty(this, "database", {
|
|
121
|
+
enumerable: true,
|
|
122
|
+
configurable: true,
|
|
123
|
+
writable: true,
|
|
124
|
+
value: void 0
|
|
125
|
+
});
|
|
126
|
+
this.database = database;
|
|
127
|
+
}
|
|
128
|
+
flat() {
|
|
129
|
+
const flat = super.flat(this.database.dialect);
|
|
130
|
+
if (flat !== this)
|
|
131
|
+
flat.database = this.database;
|
|
132
|
+
return flat;
|
|
133
|
+
}
|
|
134
|
+
query() {
|
|
135
|
+
return super.query(this.database);
|
|
136
|
+
}
|
|
137
|
+
bind() {
|
|
138
|
+
throw new Error("already bound");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
class SqlVariant {
|
|
142
|
+
constructor(variants) {
|
|
143
|
+
Object.defineProperty(this, "variants", {
|
|
144
|
+
enumerable: true,
|
|
145
|
+
configurable: true,
|
|
146
|
+
writable: true,
|
|
147
|
+
value: void 0
|
|
148
|
+
});
|
|
149
|
+
this.variants = variants;
|
|
150
|
+
}
|
|
151
|
+
toDialect(dialect) {
|
|
152
|
+
return variantToDialect(this, dialect, new Map());
|
|
153
|
+
}
|
|
154
|
+
toString() {
|
|
155
|
+
return String(this.toDialect());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
sql.Fragment = SqlFragment;
|
|
159
|
+
sql.View = SqlView;
|
|
160
|
+
sql.Variant = SqlVariant;
|
|
161
|
+
function fragmentToDialect(fragment, dialect, cache) {
|
|
162
|
+
const { strings, params } = fragment;
|
|
163
|
+
let ostrings;
|
|
164
|
+
let oparams;
|
|
165
|
+
for (let i = 0; i < params.length; ++i) {
|
|
166
|
+
const param = params[i];
|
|
167
|
+
const string = strings[i + 1];
|
|
168
|
+
if (param instanceof SqlFragment) {
|
|
169
|
+
let dparam;
|
|
170
|
+
if (cache.has(param))
|
|
171
|
+
dparam = cache.get(param);
|
|
172
|
+
else
|
|
173
|
+
cache.set(param, (dparam = fragmentToDialect(param, dialect, cache)));
|
|
174
|
+
if (dparam !== param) {
|
|
175
|
+
if (ostrings === undefined) {
|
|
176
|
+
ostrings = strings.slice(0, i + 1);
|
|
177
|
+
oparams = params.slice(0, i);
|
|
178
|
+
}
|
|
179
|
+
ostrings.push(string);
|
|
180
|
+
oparams.push(dparam);
|
|
181
|
+
}
|
|
182
|
+
else if (ostrings !== undefined) {
|
|
183
|
+
ostrings.push(string);
|
|
184
|
+
oparams.push(param);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
else if (param instanceof SqlVariant) {
|
|
188
|
+
let dparam;
|
|
189
|
+
if (cache.has(param))
|
|
190
|
+
dparam = cache.get(param);
|
|
191
|
+
else
|
|
192
|
+
cache.set(param, (dparam = variantToDialect(param, dialect, cache)));
|
|
193
|
+
if (ostrings === undefined) {
|
|
194
|
+
ostrings = strings.slice(0, i + 1);
|
|
195
|
+
oparams = params.slice(0, i);
|
|
196
|
+
}
|
|
197
|
+
ostrings.push(string);
|
|
198
|
+
oparams.push(dparam);
|
|
199
|
+
}
|
|
200
|
+
else if (ostrings !== undefined) {
|
|
201
|
+
ostrings.push(string);
|
|
202
|
+
oparams.push(param);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return ostrings === undefined ? fragment : reconstruct(fragment, ostrings, oparams);
|
|
206
|
+
}
|
|
207
|
+
function reconstruct(fragment, strings, params) {
|
|
208
|
+
return new fragment.constructor(strings, params);
|
|
209
|
+
}
|
|
210
|
+
function variantToDialect(variant, dialect, cache) {
|
|
211
|
+
let v;
|
|
212
|
+
if (dialect !== undefined && dialect in variant.variants)
|
|
213
|
+
v = variant.variants[dialect];
|
|
214
|
+
else if ("default" in variant.variants)
|
|
215
|
+
v = variant.variants.default;
|
|
216
|
+
else
|
|
217
|
+
throw new Error(dialect ? `missing variant: ${dialect}` : `missing dialect`);
|
|
218
|
+
return v instanceof SqlFragment
|
|
219
|
+
? fragmentToDialect(v, dialect, cache)
|
|
220
|
+
: v instanceof SqlVariant
|
|
221
|
+
? variantToDialect(v, dialect, cache)
|
|
222
|
+
: v;
|
|
223
|
+
}
|
|
224
|
+
// Assumptions:
|
|
225
|
+
// - the views’ names are unique and non-conflicting
|
|
226
|
+
// - the views are defined in topological order
|
|
227
|
+
// - the views do not reference any other views
|
|
228
|
+
// - the views do not reference any other tables
|
|
229
|
+
function withViews(istrings, iparams, views, iquote) {
|
|
230
|
+
const entries = Object.entries(views);
|
|
231
|
+
if (!entries.length)
|
|
232
|
+
return [istrings, iparams];
|
|
233
|
+
const input = istrings[0];
|
|
234
|
+
const withIndex = findWith(input);
|
|
235
|
+
const ostrings = [];
|
|
236
|
+
const oparams = [];
|
|
237
|
+
let first = true;
|
|
238
|
+
ostrings[0] = `${withIndex >= 0 ? input.slice(0, withIndex) : "WITH"}\n`;
|
|
239
|
+
for (const [name, view] of entries) {
|
|
240
|
+
if (view.params.some((p) => p instanceof SqlFragment))
|
|
241
|
+
throw new Error("nested fragment");
|
|
242
|
+
if (first)
|
|
243
|
+
first = false;
|
|
244
|
+
else
|
|
245
|
+
ostrings[ostrings.length - 1] += ",\n";
|
|
246
|
+
ostrings[ostrings.length - 1] += `${iquote(name)} AS (${view.strings[0]}`;
|
|
247
|
+
ostrings.push(...view.strings.slice(1));
|
|
248
|
+
ostrings[ostrings.length - 1] += ")";
|
|
249
|
+
oparams.push(...view.params);
|
|
250
|
+
}
|
|
251
|
+
ostrings[ostrings.length - 1] += withIndex >= 0 ? `,\n${input.slice(withIndex)}` : `\n${input}`;
|
|
252
|
+
ostrings.push(...istrings.slice(1));
|
|
253
|
+
oparams.push(...iparams);
|
|
254
|
+
return [ostrings, oparams];
|
|
255
|
+
}
|
|
256
|
+
function findWith(input) {
|
|
257
|
+
const match = /^\s*(--.*\n|\/\*[\s\S]*?\*\/|\s)*with\b(\s+recursive\b)?/i.exec(input);
|
|
258
|
+
return match ? match[0].length : -1;
|
|
259
|
+
}
|
|
260
|
+
function findUndernames(strings, params, names = new Set()) {
|
|
261
|
+
const string = strings.join(" ");
|
|
262
|
+
const pattern = /\b_\d+\b/g;
|
|
263
|
+
for (let match; (match = pattern.exec(string)) !== null;) {
|
|
264
|
+
names.add(match[0]);
|
|
265
|
+
}
|
|
266
|
+
for (const param of params) {
|
|
267
|
+
if (param instanceof SqlFragment) {
|
|
268
|
+
findUndernames(param.strings, param.params, names);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return names;
|
|
272
|
+
}
|
|
273
|
+
/** Quotes the specified SQL identifier. */
|
|
274
|
+
function getIquote(dialect) {
|
|
275
|
+
switch (dialect) {
|
|
276
|
+
case "databricks":
|
|
277
|
+
case "bigquery":
|
|
278
|
+
return tquote;
|
|
279
|
+
default:
|
|
280
|
+
return dquote;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** Quotes the specified name with double quotes. */
|
|
284
|
+
function dquote(name) {
|
|
285
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
286
|
+
}
|
|
287
|
+
/** Quotes the specified name with backticks. */
|
|
288
|
+
function tquote(name) {
|
|
289
|
+
return `\`${name.replace(/`/g, "``")}\``;
|
|
290
|
+
}
|
|
291
|
+
/** Quotes the specified name with single quotes. */
|
|
292
|
+
function squote(name) {
|
|
293
|
+
return `'${name.replace(/'/g, "''")}'`;
|
|
294
|
+
}
|
|
@@ -8,6 +8,7 @@ import { JSDOM } from "jsdom";
|
|
|
8
8
|
import { getQueryCachePath } from "../databases/index.js";
|
|
9
9
|
import { getInterpreterCachePath, getInterpreterCommand } from "../interpreters/index.js";
|
|
10
10
|
import { getInterpreterMethod, isInterpreter } from "../lib/interpreters.js";
|
|
11
|
+
import { isDatabase } from "../lib/notebook.js";
|
|
11
12
|
import { deserialize } from "../lib/serialize.js";
|
|
12
13
|
import { Sourcemap } from "../javascript/sourcemap.js";
|
|
13
14
|
import { transpile } from "../javascript/transpile.js";
|
|
@@ -68,7 +69,7 @@ export function observable({ window = new JSDOM().window, parser = new window.DO
|
|
|
68
69
|
statics.add(cell);
|
|
69
70
|
div.innerHTML = stripExpressions(template, value);
|
|
70
71
|
}
|
|
71
|
-
else if (mode
|
|
72
|
+
else if (isDatabase(mode) && cell.database && !cell.database.startsWith("var:")) {
|
|
72
73
|
const template = parseTemplate(value);
|
|
73
74
|
if (!template.expressions.length) {
|
|
74
75
|
const dir = dirname(context.filename);
|