@sixb/ducklake 0.2.1 → 0.2.2

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,15 @@
1
+ import type { DuckDbQueryRuntime } from "./duckdb-runtime";
2
+ /**
3
+ * Install missing core extensions from `https://extensions.duckdb.org`.
4
+ *
5
+ * DuckDB's `INSTALL` downloads from `http://extensions.duckdb.org`, and fetching over HTTPS needs
6
+ * `httpfs`, which DuckDB itself fetches over HTTP. A host that only allows HTTPS by name, such as
7
+ * a Vercel sandbox whose firewall filters on TLS SNI, can reach neither. So the file is fetched
8
+ * here and handed to `INSTALL '<file>'`, which puts it where DuckDB looks; the plain `INSTALL`
9
+ * that follows is then a no-op, and `LOAD` still checks the extension's signature.
10
+ *
11
+ * Best effort: a failure is logged and leaves the extension to DuckDB's own `INSTALL`. A project
12
+ * that set `custom_extension_repository` keeps its own repository.
13
+ */
14
+ export declare function prefetchDuckDbExtensions(runtime: DuckDbQueryRuntime, extensions: readonly string[]): Promise<void>;
15
+ //# sourceMappingURL=duckdb-extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"duckdb-extensions.d.ts","sourceRoot":"","sources":["../../src/internal/duckdb-extensions.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAO1D;;;;;;;;;;;GAWG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf"}
@@ -1 +1 @@
1
- {"version":3,"file":"duckdb-runtime.d.ts","sourceRoot":"","sources":["../../src/internal/duckdb-runtime.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAC5E,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAoB,MAAM,iBAAiB,CAAA;AAW/F;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,aAAa,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAChG,YAAY,CAAC,CAAC,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAAA;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,kBAAkB;IACvD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACzF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,kBAAkB,CAAA;AAEvD,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,IAAI,CAAA;IAClB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,GAAG,IAAI,CAAA;IAChE,MAAM,IAAI,IAAI,CAAA;CACf;AAED,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAsND;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,sBAAsB,EAC/B,YAAY,GAAE,oBAAyB,GACtC,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
1
+ {"version":3,"file":"duckdb-runtime.d.ts","sourceRoot":"","sources":["../../src/internal/duckdb-runtime.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,WAAW,EAEjB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAA;AAE5E,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,mBAAmB,EAAoB,MAAM,iBAAiB,CAAA;AAW/F;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,aAAa,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC3D,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,WAAW,EAAE,GAAG,OAAO,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAA;IAChG,YAAY,CAAC,CAAC,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,CAAC,QAAQ,EAAE,cAAc,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACxD,OAAO,CAAC,CAAC,CAAC,CAAA;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,kBAAkB;IACvD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC7E,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B;;;;;OAKG;IACH,aAAa,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACzF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,GAAG,kBAAkB,CAAA;AAEvD,MAAM,WAAW,cAAc;IAC7B,UAAU,IAAI,IAAI,CAAA;IAClB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,GAAG,IAAI,CAAA;IAChE,MAAM,IAAI,IAAI,CAAA;CACf;AAED,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IACpC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAC1B;AAsND;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,aAAa,CAAC,CAuBxB;AAED;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CASf;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,sBAAsB,EAC/B,YAAY,GAAE,oBAAyB,GACtC,OAAO,CAAC,IAAI,CAAC,CA8Bf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sixb/ducklake",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "DuckDB/DuckLake lake storage provider for Sixb",
5
5
  "keywords": [
6
6
  "sixb",
@@ -36,7 +36,7 @@
36
36
  "typecheck": "tsc --noEmit",
37
37
  "test": "bun test",
38
38
  "test:e2e": "bun run test:e2e:local && bun run test:e2e:remote",
39
- "test:e2e:local": "bun test --preload ./tests/extensions.ts ./tests/ducklake-reader.e2e.ts ./tests/ducklake-changes.e2e.ts ./tests/ducklake-contract.e2e.ts ./tests/ducklake-catalog-state.e2e.ts ./tests/ducklake-catalogs.e2e.ts ./tests/ducklake-concurrency.e2e.ts ./tests/ducklake-datasets.e2e.ts ./tests/ducklake-driver.e2e.ts ./tests/ducklake-maintenance.e2e.ts ./tests/ducklake-merges.e2e.ts ./tests/ducklake-sql-transforms.e2e.ts ./tests/ducklake-versions.e2e.ts ./tests/ducklake-long-history.e2e.ts ./tests/ducklake-writes.e2e.ts ./tests/ducklake-sqlite-writes.e2e.ts",
39
+ "test:e2e:local": "bun test --preload ./tests/extensions.ts ./tests/duckdb-extensions.e2e.ts ./tests/ducklake-reader.e2e.ts ./tests/ducklake-changes.e2e.ts ./tests/ducklake-contract.e2e.ts ./tests/ducklake-catalog-state.e2e.ts ./tests/ducklake-catalogs.e2e.ts ./tests/ducklake-concurrency.e2e.ts ./tests/ducklake-datasets.e2e.ts ./tests/ducklake-driver.e2e.ts ./tests/ducklake-maintenance.e2e.ts ./tests/ducklake-merges.e2e.ts ./tests/ducklake-sql-transforms.e2e.ts ./tests/ducklake-versions.e2e.ts ./tests/ducklake-long-history.e2e.ts ./tests/ducklake-writes.e2e.ts ./tests/ducklake-sqlite-writes.e2e.ts",
40
40
  "test:e2e:remote": "bun test --preload ./tests/extensions.ts --preload ./tests/setup.ts ./tests/ducklake-remote.e2e.ts",
41
41
  "prepack": "bun run build"
42
42
  },
@@ -44,10 +44,10 @@
44
44
  "@duckdb/node-api": "1.5.2-r.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "@sixb/core": "^0.2.1"
47
+ "@sixb/core": "^0.2.2"
48
48
  },
49
49
  "devDependencies": {
50
- "@sixb/core": "0.2.1",
50
+ "@sixb/core": "0.2.2",
51
51
  "@types/bun": "latest",
52
52
  "typescript": "^5.7.0"
53
53
  },
@@ -0,0 +1,102 @@
1
+ import { mkdtemp, rm, writeFile } from "node:fs/promises"
2
+ import { tmpdir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { promisify } from "node:util"
5
+ import { gunzip } from "node:zlib"
6
+ import type { DuckDbQueryRuntime } from "./duckdb-runtime"
7
+ import { quoteSqlString, quoteSqlStringList } from "./sql"
8
+
9
+ const extensionsUrl = "https://extensions.duckdb.org"
10
+ const downloadTimeoutMillis = 60_000
11
+ const gunzipAsync = promisify(gunzip)
12
+
13
+ /**
14
+ * Install missing core extensions from `https://extensions.duckdb.org`.
15
+ *
16
+ * DuckDB's `INSTALL` downloads from `http://extensions.duckdb.org`, and fetching over HTTPS needs
17
+ * `httpfs`, which DuckDB itself fetches over HTTP. A host that only allows HTTPS by name, such as
18
+ * a Vercel sandbox whose firewall filters on TLS SNI, can reach neither. So the file is fetched
19
+ * here and handed to `INSTALL '<file>'`, which puts it where DuckDB looks; the plain `INSTALL`
20
+ * that follows is then a no-op, and `LOAD` still checks the extension's signature.
21
+ *
22
+ * Best effort: a failure is logged and leaves the extension to DuckDB's own `INSTALL`. A project
23
+ * that set `custom_extension_repository` keeps its own repository.
24
+ */
25
+ export async function prefetchDuckDbExtensions(
26
+ runtime: DuckDbQueryRuntime,
27
+ extensions: readonly string[]
28
+ ): Promise<void> {
29
+ await prefetch(runtime, extensions).catch((error: unknown) =>
30
+ warnFallback(extensions.join("', '"), error)
31
+ )
32
+ }
33
+
34
+ async function prefetch(runtime: DuckDbQueryRuntime, extensions: readonly string[]) {
35
+ const [target] = await runtime.query(`
36
+ SELECT
37
+ version() AS version,
38
+ (SELECT platform FROM pragma_platform()) AS platform,
39
+ current_setting('custom_extension_repository') AS repository
40
+ `)
41
+ if (target?.repository !== "") return
42
+
43
+ const missing = await missingExtensions(runtime, extensions)
44
+ if (missing.length === 0) return
45
+
46
+ const directory = await mkdtemp(join(tmpdir(), "sixb-duckdb-extensions-"))
47
+ try {
48
+ const downloads = await Promise.all(
49
+ missing.map(async (name) => ({
50
+ name,
51
+ file: await download(`${target.version}/${target.platform}/${name}`, directory, name).catch(
52
+ (error: unknown) => warnFallback(name, error)
53
+ ),
54
+ }))
55
+ )
56
+
57
+ // One at a time: the runtime runs statements in order anyway, and this keeps failures apart.
58
+ for (const { name, file } of downloads) {
59
+ if (file === undefined) continue
60
+ await runtime
61
+ .run(`INSTALL ${quoteSqlString(file)}`)
62
+ .catch((error: unknown) => warnFallback(name, error))
63
+ }
64
+ } finally {
65
+ await rm(directory, { recursive: true, force: true })
66
+ }
67
+ }
68
+
69
+ function warnFallback(name: string, error: unknown): undefined {
70
+ const reason = error instanceof Error ? error.message : String(error)
71
+ console.warn(
72
+ `[SixbDuckLake] Could not install DuckDB extension '${name}' over HTTPS (${reason}); falling back to DuckDB's INSTALL.`
73
+ )
74
+ return undefined
75
+ }
76
+
77
+ /** Canonical names of the requested extensions not installed yet: `sqlite` is `sqlite_scanner`. */
78
+ async function missingExtensions(
79
+ runtime: DuckDbQueryRuntime,
80
+ extensions: readonly string[]
81
+ ): Promise<string[]> {
82
+ const requested = `[${quoteSqlStringList(extensions.map((extension) => extension.toLowerCase()))}]`
83
+ const rows = await runtime.query(`
84
+ SELECT extension_name
85
+ FROM duckdb_extensions()
86
+ WHERE NOT installed
87
+ AND (list_contains(${requested}, extension_name) OR list_has_any(${requested}, aliases))
88
+ `)
89
+ return rows.map((row) => String(row.extension_name))
90
+ }
91
+
92
+ /** `INSTALL '<file>'` names the extension after the file, so it is written as `<name>.duckdb_extension`. */
93
+ async function download(path: string, directory: string, name: string): Promise<string> {
94
+ const response = await fetch(`${extensionsUrl}/${path}.duckdb_extension.gz`, {
95
+ signal: AbortSignal.timeout(downloadTimeoutMillis),
96
+ })
97
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
98
+
99
+ const file = join(directory, `${name}.duckdb_extension`)
100
+ await writeFile(file, await gunzipAsync(new Uint8Array(await response.arrayBuffer())))
101
+ return file
102
+ }
@@ -8,6 +8,7 @@ import {
8
8
  } from "@duckdb/node-api"
9
9
  import { LakeStorageError } from "@sixb/core/lake-storage"
10
10
  import type { DuckDbRuntimeOptions, DuckLakeStorageOptions } from "../types"
11
+ import { prefetchDuckDbExtensions } from "./duckdb-extensions"
11
12
  import { type DuckDbReader, type DuckDbReaderOptions, NodeDuckDbReader } from "./duckdb-reader"
12
13
  import { sixbDuckDbValueConverter } from "./duckdb-value-converter"
13
14
  import {
@@ -323,9 +324,12 @@ export async function installDuckLakeExtensions(
323
324
  runtime: DuckDbRuntime,
324
325
  options: DuckLakeStorageOptions
325
326
  ): Promise<void> {
327
+ const extensions = requiredExtensions(options)
328
+ await prefetchDuckDbExtensions(runtime, ["ducklake", ...extensions])
329
+
326
330
  await runtime.run("INSTALL ducklake")
327
331
 
328
- for (const extension of requiredExtensions(options)) {
332
+ for (const extension of extensions) {
329
333
  await runtime.run(`INSTALL ${quoteIdentifier(extension)}`)
330
334
  }
331
335
  }