@malloy-publisher/server 0.0.203 → 0.0.205

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.
Files changed (84) hide show
  1. package/build.ts +10 -1
  2. package/dist/app/api-doc.yaml +146 -0
  3. package/dist/app/assets/{EnvironmentPage-BVQ7glKP.js → EnvironmentPage-CAge6UHD.js} +1 -1
  4. package/dist/app/assets/HomePage-DhTe8qpa.js +1 -0
  5. package/dist/app/assets/{MainPage-bYOWcgDP.js → MainPage-CeTxxGex.js} +2 -2
  6. package/dist/app/assets/MaterializationsPage-CpDHB70t.js +1 -0
  7. package/dist/app/assets/ModelPage-D9sSMb75.js +1 -0
  8. package/dist/app/assets/PackagePage-LRqQWrFY.js +1 -0
  9. package/dist/app/assets/{RouteError-_J-EBz7W.js → RouteError-xT6kuCNw.js} +1 -1
  10. package/dist/app/assets/{WorkbookPage-Bjs9Nm-_.js → WorkbookPage-DsIh9svZ.js} +1 -1
  11. package/dist/app/assets/{core-BPLlx5VM.es-C2ARtwWI.js → core-C2sQrwVu.es-Bjem0hym.js} +1 -1
  12. package/dist/app/assets/{index-CqUWJELr.js → index-BdOZDcce.js} +2 -2
  13. package/dist/app/assets/index-DHHAcY5o.js +1812 -0
  14. package/dist/app/assets/index-RX3QOTde.js +455 -0
  15. package/dist/app/assets/index.umd-D2WH3D-f.js +2469 -0
  16. package/dist/app/index.html +1 -1
  17. package/dist/package_load_worker.mjs +392 -67
  18. package/dist/runtime/publisher.js +318 -0
  19. package/dist/server.mjs +982 -346
  20. package/package.json +15 -14
  21. package/scripts/bake-duckdb-extensions.js +104 -0
  22. package/src/controller/watch-mode.controller.ts +176 -46
  23. package/src/ducklake_version.spec.ts +43 -0
  24. package/src/ducklake_version.ts +26 -0
  25. package/src/errors.spec.ts +21 -0
  26. package/src/errors.ts +18 -1
  27. package/src/mcp/error_messages.spec.ts +35 -0
  28. package/src/mcp/error_messages.ts +14 -1
  29. package/src/mcp/handler_utils.ts +12 -0
  30. package/src/package_load/package_load_pool.ts +0 -5
  31. package/src/package_load/package_load_worker.ts +41 -99
  32. package/src/package_load/protocol.ts +1 -7
  33. package/src/runtime/publisher.js +318 -0
  34. package/src/server.ts +479 -2
  35. package/src/service/annotations.spec.ts +118 -0
  36. package/src/service/annotations.ts +91 -0
  37. package/src/service/authorize.spec.ts +132 -0
  38. package/src/service/authorize.ts +241 -0
  39. package/src/service/authorize_integration.spec.ts +932 -0
  40. package/src/service/compile_authorize.spec.ts +85 -0
  41. package/src/service/connection.ts +1 -1
  42. package/src/service/environment.ts +67 -9
  43. package/src/service/environment_store.ts +142 -11
  44. package/src/service/filter.spec.ts +14 -3
  45. package/src/service/filter.ts +5 -1
  46. package/src/service/filter_bypass.spec.ts +418 -0
  47. package/src/service/given.ts +37 -12
  48. package/src/service/givens_integration.spec.ts +34 -7
  49. package/src/service/materialization_service.ts +25 -20
  50. package/src/service/materialized_table_gc.spec.ts +6 -5
  51. package/src/service/materialized_table_gc.ts +2 -50
  52. package/src/service/model.spec.ts +203 -8
  53. package/src/service/model.ts +349 -155
  54. package/src/service/package.ts +17 -6
  55. package/src/service/package_worker_path.spec.ts +113 -0
  56. package/src/service/quoting.ts +0 -20
  57. package/src/service/restricted_mode.spec.ts +299 -0
  58. package/src/service/source_extraction.ts +226 -0
  59. package/src/storage/StorageManager.ts +73 -0
  60. package/src/storage/duckdb/DuckDBConnection.ts +70 -124
  61. package/tests/fixtures/authorize-compile/model.malloy +9 -0
  62. package/tests/fixtures/authorize-compile/publisher.json +4 -0
  63. package/tests/fixtures/html-pages-nopublic/model.malloy +1 -0
  64. package/tests/fixtures/html-pages-nopublic/publisher.json +5 -0
  65. package/tests/fixtures/html-pages-test/data.csv +3 -0
  66. package/tests/fixtures/html-pages-test/public/assets/app.css +3 -0
  67. package/tests/fixtures/html-pages-test/public/data.json +1 -0
  68. package/tests/fixtures/html-pages-test/public/index.html +9 -0
  69. package/tests/fixtures/html-pages-test/public/sub/page2.html +9 -0
  70. package/tests/fixtures/html-pages-test/publisher.json +5 -0
  71. package/tests/fixtures/html-pages-test/report.malloy +1 -0
  72. package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +92 -0
  73. package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +138 -0
  74. package/tests/integration/html_pages/html_pages.integration.spec.ts +378 -0
  75. package/tests/integration/watch-mode/watch_mode.integration.spec.ts +421 -0
  76. package/tests/unit/duckdb/attached_databases.test.ts +111 -0
  77. package/tests/unit/duckdb/duckdb_connection.test.ts +181 -0
  78. package/tests/unit/duckdb/repositories.test.ts +208 -0
  79. package/dist/app/assets/HomePage-D9drXoZX.js +0 -1
  80. package/dist/app/assets/ModelPage-DT0gjNy1.js +0 -1
  81. package/dist/app/assets/PackagePage-N1ZBNJul.js +0 -1
  82. package/dist/app/assets/index-BeNwIeYQ.js +0 -454
  83. package/dist/app/assets/index-Dx7qi2LO.js +0 -1803
  84. package/dist/app/assets/index.umd-BXm2lnUO.js +0 -1145
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Shared source / query introspection extracted from a compiled `ModelDef`.
3
+ *
4
+ * Both the in-process `Model.create` path (`service/model.ts`) and the
5
+ * package-load worker (`package_load/package_load_worker.ts`, which runs in a
6
+ * separate bundle and serializes the result over the worker protocol) need to
7
+ * walk a `ModelDef` and produce the same `sources` / `queries` shapes plus the
8
+ * `#(filter)` `filterMap`. These two call sites used to carry byte-for-byte
9
+ * copies of this logic; keeping them in lockstep by hand was a standing hazard
10
+ * (a change to one silently diverged from the other). This module is the single
11
+ * source of truth — the two callers differ only in how they type the result
12
+ * (generated API types vs. worker wire types — structurally identical, so each
13
+ * casts at its boundary) and in how they report a filter parse failure (the
14
+ * service logs a warning; the worker has no logger and stays silent), which is
15
+ * threaded through the optional `onParseError` callback.
16
+ */
17
+
18
+ import {
19
+ isSourceDef,
20
+ ModelDef,
21
+ NamedModelObject,
22
+ NamedQueryDef,
23
+ StructDef,
24
+ TurtleDef,
25
+ } from "@malloydata/malloy";
26
+ import { annotationTexts, modelAnnotations } from "./annotations";
27
+ import { collectAuthorizeExprs, type AuthorizeMap } from "./authorize";
28
+ import { parseFilters, type FilterDefinition } from "./filter";
29
+
30
+ /** A `#(filter)` definition enriched with the dimension's Malloy type. */
31
+ export interface ExtractedFilter {
32
+ name: string;
33
+ dimension: string;
34
+ type: string;
35
+ implicit: boolean;
36
+ required: boolean;
37
+ dimensionType: string | undefined;
38
+ }
39
+
40
+ export interface ExtractedView {
41
+ name: string;
42
+ annotations: string[] | undefined;
43
+ }
44
+
45
+ /**
46
+ * Structural source shape both callers cast to their own typed view
47
+ * (`ApiSource` in the service, `ApiSourceWire` in the worker). `givens` is
48
+ * attached verbatim from the caller-supplied list, so it stays `unknown` here.
49
+ */
50
+ export interface ExtractedSource {
51
+ name: string;
52
+ annotations: string[] | undefined;
53
+ views: ExtractedView[];
54
+ filters: ExtractedFilter[] | undefined;
55
+ givens: unknown;
56
+ /**
57
+ * Effective `#(authorize)` / `##(authorize)` expressions gating this source:
58
+ * file-level expressions first, then the source's own. Undefined when the
59
+ * source carries no authorize annotations. Surfaced for introspection;
60
+ * enforcement happens server-side.
61
+ */
62
+ authorize: string[] | undefined;
63
+ }
64
+
65
+ export interface ExtractedQuery {
66
+ name: string;
67
+ sourceName: string | undefined;
68
+ annotations: string[] | undefined;
69
+ }
70
+
71
+ /**
72
+ * Extract every source from a compiled model, parsing `#(filter)` annotations
73
+ * along the way.
74
+ *
75
+ * Filters are collected by walking the `annotations.inherits` chain so that
76
+ * filters declared on a base source flow to an extending source. The chain runs
77
+ * child → parent, so we collect child-first then reverse — `parseFilters` uses
78
+ * "last wins" dedup, which lets a child's `#(filter)` override the base's.
79
+ *
80
+ * `givens` is attached unchanged to every source (Malloy exposes givens at the
81
+ * model level, not per-source). `onParseError`, when supplied, is invoked with
82
+ * the source name and error if a source's `#(filter)` annotations fail to parse;
83
+ * filter extraction then continues. Authorize parse errors are NOT routed here —
84
+ * they propagate (a malformed gate fails model load) so a security gate is never
85
+ * silently dropped.
86
+ *
87
+ * Authorize (`#(authorize)` / `##(authorize)`) is collected from the source's
88
+ * own `blockNotes` only — we do NOT walk the `inherits` chain. Note Malloy's
89
+ * behavior for `X is Y extend {...}`: if X declares its own `#(authorize)`,
90
+ * X.blockNotes holds only X's gates (Y's are dropped — the intended "curated
91
+ * re-exposure"); if X declares none, Malloy surfaces Y's blockNotes on X, so
92
+ * the base gate carries to the un-annotated extension (a safe default — a
93
+ * locked base stays locked unless an extension explicitly re-exposes itself).
94
+ * This carry happens through `blockNotes`, not the `inherits` chain, so reading
95
+ * own-blockNotes is sufficient. Joins are a separate concern and are not gated.
96
+ * The effective list per source is the file-level `##(authorize)` expressions
97
+ * (from `modelDef.annotations.notes`) followed by the source's own
98
+ * `#(authorize)` expressions, evaluated as one OR disjunction at request time.
99
+ */
100
+ export function extractSourcesFromModelDef(
101
+ modelDef: ModelDef,
102
+ givens: unknown,
103
+ onParseError?: (sourceName: string, err: unknown) => void,
104
+ ): {
105
+ sources: ExtractedSource[];
106
+ filterMap: Map<string, FilterDefinition[]>;
107
+ authorizeMap: AuthorizeMap;
108
+ } {
109
+ const filterMap = new Map<string, FilterDefinition[]>();
110
+ const authorizeMap: AuthorizeMap = new Map();
111
+
112
+ // File-level ##(authorize) is collected once and prepended to every source.
113
+ // Unlike filters, a malformed authorize annotation is NOT swallowed: the
114
+ // parse error propagates so the model fails to load loudly (caught per-model
115
+ // upstream and turned into a compilationError). Silently dropping a gate —
116
+ // and in the worker path there is no onParseError callback, so it would be
117
+ // truly silent — could leave a source that the author meant to lock looking
118
+ // unrestricted.
119
+ const fileLevelAuthorize = collectAuthorizeExprs(
120
+ (modelAnnotations(modelDef).notes ?? []).map((note) => note.text),
121
+ );
122
+
123
+ const sources: ExtractedSource[] = Object.values(modelDef.contents)
124
+ .filter((obj) => isSourceDef(obj))
125
+ .map((sourceObj) => {
126
+ const struct = sourceObj as StructDef;
127
+ const sourceName = struct.as || struct.name;
128
+ const annotations = annotationTexts(struct.annotations);
129
+
130
+ const collected: string[][] = [];
131
+ let cur = struct.annotations;
132
+ while (cur) {
133
+ if (cur.blockNotes) {
134
+ collected.push(cur.blockNotes.map((note) => note.text));
135
+ }
136
+ cur = cur.inherits;
137
+ }
138
+ const allAnnotations = collected.reverse().flat();
139
+
140
+ let filters: ExtractedFilter[] | undefined;
141
+ if (allAnnotations.length > 0) {
142
+ try {
143
+ const parsed = parseFilters(allAnnotations);
144
+ if (parsed.length > 0) {
145
+ filterMap.set(sourceName, parsed);
146
+ const fields = struct.fields;
147
+ filters = parsed.map((f) => {
148
+ const field = fields.find(
149
+ (fd) => (fd.as || fd.name) === f.dimension,
150
+ );
151
+ return {
152
+ name: f.name,
153
+ dimension: f.dimension,
154
+ type: f.type,
155
+ implicit: f.implicit,
156
+ required: f.required,
157
+ dimensionType: field?.type as string | undefined,
158
+ };
159
+ });
160
+ }
161
+ } catch (err) {
162
+ onParseError?.(sourceName, err);
163
+ }
164
+ }
165
+
166
+ // Authorize: the source's OWN #(authorize) annotations only — no
167
+ // inherits walk. File-level ##(authorize) is prepended so file gates
168
+ // and source gates form one OR disjunction. A malformed annotation
169
+ // propagates (model fails to load) rather than silently dropping the
170
+ // gate — see the file-level note above.
171
+ const ownNotes = (struct.annotations?.blockNotes ?? []).map(
172
+ (note) => note.text,
173
+ );
174
+ const effective = [
175
+ ...fileLevelAuthorize,
176
+ ...collectAuthorizeExprs(ownNotes),
177
+ ];
178
+ let authorize: string[] | undefined;
179
+ if (effective.length > 0) {
180
+ authorizeMap.set(sourceName, effective);
181
+ authorize = effective;
182
+ }
183
+
184
+ const views: ExtractedView[] = struct.fields
185
+ .filter((field) => field.type === "turtle")
186
+ .filter((turtle) =>
187
+ // Filter out non-reduce views (e.g. indexes).
188
+ (turtle as TurtleDef).pipeline
189
+ .map((stage) => stage.type)
190
+ .every((type) => type === "reduce"),
191
+ )
192
+ .map((turtle) => ({
193
+ name: turtle.as || turtle.name,
194
+ annotations: annotationTexts(turtle.annotations),
195
+ }));
196
+
197
+ return {
198
+ name: sourceName,
199
+ annotations,
200
+ views,
201
+ filters,
202
+ givens,
203
+ authorize,
204
+ };
205
+ });
206
+
207
+ return { sources, filterMap, authorizeMap };
208
+ }
209
+
210
+ /** Extract every named query from a compiled model. */
211
+ export function extractQueriesFromModelDef(
212
+ modelDef: ModelDef,
213
+ ): ExtractedQuery[] {
214
+ const isNamedQuery = (obj: NamedModelObject): obj is NamedQueryDef =>
215
+ obj.type === "query";
216
+ return Object.values(modelDef.contents)
217
+ .filter(isNamedQuery)
218
+ .map((queryObj) => ({
219
+ name: queryObj.as || queryObj.name,
220
+ sourceName:
221
+ typeof queryObj.structRef === "string"
222
+ ? queryObj.structRef
223
+ : undefined,
224
+ annotations: annotationTexts(queryObj.annotations),
225
+ }));
226
+ }
@@ -1,5 +1,9 @@
1
1
  import { Mutex } from "async-mutex";
