@malloydata/db-duckdb 0.0.406 → 0.0.408

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.
@@ -47,6 +47,7 @@ exports.sqlStringLiteral = sqlStringLiteral;
47
47
  exports.sqlStringListLiteral = sqlStringListLiteral;
48
48
  exports.stringifyDuckDBOption = stringifyDuckDBOption;
49
49
  const path_1 = __importDefault(require("path"));
50
+ const url_1 = require("url");
50
51
  const malloy_1 = require("@malloydata/malloy");
51
52
  const pathSecurity = __importStar(require("./path_security"));
52
53
  class DuckDBConfigValidationError extends Error {
@@ -127,7 +128,7 @@ function normalizeDuckDBConfig(config) {
127
128
  mustExist: securityPolicy === 'sandboxed',
128
129
  });
129
130
  }
130
- const databasePath = canonicalizeDatabasePath(rawDatabasePath);
131
+ const databasePath = canonicalizeDatabasePath(rawDatabasePath, workingDirectory);
131
132
  const isMotherDuck = isMotherDuckPath(databasePath);
132
133
  if (restricted && !isAllowedClosedNetworkDatabasePath(databasePath)) {
133
134
  throw new DuckDBConfigValidationError(`databasePath "${rawDatabasePath}" is not allowed when securityPolicy is "${securityPolicy}"`);
@@ -342,15 +343,46 @@ function deriveRestrictedSecretDirectory({ requiresSecretNeutralization, tempDir
342
343
  }
343
344
  throw new DuckDBConfigValidationError('restricted DuckDB policies require workingDirectory or tempDirectory so Malloy can isolate persistent DuckDB secrets');
344
345
  }
345
- function canonicalizeDatabasePath(databasePath) {
346
+ // A relative `databasePath` resolves against `workingDirectory` (which itself
347
+ // defaults to the project root via `{config: 'rootDirectory'}`), keeping a
348
+ // config file portable — `databasePath: "analytics.duckdb"` names the database
349
+ // alongside the project regardless of where the host process is launched.
350
+ // `:memory:` and remote schemes are taken as-is; absolute paths ignore the
351
+ // base; with no `workingDirectory` set, a relative path falls back to the cwd.
352
+ function canonicalizeDatabasePath(databasePath, workingDirectory) {
346
353
  if (databasePath === ':memory:' || isLikelyRemoteDatabasePath(databasePath)) {
347
354
  return databasePath;
348
355
  }
349
- return canonicalizeConfigPath(databasePath, 'databasePath');
356
+ return canonicalizeConfigPath(databasePath, 'databasePath', {
357
+ baseDirectory: workingDirectory,
358
+ });
359
+ }
360
+ // A config path can arrive as a `file://` URL rather than a plain path —
361
+ // notably `workingDirectory`, which defaults to `config.rootDirectory` (the
362
+ // config stack carries it as a URL string). Left as a URL, `path.resolve`
363
+ // treats `file:` as a relative segment and joins it to the process cwd,
364
+ // silently breaking relative `read_parquet`/glob resolution. Decode file URLs
365
+ // to a real path so every downstream consumer (FILE_SEARCH_PATH,
366
+ // allowed_directories, …) sees one. Plain paths and non-file schemes pass
367
+ // through untouched; a malformed file URL throws and surfaces as the caller's
368
+ // field-specific validation error.
369
+ function maybeFileURLToPath(input) {
370
+ // Cheap guard so plain paths (the common case) skip URL parsing entirely.
371
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(input)) {
372
+ return input;
373
+ }
374
+ let url;
375
+ try {
376
+ url = new URL(input);
377
+ }
378
+ catch {
379
+ return input; // not a URL — treat as a plain path
380
+ }
381
+ return url.protocol === 'file:' ? (0, url_1.fileURLToPath)(url) : input;
350
382
  }
351
383
  function canonicalizeConfigPath(input, fieldName, options = {}) {
352
384
  try {
353
- return pathSecurity.canonicalizePath(input, options);
385
+ return pathSecurity.canonicalizePath(maybeFileURLToPath(input), options);
354
386
  }
355
387
  catch (error) {
356
388
  throw new DuckDBConfigValidationError(`${fieldName} is invalid: ${errorMessage(error)}`);
@@ -214,6 +214,22 @@ function unwrapTable(table) {
214
214
  return table.toArray().map(row => unwrapRow(row, table.schema));
215
215
  }
216
216
  const isNode = () => { var _a; return typeof process !== 'undefined' && typeof ((_a = process.versions) === null || _a === void 0 ? void 0 : _a.node) === 'string'; };
217
+ // `workingDirectory` defaults to `config.rootDirectory`, which the config
218
+ // stack carries as a URL string. DuckDB-Wasm's virtual filesystem wants a plain
219
+ // POSIX path; a `file://` URL left intact becomes a bogus FILE_SEARCH_PATH and
220
+ // breaks relative reads. Decode file URLs to their path. Browser-safe — uses
221
+ // the WHATWG `URL` (the native connection's Node `fileURLToPath` is unavailable
222
+ // here). Plain paths and non-file schemes pass through untouched.
223
+ function fileURLToVirtualPath(input) {
224
+ if (!/^file:\/\//i.test(input)) {
225
+ return input;
226
+ }
227
+ const decoded = decodeURIComponent(new URL(input).pathname);
228
+ // Drop a trailing separator but keep the filesystem root '/'.
229
+ return decoded.length > 1 && decoded.endsWith('/')
230
+ ? decoded.slice(0, -1)
231
+ : decoded;
232
+ }
217
233
  class DuckDBWASMConnection extends duckdb_common_1.DuckDBCommon {
218
234
  constructor(arg, arg2, workingDirectory, queryOptions) {
219
235
  var _a, _b;
@@ -233,7 +249,7 @@ class DuckDBWASMConnection extends duckdb_common_1.DuckDBCommon {
233
249
  this.databasePath = arg2;
234
250
  }
235
251
  if (typeof workingDirectory === 'string') {
236
- this.workingDirectory = workingDirectory;
252
+ this.workingDirectory = fileURLToVirtualPath(workingDirectory);
237
253
  }
238
254
  if (queryOptions) {
239
255
  this.queryOptions = queryOptions;
@@ -248,7 +264,7 @@ class DuckDBWASMConnection extends duckdb_common_1.DuckDBCommon {
248
264
  this.databasePath = arg.databasePath;
249
265
  }
250
266
  if (typeof arg.workingDirectory === 'string') {
251
- this.workingDirectory = arg.workingDirectory;
267
+ this.workingDirectory = fileURLToVirtualPath(arg.workingDirectory);
252
268
  }
253
269
  if (typeof arg.motherDuckToken === 'string') {
254
270
  this.motherDuckToken = arg.motherDuckToken;
@@ -1,7 +1,8 @@
1
1
  export interface CanonicalPathOptions {
2
2
  mustExist?: boolean;
3
+ baseDirectory?: string;
3
4
  }
4
5
  export declare function isPosixHost(): boolean;
5
- export declare function canonicalizePath(input: string, { mustExist }?: CanonicalPathOptions): string;
6
+ export declare function canonicalizePath(input: string, { mustExist, baseDirectory }?: CanonicalPathOptions): string;
6
7
  export declare function canonicalizePathList(paths: string[]): string[];
7
8
  export declare function isContainedPath(parent: string, child: string): boolean;
@@ -16,11 +16,13 @@ const path_1 = __importDefault(require("path"));
16
16
  function isPosixHost() {
17
17
  return path_1.default.sep === '/';
18
18
  }
19
- function canonicalizePath(input, { mustExist = false } = {}) {
19
+ function canonicalizePath(input, { mustExist = false, baseDirectory } = {}) {
20
20
  if (input.trim() === '') {
21
21
  throw new Error('path must not be empty');
22
22
  }
23
- const resolved = path_1.default.resolve(input);
23
+ const resolved = baseDirectory !== undefined
24
+ ? path_1.default.resolve(baseDirectory, input)
25
+ : path_1.default.resolve(input);
24
26
  const canonical = (() => {
25
27
  try {
26
28
  return fs_1.default.realpathSync.native(resolved);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/db-duckdb",
3
- "version": "0.0.406",
3
+ "version": "0.0.408",
4
4
  "license": "MIT",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -61,7 +61,7 @@
61
61
  "dependencies": {
62
62
  "@duckdb/duckdb-wasm": "1.33.1-dev45.0",
63
63
  "@duckdb/node-api": "1.5.3-r.2",
64
- "@malloydata/malloy": "0.0.406",
64
+ "@malloydata/malloy": "0.0.408",
65
65
  "@motherduck/wasm-client": "^0.6.6",
66
66
  "apache-arrow": "^17.0.0",
67
67
  "web-worker": "^1.5.0"