@tlockney/cdp-kernel 0.10.0 → 0.12.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.
@@ -1,7 +1,9 @@
1
1
  import type { CommApi, Comms } from "./comm.d.mts";
2
2
  import type { Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch } from "./host.d.mts";
3
3
  import type { Widget, WidgetOptions } from "./widget.d.mts";
4
+ import type { Sql, SqlColumn, SqlFragment, SqlResult } from "./sql.d.mts";
4
5
 
6
+ export type { Sql, SqlColumn, SqlFragment, SqlResult };
5
7
  export type { CommApi, Host, HostCallOptions, HostError, HostErrorCode, HostNotebooks, HostOptions, HostSelf, NotebookEntry, NotebookHit, NotebookSearch, Widget, WidgetOptions };
6
8
 
7
9
  /**
@@ -34,6 +36,25 @@ export const RESULT_MIME: "application/vnd.ganymede.result+json";
34
36
  /** A JSON value, as `JSON.parse` returns it. */
35
37
  export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
36
38
 
39
+ /** Options for `jupyter.table`. */
40
+ export interface TableOptions {
41
+ /** Rows to embed: default 1,000, at most 10,000, and never past 1 MB of JSON. */
42
+ limit?: number;
43
+ /** The columns to show, in order. Names an array of arrays' columns. */
44
+ columns?: string[];
45
+ /** A caption for the table. */
46
+ title?: string;
47
+ }
48
+
49
+ /** Options for `jupyter.chart` with a Vega-Lite or Vega spec. */
50
+ export interface ChartOptions {
51
+ /**
52
+ * Also keep the spec under its Vega MIME type, for a frontend that
53
+ * draws Vega itself. Default `false`: the SVG and a text summary only.
54
+ */
55
+ spec?: boolean;
56
+ }
57
+
37
58
  /** Options for `jupyter.display`. */