2
2
  import * as crypto from "crypto";
3
+ import {
4
+ isCatalogVersionSupported,
5
+ SUPPORTED_CATALOG_VERSIONS,
6
+ } from "../ducklake_version";
3
7
  import { ConnectionAuthError } from "../errors";
4
8
  import { logger } from "../logger";
5
9
  import {
@@ -64,6 +68,53 @@ function catalogNameForConfig(c: DuckLakeManifestConfig): string {
64
68
  return `manifest_lake_${hash}`;
65
69
  }
66
70
 
71
+ // Read the catalog's recorded DuckLake format version from its
72
+ // `ducklake_metadata` table via a plain postgres ATTACH (does NOT invoke
73
+ // the DuckLake extension on the catalog). Returns the version string on
74
+ // success, or `undefined` on any failure (missing table, query timeout,
75
+ // connect failure) so the main ATTACH path stays the source of truth for
76
+ // unrelated errors. Only meaningful for postgres-backed catalogs; the
77
+ // caller must guard with `isPostgres`.
78
+ async function readDuckLakeCatalogVersion(
79
+ connection: DuckDBConnection,
80
+ catalogUrl: string,
81
+ catalogName: string,
82
+ ): Promise<string | undefined> {
83
+ if (!catalogUrl.startsWith("postgres:")) {
84
+ return undefined;
85
+ }
86
+ const pgConnString = catalogUrl.slice("postgres:".length);
87
+ const tempDb = `${catalogName}_preflight`;
88
+ const escaped = escapeSQL(pgConnString);
89
+ try {
90
+ await connection.run(
91
+ `ATTACH '${escaped}' AS ${tempDb} (TYPE postgres, READ_ONLY);`,
92
+ );
93
+ const rows = await connection.all<{ value: string }>(
94
+ `SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`,
95
+ );
96
+ const value = rows[0]?.value;
97
+ return typeof value === "string" ? value : undefined;
98
+ } catch (error) {
99
+ logger.warn(
100
+ "DuckLake catalog version preflight failed; falling back to ATTACH",
101
+ {
102
+ catalogName,
103
+ error: redactPgSecrets(
104
+ error instanceof Error ? error.message : String(error),
105
+ ),
106
+ },
107
+ );
108
+ return undefined;
109
+ } finally {
110
+ try {
111
+ await connection.run(`DETACH ${tempDb};`);
112
+ } catch {
113
+ // ATTACH may have failed, so DETACH may have nothing to do.
114
+ }
115
+ }
116
+ }
117
+
67
118
  /**
68
119
  * Manages the storage backend (DuckDB, Postgres, etc.) and per-environment
69
120
  * manifest stores. Environments without `materializationStorage` config use
@@ -222,6 +273,28 @@ export class StorageManager {
222
273
  await connection.run("INSTALL httpfs; LOAD httpfs;");
223
274
  }
224
275
 
276
+ // Preflight: read the catalog's recorded format version via the
277
+ // postgres extension (not DuckLake) and fail fast with a non-retryable
278
+ // 422 if the baked DuckLake extension can't read it. Without this,
279
+ // an unsupported catalog would surface as a generic DuckDB error
280
+ // from the ATTACH below, which retry loops misclassify as transient.
281
+ if (isPostgres) {
282
+ const catalogVersion = await readDuckLakeCatalogVersion(
283
+ connection,
284
+ catalogUrl,
285
+ catalogName,
286
+ );
287
+ if (catalogVersion && !isCatalogVersionSupported(catalogVersion)) {
288
+ const supportedMax =
289
+ SUPPORTED_CATALOG_VERSIONS[
290
+ SUPPORTED_CATALOG_VERSIONS.length - 1
291
+ ];
292
+ throw new ConnectionAuthError(
293
+ `DuckLake catalog version ${catalogVersion} is newer than this Publisher's extension supports (max ${supportedMax}). Upgrade the Publisher image or downgrade the catalog.`,
294
+ );
295
+ }
296
+ }
297
+
225
298
  let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
226
299
  const attachOpts: string[] = [
227
300
  `DATA_PATH '${escapedDataPath}'`,
@@ -1,11 +1,30 @@
1
1
  import { Mutex } from "async-mutex";
2
- import duckdb from "duckdb";
2
+ import {
3
+ DuckDBConnection as NeoConnection,
4
+ DuckDBInstance,
5
+ type DuckDBValue,
6
+ } from "@duckdb/node-api";
3
7
  import * as path from "path";
4
8
  import { DatabaseConnection } from "../DatabaseInterface";
5
9
 
10
+ /**
11
+ * Embedded persistence layer for the publisher's own metadata (environments,
12
+ * packages, connections, materializations, build manifests) in `publisher.db`.
13
+ *
14
+ * This is a plain DAO over a durable, exclusively-owned DuckDB handle -- it is
15
+ * deliberately NOT Malloy's `@malloydata/db-duckdb` connection, which is an
16
+ * analytical query connection (no prepared-statement parameter binding, a
17
+ * `:memory:` primary with ATTACH/DETACH/idle lifecycle, pooled/shared
18
+ * instances, and a poison-pill close). Those semantics are wrong for a
19
+ * source-of-truth store that must hold one handle open for the server's
20
+ * lifetime and run parameterized CRUD.
21
+ *
22
+ * It wraps `@duckdb/node-api` (the same DuckDB engine Malloy pulls in), so the
23
+ * repo carries a single DuckDB engine rather than a second, redundant driver.
24
+ */
6
25
  export class DuckDBConnection implements DatabaseConnection {
7
- private db: duckdb.Database | null = null;
8
- private connection: duckdb.Connection | null = null;
26
+ private instance: DuckDBInstance | null = null;
27
+ private connection: NeoConnection | null = null;
9
28
  private dbPath: string;
10
29
  private mutex: Mutex = new Mutex();
11
30
 
@@ -15,94 +34,47 @@ export class DuckDBConnection implements DatabaseConnection {
15
34
  }
16
35
 
17
36
  async initialize(): Promise<void> {
18
- return new Promise((resolve, reject) => {
19
- this.db = new duckdb.Database(this.dbPath, {}, (err) => {
20
- if (err) {
21
- console.error("Failed to create DuckDB database:", err);
22
- reject(new Error(`Failed to initialize DuckDB: ${err.message}`));
23
- return;
24
- }
25
-
26
- // Connect synchronously
27
- this.connection = (
28
- this.db as duckdb.Database & { connect(): duckdb.Connection }
29
- ).connect();
30
-
31
- if (!this.connection) {
32
- reject(new Error("Failed to create connection object"));
33
- return;
34
- }
35
-
36
- // Verify connection works
37
- this.connection.all("SELECT 42 as answer", (testErr, _rows) => {
38
- if (testErr) {
39
- console.error("Connection test failed:", testErr);
40
- reject(
41
- new Error(
42
- `Failed to verify DuckDB connection: ${testErr.message}`,
43
- ),
44
- );
45
- return;
46
- }
47
-
48
- resolve();
49
- });
50
- });
51
- });
37
+ try {
38
+ this.instance = await DuckDBInstance.create(this.dbPath);
39
+ this.connection = await this.instance.connect();
40
+ // Verify the connection works
41
+ await this.connection.run("SELECT 42 as answer");
42
+ } catch (err) {
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ console.error("Failed to create DuckDB database:", err);
45
+ throw new Error(`Failed to initialize DuckDB: ${message}`);
46
+ }
52
47
  }
53
48
 
54
49
  async close(): Promise<void> {
55
- return new Promise((resolve, reject) => {
50
+ try {
56
51
  if (this.connection) {
57
- this.connection.close((err) => {
58
- if (err) {
59
- reject(
60
- new Error(
61
- `Failed to close DuckDB connection: ${err.message}`,
62
- ),
63
- );
64
- return;
65
- }
66
-
67
- if (this.db) {
68
- this.db.close((dbErr) => {
69
- if (dbErr) {
70
- reject(
71
- new Error(
72
- `Failed to close DuckDB: ${dbErr.message}`,
73
- ),
74
- );
75
- return;
76
- }
77
- console.log("DuckDB connection closed");
78
- resolve();
79
- });
80
- } else {
81
- resolve();
82
- }
83
- });
84
- } else {
85
- resolve();
52
+ this.connection.closeSync();
53
+ this.connection = null;
86
54
  }
87
- });
55
+ if (this.instance) {
56
+ this.instance.closeSync();
57
+ this.instance = null;
58
+ }
59
+ console.log("DuckDB connection closed");
60
+ } catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ throw new Error(`Failed to close DuckDB connection: ${message}`);
63
+ }
88
64
  }
89
65
 
90
66
  async isInitialized(): Promise<boolean> {
91
67
  if (!this.connection) return false;
92
68
 
93
69
  return this.mutex.runExclusive(async () => {
94
- return new Promise<boolean>((resolve) => {
95
- this.connection!.all(
70
+ try {
71
+ const reader = await this.connection!.runAndReadAll(
96
72
  "SELECT name FROM sqlite_master WHERE type='table' AND name='environments'",
97
- (err, rows) => {
98
- if (err) {
99
- resolve(false);
100
- return;
101
- }
102
- resolve(rows && rows.length > 0);
103
- },
104
73
  );
105
- });
74
+ return reader.getRowObjectsJS().length > 0;
75
+ } catch {
76
+ return false;
77
+ }
106
78
  });
107
79
  }
108
80
 
@@ -112,26 +84,14 @@ export class DuckDBConnection implements DatabaseConnection {
112
84
  }
113
85
 
114
86
  return this.mutex.runExclusive(async () => {
115
- return new Promise<void>((resolve, reject) => {
116
- const callback = (err: Error | null) => {
117
- if (err) {
118
- reject(
119
- new Error(
120
- `Query execution failed: ${err.message}\nQuery: ${query}`,
121
- ),
122
- );
123
- return;
124
- }
125
- resolve();
126
- };
127
-
128
- // Pass params directly without the params argument if empty
129
- if (params && params.length > 0) {
130
- this.connection!.run(query, ...params, callback);
131
- } else {
132
- this.connection!.run(query, callback);
133
- }
134
- });
87
+ try {
88
+ await this.connection!.run(query, params as DuckDBValue[]);
89
+ } catch (err) {
90
+ const message = err instanceof Error ? err.message : String(err);
91
+ throw new Error(
92
+ `Query execution failed: ${message}\nQuery: ${query}`,
93
+ );
94
+ }
135
95
  });
136
96
  }
137
97
 
@@ -141,25 +101,18 @@ export class DuckDBConnection implements DatabaseConnection {
141
101
  }
142
102
 
143
103
  return this.mutex.runExclusive(async () => {
144
- return new Promise<T[]>((resolve, reject) => {
145
- const callback = (err: Error | null, rows: unknown[]) => {
146
- if (err) {
147
- reject(
148
- new Error(
149
- `Query execution failed: ${err.message}\nQuery: ${query}`,
150
- ),
151
- );
152
- return;
153
- }
154
- resolve((rows || []) as T[]);
155
- };
156
-
157
- if (params && params.length > 0) {
158
- this.connection!.all(query, ...params, callback);
159
- } else {
160
- this.connection!.all(query, callback);
161
- }
162
- });
104
+ try {
105
+ const reader = await this.connection!.runAndReadAll(
106
+ query,
107
+ params as DuckDBValue[],
108
+ );
109
+ return reader.getRowObjectsJS() as T[];
110
+ } catch (err) {
111
+ const message = err instanceof Error ? err.message : String(err);
112
+ throw new Error(
113
+ `Query execution failed: ${message}\nQuery: ${query}`,
114
+ );
115
+ }
163
116
  });
164
117
  }
