@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.
- package/README.md +12 -0
- package/dist/index.js +74 -7
- package/dist/index.js.map +5 -4
- package/dist/internal/duckdb-extensions.d.ts +15 -0
- package/dist/internal/duckdb-extensions.d.ts.map +1 -0
- package/dist/internal/duckdb-runtime.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/internal/duckdb-extensions.ts +102 -0
- package/src/internal/duckdb-runtime.ts +5 -1
|
@@ -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;
|
|
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.
|
|
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.
|
|
47
|
+
"@sixb/core": "^0.2.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@sixb/core": "0.2.
|
|
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
|
|
332
|
+
for (const extension of extensions) {
|
|
329
333
|
await runtime.run(`INSTALL ${quoteIdentifier(extension)}`)
|
|
330
334
|
}
|
|
331
335
|
}
|