38
59
  export interface DisplayOptions {
39
60
  /**
@@ -62,33 +83,6 @@ export interface Displayable {
62
83
  * as `Deno.jupyter` in `deno jupyter`.
63
84
  */
64
85
  export interface JupyterNamespace {
65
- /**
66
- * Send a raw Jupyter message on the kernel's output channel (IOPub), as a
67
- * reply to the running cell. The other helpers are built on it. It does
68
- * not check the message: a wrong field is ignored or mishandled by the
69
- * frontend.
70
- *
71
- * @param msgType An IOPub message type, such as `"clear_output"` or
72
- * `"display_data"`.
73
- * @param content The message content, as the messaging spec defines it
74
- * for `msgType`.
75
- * @param options `metadata` for the message, and binary `buffers`.
76
- * @example
77
- * ```ts
78
- * // Count down in place: clear_output with wait avoids a flicker.
79
- * for (const n of [3, 2, 1]) {
80
- * jupyter.broadcast("clear_output", { wait: true });
81
- * await jupyter.display(jupyter.md`Launching in **${n}**…`);
82
- * await new Promise((resolve) => setTimeout(resolve, 1000));
83
- * }
84
- * ```
85
- * @see jupyter-api.md#raw-messages-with-broadcast
86
- */
87
- broadcast(
88
- msgType: string,
89
- content: object,
90
- options?: { metadata?: object; buffers?: Uint8Array[] },
91
- ): void;
92
86
  /**
93
87
  * Show a value now, in the middle of a cell, or replace one shown
94
88
  * earlier. The value can be anything a cell could end with: a template
@@ -121,22 +115,6 @@ export interface JupyterNamespace {
121
115
  * @see jupyter-api.md#showing-output-with-display
122
116
  */
123
117
  display(obj: unknown, options?: DisplayOptions): Promise<void>;
124
- /**
125
- * The MIME bundle that displaying a value would send, without showing
126
- * anything. For plain values `text/plain` holds terminal color codes,
127
- * as the notebook shows them.
128
- *
129
- * @param obj The value to format.
130
- * @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
131
- * for `42`.
132
- * @example
133
- * ```ts
134
- * const bundle = await jupyter.format(jupyter.md`# Weekly report`);
135
- * bundle["text/markdown"]; // "# Weekly report"
136
- * ```
137
- * @see jupyter-api.md#getting-a-bundle-with-format
138
- */
139
- format(obj: unknown): Promise<MediaBundle>;
140
118
  /**
141
119
  * A tagged template that renders as Markdown. Interpolated values are
142
120
  * inserted as is, without escaping.
@@ -228,6 +206,175 @@ export interface JupyterNamespace {
228
206
  * @see jupyter-api.md#custom-display-for-your-own-classes
229
207
  */
230
208
  $display: typeof $display;
209
+ /**
210
+ * The MIME bundle that displaying a value would send, without showing
211
+ * anything. For plain values `text/plain` holds terminal color codes,
212
+ * as the notebook shows them.
213
+ *
214
+ * @param obj The value to format.
215
+ * @returns The bundle, for example `{ "text/plain": "\x1b[33m42\x1b[39m" }`
216
+ * for `42`.
217
+ * @example
218
+ * ```ts
219
+ * const bundle = await jupyter.format(jupyter.md`# Weekly report`);
220
+ * bundle["text/markdown"]; // "# Weekly report"
221
+ * ```
222
+ * @see jupyter-api.md#getting-a-bundle-with-format
223
+ */
224
+ format(obj: unknown): Promise<MediaBundle>;
225
+ /**
226
+ * Send a raw Jupyter message on the kernel's output channel (IOPub), as a
227
+ * reply to the running cell. The other helpers are built on it. It does
228
+ * not check the message: a wrong field is ignored or mishandled by the
229
+ * frontend.
230
+ *
231
+ * @param msgType An IOPub message type, such as `"clear_output"` or
232
+ * `"display_data"`.
233
+ * @param content The message content, as the messaging spec defines it
234
+ * for `msgType`.
235
+ * @param options `metadata` for the message, and binary `buffers`.
236
+ * @example
237
+ * ```ts
238
+ * // Count down in place: clear_output with wait avoids a flicker.
239
+ * for (const n of [3, 2, 1]) {
240
+ * jupyter.broadcast("clear_output", { wait: true });
241
+ * await jupyter.display(jupyter.md`Launching in **${n}**…`);
242
+ * await new Promise((resolve) => setTimeout(resolve, 1000));
243
+ * }
244
+ * ```
245
+ * @see jupyter-api.md#raw-messages-with-broadcast
246
+ */
247
+ broadcast(
248
+ msgType: string,
249
+ content: object,
250
+ options?: { metadata?: object; buffers?: Uint8Array[] },
251
+ ): void;
252
+ /**
253
+ * Show rows and columns as a table. The output has Ganymede's table
254
+ * type, `application/vnd.dataresource+json` (Table Schema fields, up to
255
+ * `limit` rows as JSON, and the total row count), which the Ganymede
256
+ * apps show with sorting and paging; a plain HTML table of the first
257
+ * and last rows for JupyterLab and VS Code; and a text summary of each
258
+ * column's type and range for agents and consoles.
259
+ *
260
+ * An Arrow table, a Polars DataFrame or a DuckDB result also shows this
261
+ * way when it is a cell's value; pass one here to set `limit`,
262
+ * `columns` or `title`.
263
+ *
264
+ * @param value An array of row objects, an array of arrays (name them
265
+ * with `columns`), an Arrow table (apache-arrow, duckdb-wasm), a
266
+ * Polars DataFrame, or a `@duckdb/node-api` result.
267
+ * @param options `limit`: rows to embed, default 1,000, at most 10,000,
268
+ * and never more than 1 MB of JSON. `columns`: which columns, in order.
269
+ * `title`: a caption.
270
+ * @throws {TypeError} `value` isn't one of those, or `limit` isn't a
271
+ * whole number.
272
+ * @example
273
+ * ```ts
274
+ * const moons = [
275
+ * { moon: "Io", radius_km: 1821.6, discovered: new Date("1610-01-08") },
276
+ * { moon: "Europa", radius_km: 1560.8, discovered: new Date("1610-01-08") },
277
+ * { moon: "Ganymede", radius_km: 2634.1, discovered: new Date("1610-01-07") },
278
+ * ];
279
+ * jupyter.table(moons, { title: "Galilean moons" });
280
+ * ```
281
+ * @see jupyter-api.md#tables
282
+ */
283
+ table(value: unknown, options?: TableOptions): Displayable;
284
+ /**
285
+ * Run SQL on an in-memory [DuckDB](https://duckdb.org) database, as a
286
+ * tagged template: each `${value}` becomes a bound parameter, never
287
+ * query text, so values can't change the query. Resolves to a
288
+ * `SqlResult`, which shows as a table and has `columns` and `rows`.
289
+ *
290
+ * The database opens on the first query and lasts until the kernel
291
+ * restarts or `jupyter.sql.close()`. DuckDB reads CSV, Parquet and JSON
292
+ * files by path or URL; `jupyter.sql.load` makes a table from rows the
293
+ * notebook already has. An interrupt stops a running query.
294
+ *
295
+ * @throws {TypeError} Called as a function rather than a tag.
296
+ * @availability Node, Deno and Bun use `@duckdb/node-api`; the browser
297
+ * target (the Ganymede apps) uses duckdb-wasm from a CDN. Not on
298
+ * workerd or celld.
299
+ * @example
300
+ * ```ts
301
+ * const min = 2000;
302
+ * await jupyter.sql`
303
+ * select moon, radius_km
304
+ * from (values ('Io', 1821.6), ('Ganymede', 2634.1), ('Callisto', 2410.3)) as m(moon, radius_km)
305
+ * where radius_km > ${min}
306
+ * order by radius_km desc`;
307
+ * ```
308
+ * @see jupyter-api.md#sql-with-duckdb
309
+ */
310
+ readonly sql: Sql;
311
+ /**
312
+ * Draw a chart in the kernel as one SVG, with its title and legend
313
+ * inside, plus a text summary for readers that can't see it. The SVG
314
+ * shows the same in the Ganymede apps, JupyterLab, VS Code and a
315
+ * published page. Text and axes are drawn in `currentColor`: the
316
+ * notebook's text color in the Ganymede apps, black in JupyterLab.
317
+ *
318
+ * Pass a [Vega-Lite](https://vega.github.io/vega-lite/) or Vega spec
319
+ * (or a vega-lite-api chart), or the
320
+ * [Observable Plot](https://observablehq.com/plot/) module and the
321
+ * options you would give `Plot.plot`. Plot's `title`, `subtitle`,
322
+ * `caption` and the color legend (`legend: true`, `"swatches"` or
323
+ * `"ramp"`) are drawn into the SVG; other scales' legends are left
324
+ * out. An SVG element shows as it is.
325
+ *
326
+ * vega and vega-lite (version 6), and linkedom for Plot where there is
327
+ * no DOM, load through the cell's import on first use. A chart past
328
+ * 2 MB of SVG logs a warning: aggregate or sample first.
329
+ *
330
+ * @param chart A Vega-Lite or Vega spec, the Plot module, or an SVG
331
+ * element.
332
+ * @param options With a spec, `ChartOptions`; with Plot, Plot's options.
333
+ * @throws {TypeError} `chart` is none of those, or Plot comes without
334
+ * options.
335
+ * @example
336
+ * ```ts
337
+ * jupyter.chart({
338
+ * title: "Galilean moons",
339
+ * data: { values: [
340
+ * { moon: "Io", radius_km: 1821.6 },
341
+ * { moon: "Europa", radius_km: 1560.8 },
342
+ * { moon: "Ganymede", radius_km: 2634.1 },
343
+ * { moon: "Callisto", radius_km: 2410.3 },
344
+ * ] },
345
+ * mark: "bar",
346
+ * encoding: {
347
+ * x: { field: "moon", sort: "-y" },
348
+ * y: { field: "radius_km", type: "quantitative" },
349
+ * },
350
+ * });
351
+ * ```
352
+ * @see jupyter-api.md#charts
353
+ */
354
+ chart(chart: object, options?: ChartOptions | Record<string, unknown>): Displayable;
355
+ /**
356
+ * An `AbortSignal` that an interrupt aborts, so code waiting on something
357
+ * only it can end can stop and clean up. Each cell gets its own, as does
358
+ * a comm handler that starts while no cell runs. Read it once when the
359
+ * work starts: a later read gets the signal of whatever started last.
360
+ * The abort reason is a `DOMException` named `AbortError`.
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
365
+ * ```
366
+ * @example
367
+ * ```ts
368
+ * const signal = jupyter.signal;
369
+ * const ask = jupyter.comm.open("my.app", { question: "ready?" });
370
+ * const answer = await new Promise((resolve, reject) => {
371
+ * ask.onMsg((m) => resolve(m.data));
372
+ * signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
373
+ * });
374
+ * ```
375
+ * @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
376
+ */
377
+ readonly signal: AbortSignal;
231
378
  /**
232
379
  * Jupyter comms: message channels to the frontend outside cell
233
380
  * execution. Its methods are in the widgets and comms reference.
@@ -236,13 +383,14 @@ export interface JupyterNamespace {
236
383
  */
237
384
  comm: CommApi;
238
385
  /**
239
- * Create an anywidget-compatible widget model. Display it (or end the
386
+ * Create an [anywidget](https://anywidget.dev)-compatible [widget
387
+ * model](widgets-reference.md#widget-model). Display it (or end the
240
388
  * cell with it) to show a view.
241
389
  *
242
390
  * @param options The view module, its CSS, and the initial state.
243
391
  * @returns The model, synced with every view of it.
244
392
  * @throws {TypeError} `options.esm` is not a string.
245
- * @availability The frontend needs the anywidget extension.
393
+ * @availability The frontend needs the [anywidget](https://anywidget.dev) extension.
246
394
  * @example
247
395
  * ```ts
248
396
  * const counter = jupyter.widget({
@@ -269,29 +417,6 @@ export interface JupyterNamespace {
269
417
  * @see widgets.md#a-first-widget-a-counter
270
418
  */
271
419
  widget(options: WidgetOptions): Widget;
272
- /**
273
- * An `AbortSignal` that an interrupt aborts, so code waiting on something
274
- * only it can end can stop and clean up. Each cell gets its own, as does
275
- * a comm handler that starts while no cell runs. Read it once when the
276
- * work starts: a later read gets the signal of whatever started last.
277
- * The abort reason is a `DOMException` named `AbortError`.
278
- *
279
- * @example
280
- * ```ts
281
- * const response = await fetch("https://example.com/large.csv", { signal: jupyter.signal });
282
- * ```
283
- * @example
284
- * ```ts
285
- * const signal = jupyter.signal;
286
- * const ask = jupyter.comm.open("my.app", { question: "ready?" });
287
- * const answer = await new Promise((resolve, reject) => {
288
- * ask.onMsg((m) => resolve(m.data));
289
- * signal.addEventListener("abort", () => { ask.close(); reject(signal.reason); }, { once: true });
290
- * });
291
- * ```
292
- * @see jupyter-api.md#stopping-on-interrupt-jupyter-signal
293
- */
294
- readonly signal: AbortSignal;
295
420
  /**
296
421
  * The parameters of this run: a frozen object, empty unless whoever
297
422
  * started the kernel passed some, such as `ganymede execute -p
@@ -4,19 +4,27 @@
4
4
  //
5
5
  // Ported from Deno's cli/js/40_jupyter.js (v2.9.6), which is
6
6
  // Copyright 2018-2026 the Deno authors, MIT license. The formatting rules
7
- // (display symbol, canvas, Vega, Polars-style DataFrames, JPEG/PNG bytes,
8
- // HTML/SVG elements) are unchanged; the Deno internals it used are injected:
7
+ // (display symbol, canvas, Vega, JPEG/PNG bytes, HTML/SVG elements) are
8
+ // unchanged; Polars-style DataFrames show through table.mjs instead (#475),
9
+ // as do Arrow tables and DuckDB results. The Deno internals it used are
10
+ // injected:
9
11
  // Deno[Deno.internal].inspectArgs -> host.inspect
10
12
  // op_base64_encode_from_buffer -> host.base64
11
13
  // Deno.readFileSync -> host.readFile
12
14
  // op_jupyter_broadcast -> host.send
13
15
  // WebGPU texture/buffer display depends on Deno's wgpu bindings and is
14
- // omitted. Comms and widgets (jupyter.comm, jupyter.widget) are additions
15
- // Deno doesn't have; see comm.mjs and widget.mjs.
16
+ // omitted. Comms, widgets, tables, SQL and charts (jupyter.comm,
17
+ // jupyter.widget, jupyter.table, jupyter.sql, jupyter.chart) are additions
18
+ // Deno doesn't have; see comm.mjs, widget.mjs, table.mjs, sql.mjs and
19
+ // chart.mjs.
16
20
 
17
21
  import { createComms } from "./comm.mjs";
18
22
  import { createHost } from "./host.mjs";
19
23
  import { createWidgetFactory } from "./widget.mjs";
24
+ import { createSql } from "./sql.mjs";
25
+ import { createChart } from "./chart.mjs";
26
+ import { isTableLike, pageTable, tableBundle, tableSource } from "./table.mjs";
27
+ import { createTablePager } from "./table-paging.mjs";
20
28
 
21
29
  export const $display = Symbol.for("Jupyter.display");
22
30
 
@@ -243,66 +251,6 @@ function summarizeChart(spec) {
243
251
  }
244
252
  }
245
253
 
246
- function isDataFrameLike(obj) {
247
- return obj !== null && typeof obj === "object" &&
248
- obj.schema !== undefined && typeof obj.schema === "object" &&
249
- typeof obj.head === "function" && typeof obj.toRecords === "function";
250
- }
251
-
252
- function mapPolarsTypeToJSONSchema(colType) {
253
- const typeMapping = {
254
- Null: "null",
255
- Bool: "boolean",
256
- Int8: "integer",
257
- Int16: "integer",
258
- Int32: "integer",
259
- Int64: "integer",
260
- UInt8: "integer",
261
- UInt16: "integer",
262
- UInt32: "integer",
263
- UInt64: "integer",
264
- Float32: "number",
265
- Float64: "number",
266
- Date: "string",
267
- Datetime: "string",
268
- Utf8: "string",
269
- Categorical: "string",
270
- List: "array",
271
- Struct: "object",
272
- };
273
- // Polars DataTypes only expose their name through toJSON().
274
- const dataType = typeof colType?.toJSON === "function" ? colType.toJSON()["DataType"] : undefined;
275
- return typeMapping[dataType] || "string";
276
- }
277
-
278
- function extractDataFrame(df) {
279
- const fields = [];
280
- for (const [name, colType] of Object.entries(df.schema)) {
281
- fields.push({ name, type: mapPolarsTypeToJSONSchema(colType) });
282
- }
283
- const data = df.head(50).toRecords();
284
- let html = "<table><thead><tr>";
285
- for (const field of fields) html += `<th>${escapeHTML(String(field.name))}</th>`;
286
- html += "</tr></thead><tbody>";
287
- for (const row of df.head(10).toRecords()) {
288
- html += "<tr>";
289
- for (const field of fields) html += `<td>${escapeHTML(String(row[field.name]))}</td>`;
290
- html += "</tr>";
291
- }
292
- html += "</tbody></table>";
293
- // Polars has `height`; the row count is left out when there isn't one.
294
- const size = [];
295
- if (Number.isInteger(df.height)) size.push(`${df.height} ${df.height === 1 ? "row" : "rows"}`);
296
- size.push(`${fields.length} ${fields.length === 1 ? "column" : "columns"}`);
297
- const columns = fields.slice(0, 20).map((field) => `${clip(String(field.name), 40)} (${field.type})`);
298
- if (fields.length > 20) columns.push(`… ${fields.length - 20} more`);
299
- return {
300
- "application/vnd.dataresource+json": { data, schema: { fields } },
301
- "text/html": html,
302
- "text/plain": `DataFrame (${size.join(", ")})${columns.length ? `: ${columns.join(", ")}` : ""}`,
303
- };
304
- }
305
-
306
254
  function isCanvasLike(obj) {
307
255
  return obj !== null && typeof obj === "object" && "toDataURL" in obj;
308
256
  }
@@ -377,7 +325,8 @@ export function createJupyter(host) {
377
325
  const vega = extractVega(obj);
378
326
  if (vega) return vega;
379
327
  }
380
- if (isDataFrameLike(obj)) return extractDataFrame(obj);
328
+ // Arrow tables, Polars DataFrames and DuckDB results (#475).
329
+ if (isTableLike(obj)) return tableBundle(tableSource(obj));
381
330
  if (isJpg(obj)) return { "image/jpeg": host.base64(obj) };
382
331
  if (isPng(obj)) return { "image/png": host.base64(obj) };
383
332
  if (isSVGElementLike(obj)) return { "image/svg+xml": obj.outerHTML };
@@ -409,14 +358,25 @@ export function createJupyter(host) {
409
358
  );
410
359
  }
411
360
 
361
+ // jupyter.table (#475): rows and columns as Ganymede's table output,
362
+ // built now so a bad value fails the call, not the display.
363
+ function table(value, options = {}) {
364
+ const bundle = tableBundle(tableSource(value, options), options);
365
+ return { [$display]: () => bundle };
366
+ }
367
+
412
368
  function broadcast(msgType, content, { metadata = {}, buffers = [] } = {}) {
413
369
  host.send(msgType, content, metadata, buffers);
414
370
  }
415
371
 
372
+ // Set below, once the comms are: a table cut by the cap is kept for
373
+ // paging when it is sent as an output, not when it is formatted.
374
+ let pager;
375
+
416
376
  async function display(obj, options = { raw: false, update: false }) {
417
377
  const bundle = options.raw && isMediaBundle(obj) ? obj : await format(obj);
418
378
  broadcast(options.update ? "update_display_data" : "display_data", {
419
- data: bundle,
379
+ data: pageTable(bundle, pager, options.display_id),
420
380
  metadata: {},
421
381
  transient: options.display_id ? { display_id: options.display_id } : {},
422
382
  });
@@ -427,7 +387,7 @@ export function createJupyter(host) {
427
387
  try {
428
388
  broadcast("execute_result", {
429
389
  execution_count: executionCount,
430
- data: await format(result),
390
+ data: pageTable(await format(result), pager),
431
391
  metadata: {},
432
392
  // nbclient crashes on a null `transient`; Deno learned this too.
433
393
  transient: {},
@@ -492,7 +452,24 @@ export function createJupyter(host) {
492
452
  });
493
453
  }
494
454
 
455
+ // The cell's own import, so npm: resolution and the browser target's CDN
456
+ // apply to the packages jupyter.sql and jupyter.chart load.
457
+ const cellImport = (what) => (specifier) => {
458
+ const load = globalThis.__cdpk?.import;
459
+ if (typeof load !== "function") return Promise.reject(new Error(`${what}: no module loader in this runtime`));
460
+ return load(specifier);
461
+ };
462
+
463
+ // jupyter.sql (#477): DuckDB, loaded on the first query; an interrupt
464
+ // (jupyter.signal) interrupts the query.
465
+ const sql = createSql({ importModule: cellImport("jupyter.sql"), signal: () => current.signal });
466
+
467
+ // jupyter.chart (#479): vega, vega-lite or linkedom, loaded on first use.
468
+ const chart = createChart({ importModule: cellImport("jupyter.chart"), summarizeSpec: summarizeChart });
469
+
495
470
  const comms = createComms((msgType, content, metadata, buffers) => host.send(msgType, content, metadata, buffers));
471
+ // Tables cut by the cap page through the kernel over a comm (ganymede#747).
472
+ pager = createTablePager(comms.api);
496
473
 
497
474
  // jupyter.signal (#418): aborted when the kernel is interrupted, so cell
498
475
  // code can stop waiting on what the interrupt can't reach, such as a comm
@@ -546,6 +523,9 @@ export function createJupyter(host) {
546
523
  return readParams();
547
524
  },
548
525
  result,
526
+ table,
527
+ sql,
528
+ chart,
549
529
  format,
550
530
  md: taggedTemplate("text/markdown"),
551
531
  html: taggedTemplate("text/html"),
@@ -103,7 +103,7 @@ export interface Host {
103
103
  call(method: string, params?: HostParams, options?: HostCallOptions): Promise<any>;
104
104
  /** Other notebooks under the server's root, as far as this notebook's grant reaches. */
105
105
  readonly notebooks: HostNotebooks;
106
- /** This notebook's own cells. Not on a server yet: they reject `unavailable`. */
106
+ /** This notebook's own cells. Ganymede 0.8.0 or later; an older server rejects `unavailable`. */
107
107
  readonly self: HostSelf;
108
108
  }
109
109
 
@@ -160,7 +160,8 @@ export interface HostNotebooks {
160
160
  options?: HostCallOptions,
161
161
  ): Promise<NotebookSearch>;
162
162
  /**
163
- * A notebook's named results. Not on a server yet: rejects `unavailable`.
163
+ * A notebook's named results. Ganymede 0.8.0 or later; an older server
164
+ * rejects `unavailable`.
164
165
  *
165
166
  * @param params `path`: the notebook.
166
167
  * @param options `signal` cancels the call.
@@ -172,8 +173,9 @@ export interface HostNotebooks {
172
173
  */
173
174
  results(params: { path: string }, options?: HostCallOptions): Promise<any>;
174
175
  /**
175
- * Run a notebook with parameters and return its results. Not on a
176
- * server yet: rejects `unavailable`.
176
+ * Run a notebook with parameters and return its results, in a fresh
177
+ * kernel, when this notebook's grant names it. Ganymede 0.8.0 or later;
178
+ * an older server rejects `unavailable`.
177
179
  *
178
180
  * @param params `path`: the notebook; `params`: its parameters.
179
181
  * @param options `signal` cancels the run.
@@ -186,7 +188,7 @@ export interface HostNotebooks {
186
188
  run(params: { path: string; params?: HostParams }, options?: HostCallOptions): Promise<any>;
187
189
  }
188
190
 
189
- /** `host.self`: this notebook's own cells. Not on a server yet: each rejects `unavailable`. */
191
+ /** `host.self`: this notebook's own cells. Ganymede 0.8.0 or later; an older server rejects each with `unavailable`. */
190
192
  export interface HostSelf {
191
193
  /**
192
194
  * Add a cell to this notebook.
@@ -0,0 +1,147 @@
1
+ import type { Displayable } from "./display.d.mts";
2
+
3
+ export const NODE_API: string;
4
+ export const DUCKDB_WASM: string;
5
+
6
+ /** One column of a query's result. */
7
+ export interface SqlColumn {
8
+ name: string;
9
+ /** DuckDB's type name (`BIGINT`, `VARCHAR`, `DATE`), or Arrow's in the browser. */
10
+ type: string;
11
+ }
12
+
13
+ /**
14
+ * A query's result. It shows as a table (the same output as
15
+ * `jupyter.table`) when it is a cell's value or passed to
16
+ * `jupyter.display`.
17
+ */
18
+ export interface SqlResult extends Displayable {
19
+ /** The result's columns, in order. */
20
+ readonly columns: SqlColumn[];
21
+ /**
22
+ * The rows as plain objects keyed by column name. Integers that fit a
23
+ * JavaScript number are numbers, larger ones `bigint`; `DATE` and
24
+ * `TIMESTAMP` values are `Date`s.
25
+ */
26
+ readonly rows: Record<string, unknown>[];
27
+ }
28
+
29
+ export const SqlResult: new (columns: SqlColumn[], rows: Record<string, unknown>[]) => SqlResult & {
30
+ toTableSource(names?: string[]): import("./table.d.mts").TableSource;
31
+ };
32
+
33
+ /** Text that `jupyter.sql` puts into the query as written: see `sql.raw`. */
34
+ export interface SqlFragment {
35
+ readonly __sqlRaw: never;
36
+ }
37
+
38
+ /**
39
+ * `jupyter.sql`: a tagged template that runs SQL on the kernel's DuckDB.
40
+ * Each `${value}` becomes a bound parameter.
41
+ */
42
+ export interface Sql {
43
+ /**
44
+ * Run the query, with each `${value}` as a bound parameter.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * await jupyter.sql`select ${"Ganymede"} as moon`;
49
+ * ```
50
+ */
51
+ (strings: TemplateStringsArray, ...values: unknown[]): Promise<SqlResult>;
52
+ /**
53
+ * Run SQL from a string, with values for its `?` parameters. For a
54
+ * query built at run time; prefer the tag otherwise.
55
+ *
56
+ * @param text The SQL.
57
+ * @param params Values for its `?` placeholders, in order.
58
+ * @example
59
+ * ```ts
60
+ * const op = ">";
61
+ * await jupyter.sql.query(`select 2 ${op} ? as answer`, [1]);
62
+ * ```
63
+ */
64
+ query(text: string, params?: unknown[]): Promise<SqlResult>;
65
+ /**
66
+ * Text the tag puts into the query as written, rather than as a
67
+ * parameter: a table name or a file path built in code. Never pass it
68
+ * text from outside the notebook.
69
+ *
70
+ * @param text SQL text.
71
+ * @example
72
+ * ```ts
73
+ * const table = "moons";
74
+ * await jupyter.sql`select count(*) as n from ${jupyter.sql.raw(table)}`;
75
+ * ```
76
+ */
77
+ raw(text: string): SqlFragment;
78
+ /**
79
+ * Create (or replace) table `name` from a file DuckDB can read (CSV,
80
+ * Parquet or JSON, by path or URL), from an array of row objects, or
81
+ * from an Arrow table or Polars DataFrame. Rows go in as one JSON
82
+ * parameter, typed from the values: integers as `BIGINT`, numbers as
83
+ * `DOUBLE`, `Date`s as `TIMESTAMP`, arrays and objects as `JSON`.
84
+ *
85
+ * @param name The table's name.
86
+ * @param source A path or URL, rows, an Arrow table or a DataFrame.
87
+ * @throws {TypeError} `name` is empty, or the rows have no columns.
88
+ * @example
89
+ * ```ts
90
+ * await jupyter.sql.load("moons", [
91
+ * { moon: "Io", radius_km: 1821.6 },
92
+ * { moon: "Ganymede", radius_km: 2634.1 },
93
+ * ]);
94
+ * await jupyter.sql`select moon from moons order by radius_km desc`;
95
+ * ```
96
+ */
97
+ load(name: string, source: unknown): Promise<void>;
98
+ /**
99
+ * Close the database and free its memory. The next query opens a new,
100
+ * empty one.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * await jupyter.sql.close();
105
+ * ```
106
+ */
107
+ close(): Promise<void>;
108
+ /**
109
+ * The DuckDB connection itself, for what the tag doesn't cover: a
110
+ * `@duckdb/node-api` `DuckDBConnection` on Node, Deno and Bun, a
111
+ * duckdb-wasm `AsyncDuckDBConnection` in the browser.
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const conn = await jupyter.sql.connection();
116
+ * ```
117
+ */
118
+ connection(): Promise<unknown>;
119
+ }
120
+
121
+ export function decimalValue(words: ArrayLike<number> | number | bigint, scale: number): number;
122
+ export function toParam(value: unknown): unknown;
123
+ /** A parameter as a SQL literal. */
124
+ export function sqlLiteral(value: unknown): string;
125
+ /** The query with each `?` placeholder (outside strings, quoted names and comments) replaced by its parameter as a literal. */
126
+ export function inlineParams(text: string, params: unknown[]): string;
127
+ /** A DuckDB TIME as `HH:MM:SS[.fraction]`, from its count in the declared type's unit (microseconds by default). */
128
+ export function timeText(value: number | bigint, declared?: string): string;
129
+ /** Column names made unique: a repeated `id` becomes `id_1`, `id_2`, and so on. */
130
+ export function uniqueNames(names: readonly string[]): string[];
131
+ export function compose(strings: readonly string[], values: unknown[]): { text: string; params: unknown[] };
132
+ export function quoteIdent(name: string): string;
133
+ export function quoteString(text: string): string;
134
+ export function rowsAsJson(source: unknown): { structure: string | null; json: string };
135
+
136
+ export interface SqlEngine {
137
+ query: (text: string, params: unknown[]) => Promise<SqlResult>;
138
+ interrupt?: () => void;
139
+ close: () => Promise<void>;
140
+ connection: unknown;
141
+ }
142
+
143
+ export function createSql(options: {
144
+ importModule: (specifier: string) => Promise<any>;
145
+ signal?: () => AbortSignal | undefined;
146
+ openEngine?: () => Promise<SqlEngine>;
147
+ }): Sql;