165
118
 
@@ -167,11 +120,4 @@ export class DuckDBConnection implements DatabaseConnection {
167
120
  const rows = await this.all<T>(query, params);
168
121
  return rows.length > 0 ? rows[0] : null;
169
122
  }
170
-
171
- getConnection(): duckdb.Connection {
172
- if (!this.connection) {
173
- throw new Error("Database not initialized");
174
- }
175
- return this.connection;
176
- }
177
123
  }
@@ -0,0 +1,9 @@
1
+ ##! experimental.givens
2
+
3
+ given:
4
+ ROLE :: string
5
+
6
+ #(authorize) "$ROLE = 'analyst'"
7
+ source: gated is duckdb.sql("SELECT 1 as x") extend { measure: c is count() }
8
+
9
+ source: open_src is duckdb.sql("SELECT 1 as x") extend { measure: c is count() }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "authorize-compile",
3
+ "description": "Fixture: a gated source for the /compile authorize HTTP integration test."
4
+ }
@@ -0,0 +1 @@
1
+ source: nums is duckdb.sql("SELECT 1 as n")
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "html-pages-nopublic",
3
+ "version": "0.0.1",
4
+ "description": "Package with no public/ directory; static file requests return 404."
5
+ }
@@ -0,0 +1,3 @@
1
+ id,name
2
+ 1,alpha
3
+ 2,beta
@@ -0,0 +1,3 @@
1
+ body {
2
+ font-family: system-ui, sans-serif;
3
+ }
@@ -0,0 +1 @@
1
+ { "ok": true }
@@ -0,0 +1,9 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Carrier Dashboard</title>
5
+ </head>
6
+ <body>
7
+ <h1>Hello from the in-package data app</h1>
8
+ </body>
9
+ </html>
@@ -0,0 +1,9 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <title>Second Page</title>
5
+ </head>
6
+ <body>
7
+ <p>A page in a subdirectory.</p>
8
+ </body>
9
+ </html>
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "html-pages-test",
3
+ "version": "1.0.0",
4
+ "description": "Fixture for in-package HTML static-file serving + /pages + /events"
5
+ }
@@ -0,0 +1 @@
1
+ source: report is duckdb.sql("SELECT 1 as n")