@tlockney/cdp-kernel 0.9.0 → 0.11.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.
@@ -0,0 +1,248 @@
1
+ import type { CommApi } from "./comm.d.mts";
2
+
3
+ /** The target name of the comm a Ganymede server opens to the kernel. */
4
+ export const HOST_TARGET: "ganymede.host.v1";
5
+
6
+ /** Why a host call failed. The codes are ganymede-device's. */
7
+ export type HostErrorCode = "unavailable" | "denied" | "cancelled" | "timeout" | "failed" | "disconnected";
8
+
9
+ /**
10
+ * A failed host call. Branch on `code` (or `name === "HostError"`): the
11
+ * class isn't a global, so `instanceof` isn't available in a cell.
12
+ *
13
+ * A failed host call: `unavailable` (no such method), `denied` (not
14
+ * granted to this notebook), `cancelled` (its signal aborted), `timeout`,
15
+ * `failed` (the server couldn't do it), `disconnected` (the host comm
16
+ * closed, such as on a server restart).
17
+ */
18
+ export class HostError extends Error {
19
+ readonly code: HostErrorCode;
20
+ constructor(code: HostErrorCode, message: string);
21
+ static from(err: unknown): HostError;
22
+ }
23
+
24
+ /** Options for a host call. */
25
+ export interface HostCallOptions {
26
+ /** Aborting it cancels the call. Default `jupyter.signal`. */
27
+ signal?: AbortSignal;
28
+ }
29
+
30
+ /** Parameters and results are JSON; the host API's design note lists each method's. */
31
+ export type HostParams = { [key: string]: unknown };
32
+
33
+ /** An entry `notebooks.list` returns: a folder, or a notebook this notebook may read. */
34
+ export interface NotebookEntry {
35
+ path: string;
36
+ type: "directory" | "notebook";
37
+ /** Its first heading, for a notebook that has one. */
38
+ title?: string | null;
39
+ /** Its kernelspec name. */
40
+ kernel?: string | null;
41
+ /** ISO 8601. */
42
+ modified: string;
43
+ }
44
+
45
+ /** A cell `notebooks.search` matched. */
46
+ export interface NotebookHit {
47
+ path: string;
48
+ title: string | null;
49
+ /** The cell's position in the notebook. */
50
+ index: number;
51
+ id: string | null;
52
+ cell_type: "code" | "markdown" | "raw";
53
+ /** Where the first word matched. */
54
+ in: "source" | "output";
55
+ excerpt: string;
56
+ }
57
+
58
+ /** What `notebooks.search` returns. */
59
+ export interface NotebookSearch {
60
+ hits: NotebookHit[];
61
+ /** Matching cells, beyond `limit` too. */
62
+ total: number;
63
+ truncated: boolean;
64
+ }
65
+
66
+ /** The Ganymede server that runs this kernel, from `jupyter.host()`. */
67
+ export interface Host {
68
+ /** The host API's protocol version. */
69
+ readonly protocol: number;
70
+ /** The server's version. */
71
+ readonly server: string;
72
+ /** This notebook's path under the server's root, or null when the kernel has none. */
73
+ readonly notebook: string | null;
74
+ /** The methods this notebook may call. */
75
+ readonly methods: readonly string[];
76
+ /**
77
+ * Whether this notebook may call `method`.
78
+ *
79
+ * @param method The method, such as `"notebooks.run"`.
80
+ * @example
81
+ * ```ts
82
+ * const host = await jupyter.host();
83
+ * host?.has("notebooks.run"); // false unless the notebook is granted runs
84
+ * ```
85
+ */
86
+ has(method: string): boolean;
87
+ /**
88
+ * Call a method by name. The shortcuts under `notebooks` and `self` call
89
+ * this.
90
+ *
91
+ * @param method The method, such as `"notebooks.read"`.
92
+ * @param params Its parameters, a JSON object. Default `{}`.
93
+ * @param options `signal` cancels the call.
94
+ * @returns What the server answered, as JSON.
95
+ * @throws {HostError} The call failed; `code` says why.
96
+ * @example
97
+ * ```ts
98
+ * const host = await jupyter.host();
99
+ * const { notebook } = await host!.call("notebooks.read", { path: "journal/a.ipynb" });
100
+ * notebook.cells.length;
101
+ * ```
102
+ */
103
+ call(method: string, params?: HostParams, options?: HostCallOptions): Promise<any>;
104
+ /** Other notebooks under the server's root, as far as this notebook's grant reaches. */
105
+ readonly notebooks: HostNotebooks;
106
+ /** This notebook's own cells. Ganymede 0.8.0 or later; an older server rejects `unavailable`. */
107
+ readonly self: HostSelf;
108
+ }
109
+
110
+ /** `host.notebooks`: other notebooks under the server's root. */
111
+ export interface HostNotebooks {
112
+ /**
113
+ * The notebooks this notebook may read, and the folders, in a folder.
114
+ *
115
+ * @param params `dir`: the folder, relative to the root. Default the root.
116
+ * @param options `signal` cancels the call.
117
+ * @returns One entry per notebook or folder.
118
+ * @throws {HostError} `denied` when the grant doesn't reach `dir`.
119
+ * @example
120
+ * ```ts
121
+ * const host = await jupyter.host();
122
+ * const entries = await host!.notebooks.list({ dir: "journal" });
123
+ * entries.filter((e) => e.type === "notebook").map((e) => e.title ?? e.path);
124
+ * ```
125
+ */
126
+ list(params?: { dir?: string }, options?: HostCallOptions): Promise<NotebookEntry[]>;
127
+ /**
128
+ * A notebook as nbformat JSON (from its open document when someone has
129
+ * it open), and each cell's source hash by cell id.
130
+ *
131
+ * @param params `path`: the notebook, relative to the root.
132
+ * @param options `signal` cancels the call.
133
+ * @returns The notebook and its hashes.
134
+ * @throws {HostError} `denied` when the grant doesn't reach `path`.
135
+ * @example
136
+ * ```ts
137
+ * const host = await jupyter.host();
138
+ * const { notebook } = await host!.notebooks.read({ path: "journal/a.ipynb" });
139
+ * notebook.cells.length;
140
+ * ```
141
+ */
142
+ read(params: { path: string }, options?: HostCallOptions): Promise<{ notebook: any; hashes: { [cellId: string]: string } }>;
143
+ /**
144
+ * Cells that hold every word of `query`, in the notebooks this notebook
145
+ * may read: in sources, and in outputs too with `outputs: true`.
146
+ *
147
+ * @param params `query`, and optionally `dir`, `outputs`, `cell_type`
148
+ * (`code`, `markdown` or `raw`) and `limit` (default 20, at most 200).
149
+ * @param options `signal` cancels the call.
150
+ * @returns The matches, how many there are in all, and whether `limit` cut them.
151
+ * @example
152
+ * ```ts
153
+ * const host = await jupyter.host();
154
+ * const { hits } = await host!.notebooks.search({ query: "weekday drop", dir: "journal" });
155
+ * hits.map((h) => `${h.path}: ${h.excerpt}`);
156
+ * ```
157
+ */
158
+ search(
159
+ params: { query: string; dir?: string; outputs?: boolean; cell_type?: "code" | "markdown" | "raw"; limit?: number },
160
+ options?: HostCallOptions,
161
+ ): Promise<NotebookSearch>;
162
+ /**
163
+ * A notebook's named results. Ganymede 0.8.0 or later; an older server
164
+ * rejects `unavailable`.
165
+ *
166
+ * @param params `path`: the notebook.
167
+ * @param options `signal` cancels the call.
168
+ * @example
169
+ * ```ts
170
+ * const host = await jupyter.host();
171
+ * const results = await host!.notebooks.results({ path: "journal/fremont.ipynb" });
172
+ * ```
173
+ */
174
+ results(params: { path: string }, options?: HostCallOptions): Promise<any>;
175
+ /**
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`.
179
+ *
180
+ * @param params `path`: the notebook; `params`: its parameters.
181
+ * @param options `signal` cancels the run.
182
+ * @example
183
+ * ```ts
184
+ * const host = await jupyter.host();
185
+ * const run = await host!.notebooks.run({ path: "journal/fremont.ipynb", params: { counter: "2nd Ave" } });
186
+ * ```
187
+ */
188
+ run(params: { path: string; params?: HostParams }, options?: HostCallOptions): Promise<any>;
189
+ }
190
+
191
+ /** `host.self`: this notebook's own cells. Ganymede 0.8.0 or later; an older server rejects each with `unavailable`. */
192
+ export interface HostSelf {
193
+ /**
194
+ * Add a cell to this notebook.
195
+ *
196
+ * @param params The cell, as the host API's design note describes.
197
+ * @param options `signal` cancels the call.
198
+ * @example
199
+ * ```ts
200
+ * const host = await jupyter.host();
201
+ * await host!.self.add({ cell_type: "markdown", source: "## Index" });
202
+ * ```
203
+ */
204
+ add(params?: HostParams, options?: HostCallOptions): Promise<any>;
205
+ /**
206
+ * Change a cell's source, given the hash of the source it replaces.
207
+ *
208
+ * @param params The cell id, its new source and the old source's hash.
209
+ * @param options `signal` cancels the call.
210
+ * @example
211
+ * ```ts
212
+ * const host = await jupyter.host();
213
+ * const { notebook, hashes } = await host!.notebooks.read({ path: host!.notebook! });
214
+ * const id = notebook.cells[0].id;
215
+ * await host!.self.edit({ id, source: "# New title", source_hash: hashes[id] });
216
+ * ```
217
+ */
218
+ edit(params?: HostParams, options?: HostCallOptions): Promise<any>;
219
+ /**
220
+ * Delete a cell, given its source's hash.
221
+ *
222
+ * @param params The cell id and its source's hash.
223
+ * @param options `signal` cancels the call.
224
+ * @example
225
+ * ```ts
226
+ * const host = await jupyter.host();
227
+ * const { notebook, hashes } = await host!.notebooks.read({ path: host!.notebook! });
228
+ * const id = notebook.cells.at(-1).id;
229
+ * await host!.self.delete({ id, source_hash: hashes[id] });
230
+ * ```
231
+ */
232
+ delete(params?: HostParams, options?: HostCallOptions): Promise<any>;
233
+ }
234
+
235
+ /** Options for `jupyter.host()`. */
236
+ export interface HostOptions {
237
+ /**
238
+ * How long to wait for a server that hasn't opened the comm, in ms.
239
+ * Default 1000. Once a wait goes unanswered, later calls answer `null`
240
+ * at once.
241
+ */
242
+ timeout?: number;
243
+ }
244
+
245
+ export function createHost(
246
+ comms: CommApi,
247
+ defaultSignal?: () => AbortSignal | undefined,
248
+ ): { host(options?: HostOptions): Promise<Host | null> };
@@ -0,0 +1,215 @@
1
+ // @ts-self-types="./host.d.mts"
2
+ // jupyter.host() (#461): the client for a Ganymede server's host API, which
3
+ // lets a cell read, search and (when granted) run other notebooks. The
4
+ // server opens a comm to target `ganymede.host.v1` when the kernel starts,
5
+ // with its `hello` as the open's data. Calls go out as comm messages
6
+ // `{ id, method, params }`; the server replies `{ id, result }` or
7
+ // `{ id, error: { code, message } }` on a kernel subshell, so a cell that
8
+ // awaits a call gets its reply. The message format and error codes are
9
+ // ganymede-device's, so the two clients behave alike.
10
+ //
11
+ // Design: https://github.com/tlockney/ganymede/blob/main/docs/host-api.md
12
+
13
+ export const HOST_TARGET = "ganymede.host.v1";
14
+
15
+ const CODES = new Set(["unavailable", "denied", "cancelled", "timeout", "failed", "disconnected"]);
16
+
17
+ export class HostError extends Error {
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.name = "HostError";
21
+ this.code = code;
22
+ }
23
+
24
+ /** An abort becomes `cancelled`, a timeout signal's abort `timeout`, anything else `failed`. */
25
+ static from(err) {
26
+ if (err instanceof HostError) return err;
27
+ const message = typeof err?.message === "string" ? err.message : String(err);
28
+ if (err?.name === "AbortError") return new HostError("cancelled", message);
29
+ if (err?.name === "TimeoutError") return new HostError("timeout", message);
30
+ if (typeof err?.code === "string" && CODES.has(err.code)) return new HostError(err.code, message);
31
+ return new HostError("failed", message);
32
+ }
33
+ }
34
+
35
+ /** Why an aborted signal ended a call: `timeout` for a timeout signal, else `cancelled`. */
36
+ function abortError(reason) {
37
+ const message = typeof reason?.message === "string" ? reason.message : String(reason ?? "aborted");
38
+ return new HostError(reason?.name === "TimeoutError" ? "timeout" : "cancelled", message);
39
+ }
40
+
41
+ /** A server's `hello`, or null when `data` isn't one. */
42
+ function helloOf(data) {
43
+ if (data === null || typeof data !== "object" || typeof data.protocol !== "number") return null;
44
+ return {
45
+ protocol: data.protocol,
46
+ server: typeof data.server === "string" ? data.server : "",
47
+ notebook: typeof data.notebook === "string" ? data.notebook : null,
48
+ methods: Array.isArray(data.methods) ? data.methods.filter((m) => typeof m === "string") : [],
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {import("./comm.d.mts").CommApi} comms
54
+ * @param {() => AbortSignal | undefined} [defaultSignal] the signal a call
55
+ * watches when it is given none: jupyter.signal.
56
+ */
57
+ export function createHost(comms, defaultSignal = () => undefined) {
58
+ /** The open host comm: { comm, hello, host, pending }, or null. */
59
+ let link = null;
60
+ /** Whoever waits for a hello: called with the link's Host, or null once there is no link. */
61
+ const waiters = new Set();
62
+ /**
63
+ * A comm this runtime opened went unanswered: no server. Remembered, so
64
+ * each `jupyter.host()` after the first answers null at once, until a
65
+ * server opens the comm itself.
66
+ */
67
+ let noServer = false;
68
+ let nextId = 0;
69
+
70
+ function settle() {
71
+ const host = link?.host ?? null;
72
+ if (host === null && link !== null) return;
73
+ for (const waiter of [...waiters]) waiter(host);
74
+ }
75
+
76
+ /** `ours`: this runtime opened the comm, not the server. */
77
+ function adopt(comm, data, ours = false) {
78
+ // A new comm replaces the old one: the server opens one per kernel start.
79
+ const old = link;
80
+ const l = { comm, host: null, pending: new Map(), ours };
81
+ if (!ours) noServer = false;
82
+ link = l;
83
+ if (old !== null && !old.comm.closed) old.comm.close();
84
+ const hello = helloOf(data);
85
+ if (hello !== null) l.host = makeHost(l, hello);
86
+ comm.onMsg(({ data }) => {
87
+ if (data.hello !== undefined) {
88
+ const hello = helloOf(data.hello);
89
+ if (hello !== null) {
90
+ l.host = makeHost(l, hello);
91
+ if (link === l) settle();
92
+ }
93
+ return;
94
+ }
95
+ const call = l.pending.get(data.id);
96
+ if (call === undefined) return;
97
+ if (data.error !== undefined) {
98
+ const { code, message } = data.error ?? {};
99
+ call.reject(new HostError(CODES.has(code) ? code : "failed", typeof message === "string" ? message : String(code)));
100
+ } else {
101
+ call.resolve(data.result);
102
+ }
103
+ });
104
+ comm.onClose(({ data }) => {
105
+ for (const call of l.pending.values()) {
106
+ call.reject(new HostError("disconnected", typeof data?.error === "string" ? `host comm closed: ${data.error}` : "host comm closed"));
107
+ }
108
+ l.pending.clear();
109
+ if (link === l) {
110
+ link = null;
111
+ // The frontend closed the comm this runtime opened: no server.
112
+ if (l.ours && l.host === null) noServer = true;
113
+ settle();
114
+ }
115
+ });
116
+ if (l.host !== null) settle();
117
+ }
118
+
119
+ function makeHost(l, hello) {
120
+ const methods = Object.freeze([...hello.methods]);
121
+ function call(method, params = {}, { signal = defaultSignal() } = {}) {
122
+ if (signal?.aborted) return Promise.reject(abortError(signal.reason));
123
+ if (l.comm.closed) return Promise.reject(new HostError("disconnected", "host comm closed"));
124
+ const id = nextId++;
125
+ return new Promise((resolve, reject) => {
126
+ const onAbort = () => {
127
+ l.pending.delete(id);
128
+ if (!l.comm.closed) l.comm.send({ id, cancel: true });
129
+ reject(abortError(signal.reason));
130
+ };
131
+ const done = () => signal?.removeEventListener("abort", onAbort);
132
+ const call = {
133
+ resolve: (value) => (l.pending.delete(id), done(), resolve(value)),
134
+ reject: (error) => (l.pending.delete(id), done(), reject(error)),
135
+ };
136
+ l.pending.set(id, call);
137
+ signal?.addEventListener("abort", onAbort, { once: true });
138
+ try {
139
+ l.comm.send({ id, method, params });
140
+ } catch (err) {
141
+ // Params that can't be sent, such as a BigInt: the server never
142
+ // got the call.
143
+ call.reject(new HostError("failed", `cannot send ${method}: ${err instanceof Error ? err.message : err}`));
144
+ }
145
+ });
146
+ }
147
+ const bind = (method) => (params, options) => call(method, params, options);
148
+ return Object.freeze({
149
+ protocol: hello.protocol,
150
+ server: hello.server,
151
+ notebook: hello.notebook,
152
+ methods,
153
+ has: (method) => methods.includes(method),
154
+ call,
155
+ notebooks: Object.freeze({
156
+ list: bind("notebooks.list"),
157
+ read: bind("notebooks.read"),
158
+ search: bind("notebooks.search"),
159
+ results: bind("notebooks.results"),
160
+ run: bind("notebooks.run"),
161
+ }),
162
+ self: Object.freeze({
163
+ add: bind("self.add"),
164
+ edit: bind("self.edit"),
165
+ delete: bind("self.delete"),
166
+ }),
167
+ });
168
+ }
169
+
170
+ comms.registerTarget(HOST_TARGET, (comm, msg) => adopt(comm, msg.data));
171
+
172
+ async function host({ timeout = 1000 } = {}) {
173
+ if (link?.host) return link.host;
174
+ // The wait for a server watches jupyter.signal, so an interrupt ends it.
175
+ const signal = defaultSignal();
176
+ if (signal?.aborted) throw abortError(signal.reason);
177
+ if (noServer) return null;
178
+ // No comm from the server (one whose comm closed, as when a runtime
179
+ // restarts in the same kernel): open it from here. A Ganymede server
180
+ // answers `{ hello }`; anything else closes it or stays silent.
181
+ if (link === null) adopt(comms.open(HOST_TARGET), null, true);
182
+ const mine = link;
183
+ return new Promise((resolve, reject) => {
184
+ let timer;
185
+ const onAbort = () => {
186
+ done();
187
+ reject(abortError(signal.reason));
188
+ };
189
+ const done = () => {
190
+ waiters.delete(waiter);
191
+ clearTimeout(timer);
192
+ signal?.removeEventListener("abort", onAbort);
193
+ };
194
+ const waiter = (host) => {
195
+ done();
196
+ resolve(host);
197
+ };
198
+ waiters.add(waiter);
199
+ signal?.addEventListener("abort", onAbort, { once: true });
200
+ // Each caller waits its own timeout; the last to give up on a comm
201
+ // this runtime opened closes it.
202
+ timer = setTimeout(() => {
203
+ done();
204
+ if (waiters.size === 0 && link === mine && mine.host === null && mine.ours) {
205
+ link = null;
206
+ noServer = true;
207
+ mine.comm.close();
208
+ }
209
+ resolve(null);
210
+ }, timeout);
211
+ });
212
+ }
213
+
214
+ return { host };
215
+ }
@@ -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;