@fougere/adapter-duckdb 0.19.0-alpha.0 → 0.20.0-alpha.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.
- package/dist/index.d.ts +7 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -36
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +39 -57
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { type Entity } from '@fougere/schema';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
/** A file, for sqlite. */
|
|
2
|
+
/** An entry of `sources:` as this reader sees it — what its own adapter declared, nothing added. */
|
|
3
|
+
export interface SourceEntry {
|
|
4
|
+
type?: string;
|
|
6
5
|
path?: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** Defaults to sqlite, the engine `db:` resolves from a name. */
|
|
10
|
-
type?: Attachable;
|
|
11
|
-
/** Registration names of the entities whose rows live here. */
|
|
12
|
-
entities: string[];
|
|
6
|
+
url?: string;
|
|
7
|
+
entities?: string[];
|
|
13
8
|
}
|
|
14
9
|
export interface ConnectOptions {
|
|
15
|
-
/** The
|
|
16
|
-
|
|
17
|
-
path?: string;
|
|
18
|
-
attach?: string;
|
|
19
|
-
type?: Attachable;
|
|
20
|
-
};
|
|
21
|
-
/** The other places, exactly as `fougere.config.ts` states them. */
|
|
22
|
-
sources?: Record<string, SourceDeclaration>;
|
|
10
|
+
/** The app's sources, exactly as `fougere.config.ts` states them. */
|
|
11
|
+
sources: Record<string, SourceEntry>;
|
|
23
12
|
/** What this scope may read — and therefore what gets attached at all. */
|
|
24
13
|
reads: readonly Entity[];
|
|
25
14
|
/** Same resolver the storage uses, when an app renames tables. */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,MAAM,EAAmB,MAAM,iBAAiB,CAAC;AAI3E,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,MAAM,EAAmB,MAAM,iBAAiB,CAAC;AAI3E,oGAAoG;AACpG,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,0EAA0E;IAC1E,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,kEAAkE;IAClE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC;AAED,2EAA2E;AAC3E,MAAM,WAAW,KAAK;IACpB,6DAA6D;IAC7D,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,oBAAoB,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAChH,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAeD,kDAAkD;AAClD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,CAkE5E"}
|
package/dist/index.js
CHANGED
|
@@ -2,34 +2,27 @@
|
|
|
2
2
|
import { DuckDBInstance } from '@duckdb/node-api';
|
|
3
3
|
import { lowerFirst } from '@fougere/schema';
|
|
4
4
|
import { toTable, toTableName, toSnakeCase, codecsOf } from '@fougere/adapter-sql';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* What DuckDB attaches, read off the entry's own type — the extension it loads and the key that
|
|
7
|
+
* says where. MSSQL, files and memory have no extension, which is why the list is short.
|
|
8
|
+
*/
|
|
9
|
+
const ATTACHABLE = {
|
|
10
|
+
sqlite: { extension: 'sqlite', target: (entry) => entry.path },
|
|
11
|
+
pg: { extension: 'postgres', target: (entry) => entry.url },
|
|
12
|
+
mysql: { extension: 'mysql', target: (entry) => entry.url },
|
|
9
13
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
postgres: 'postgres',
|
|
13
|
-
mysql: 'mysql',
|
|
14
|
-
};
|
|
15
|
-
/** The default source's alias — the same word `fougere.config.ts` uses for it. */
|
|
16
|
-
const DEFAULT_ALIAS = 'db';
|
|
17
|
-
/** 'Book' and 'book' name the same rows — the spelling `sources:` accepts either way. */
|
|
18
|
-
function targetOf(declaration, alias) {
|
|
19
|
-
const target = declaration.attach ?? declaration.path;
|
|
20
|
-
if (!target) {
|
|
21
|
-
throw new Error(`source '${alias}': neither \`path\` nor \`attach\` — DuckDB is told what to open, ` +
|
|
22
|
-
`and an in-memory database of its own would hold none of your rows.`);
|
|
23
|
-
}
|
|
24
|
-
return target;
|
|
25
|
-
}
|
|
14
|
+
/** DuckDB reserves `main`, `temp` and `system`, and a source may be called any of them. */
|
|
15
|
+
const attachedAs = (source) => `source_${source}`;
|
|
26
16
|
/** Open a read-only view of the app's sources. */
|
|
27
17
|
export async function connectSources(options) {
|
|
28
18
|
const resolve = options.tableName ?? toTableName;
|
|
29
|
-
// Where each named entity lives; anything unnamed is in the
|
|
19
|
+
// Where each named entity lives; anything unnamed is in the one source listing none.
|
|
30
20
|
const home = new Map();
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
let fallback;
|
|
22
|
+
for (const [alias, entry] of Object.entries(options.sources)) {
|
|
23
|
+
if (entry.entities === undefined)
|
|
24
|
+
fallback = alias;
|
|
25
|
+
for (const entity of entry.entities ?? [])
|
|
33
26
|
home.set(lowerFirst(entity), alias);
|
|
34
27
|
}
|
|
35
28
|
// Only what this scope reads is attached — the declaration IS the environment.
|
|
@@ -37,7 +30,9 @@ export async function connectSources(options) {
|
|
|
37
30
|
const placed = new Map();
|
|
38
31
|
for (const shape of options.reads) {
|
|
39
32
|
const name = lowerFirst(shape.name ?? '');
|
|
40
|
-
const alias = home.get(name) ??
|
|
33
|
+
const alias = home.get(name) ?? fallback;
|
|
34
|
+
if (!alias)
|
|
35
|
+
throw new Error(`${name} lives in no declared source — no source lists it, and none holds the rest.`);
|
|
41
36
|
wanted.add(alias);
|
|
42
37
|
placed.set(shape, { alias, table: resolve(name) });
|
|
43
38
|
}
|
|
@@ -45,20 +40,23 @@ export async function connectSources(options) {
|
|
|
45
40
|
const db = await instance.connect();
|
|
46
41
|
const attached = new Map();
|
|
47
42
|
for (const alias of wanted) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
: options.sources?.[alias];
|
|
51
|
-
if (!declaration)
|
|
43
|
+
const entry = options.sources[alias];
|
|
44
|
+
if (!entry)
|
|
52
45
|
throw new Error(`source '${alias}' is read but never declared.`);
|
|
53
|
-
const type =
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
const type = entry.type ?? 'sqlite';
|
|
47
|
+
const attachable = ATTACHABLE[type];
|
|
48
|
+
if (!attachable) {
|
|
49
|
+
throw new Error(`source '${alias}' is a ${type} source, which DuckDB cannot attach — there is no ` +
|
|
56
50
|
`such extension. Read it by key through its own storage, or mirror it into one of ` +
|
|
57
|
-
`${Object.keys(
|
|
51
|
+
`${Object.keys(ATTACHABLE).join(', ')}.`);
|
|
52
|
+
}
|
|
53
|
+
const target = attachable.target(entry);
|
|
54
|
+
if (!target) {
|
|
55
|
+
throw new Error(`source '${alias}': nothing says where it is — DuckDB is told what to open.`);
|
|
58
56
|
}
|
|
59
|
-
await db.run(`INSTALL ${
|
|
60
|
-
await db.run(`ATTACH '${
|
|
61
|
-
attached.set(alias,
|
|
57
|
+
await db.run(`INSTALL ${attachable.extension}; LOAD ${attachable.extension};`);
|
|
58
|
+
await db.run(`ATTACH '${target}' AS ${quote(attachedAs(alias))} (TYPE ${attachable.extension}, READ_ONLY);`);
|
|
59
|
+
attached.set(alias, target);
|
|
62
60
|
}
|
|
63
61
|
return {
|
|
64
62
|
attached,
|
|
@@ -82,7 +80,7 @@ export async function connectSources(options) {
|
|
|
82
80
|
function qualify(ref, placed) {
|
|
83
81
|
const found = placed.get(ref);
|
|
84
82
|
if (found)
|
|
85
|
-
return `${quote(found.alias)}.${quote(found.table)}`;
|
|
83
|
+
return `${quote(attachedAs(found.alias))}.${quote(found.table)}`;
|
|
86
84
|
const name = ref?.name;
|
|
87
85
|
if (name) {
|
|
88
86
|
throw new Error(`${name} is named in a query but not in \`reads\` — its source is not attached, so its ` +
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAgC,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/adapter-duckdb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0-alpha.0",
|
|
4
4
|
"description": "Read across an app's sources — one SQL query over what DuckDB can attach.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fougere",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"src"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@fougere/schema": "0.
|
|
33
|
-
"@fougere/adapter-sql": "0.
|
|
32
|
+
"@fougere/schema": "0.20.0-alpha.0",
|
|
33
|
+
"@fougere/adapter-sql": "0.20.0-alpha.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@duckdb/node-api": ">=1.4.0-0"
|
package/src/index.ts
CHANGED
|
@@ -4,25 +4,17 @@ import { lowerFirst, type Entity, type SchemaView } from '@fougere/schema';
|
|
|
4
4
|
import { toTable, toTableName, toSnakeCase, codecsOf } from '@fougere/adapter-sql';
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
export interface SourceDeclaration {
|
|
11
|
-
/** A file, for sqlite. */
|
|
7
|
+
/** An entry of `sources:` as this reader sees it — what its own adapter declared, nothing added. */
|
|
8
|
+
export interface SourceEntry {
|
|
9
|
+
type?: string;
|
|
12
10
|
path?: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/** Defaults to sqlite, the engine `db:` resolves from a name. */
|
|
16
|
-
type?: Attachable;
|
|
17
|
-
/** Registration names of the entities whose rows live here. */
|
|
18
|
-
entities: string[];
|
|
11
|
+
url?: string;
|
|
12
|
+
entities?: string[];
|
|
19
13
|
}
|
|
20
14
|
|
|
21
15
|
export interface ConnectOptions {
|
|
22
|
-
/** The
|
|
23
|
-
|
|
24
|
-
/** The other places, exactly as `fougere.config.ts` states them. */
|
|
25
|
-
sources?: Record<string, SourceDeclaration>;
|
|
16
|
+
/** The app's sources, exactly as `fougere.config.ts` states them. */
|
|
17
|
+
sources: Record<string, SourceEntry>;
|
|
26
18
|
/** What this scope may read — and therefore what gets attached at all. */
|
|
27
19
|
reads: readonly Entity[];
|
|
28
20
|
/** Same resolver the storage uses, when an app renames tables. */
|
|
@@ -38,43 +30,29 @@ export interface Reads {
|
|
|
38
30
|
close(): Promise<void>;
|
|
39
31
|
}
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
postgres: 'postgres',
|
|
50
|
-
mysql: 'mysql',
|
|
33
|
+
/**
|
|
34
|
+
* What DuckDB attaches, read off the entry's own type — the extension it loads and the key that
|
|
35
|
+
* says where. MSSQL, files and memory have no extension, which is why the list is short.
|
|
36
|
+
*/
|
|
37
|
+
const ATTACHABLE: Record<string, { extension: string; target: (entry: SourceEntry) => string | undefined }> = {
|
|
38
|
+
sqlite: { extension: 'sqlite', target: (entry) => entry.path },
|
|
39
|
+
pg: { extension: 'postgres', target: (entry) => entry.url },
|
|
40
|
+
mysql: { extension: 'mysql', target: (entry) => entry.url },
|
|
51
41
|
};
|
|
52
42
|
|
|
53
|
-
/**
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
/** 'Book' and 'book' name the same rows — the spelling `sources:` accepts either way. */
|
|
57
|
-
|
|
58
|
-
function targetOf(declaration: { path?: string; attach?: string }, alias: string): string {
|
|
59
|
-
const target = declaration.attach ?? declaration.path;
|
|
60
|
-
if (!target) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`source '${alias}': neither \`path\` nor \`attach\` — DuckDB is told what to open, ` +
|
|
63
|
-
`and an in-memory database of its own would hold none of your rows.`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return target;
|
|
68
|
-
}
|
|
43
|
+
/** DuckDB reserves `main`, `temp` and `system`, and a source may be called any of them. */
|
|
44
|
+
const attachedAs = (source: string): string => `source_${source}`;
|
|
69
45
|
|
|
70
46
|
/** Open a read-only view of the app's sources. */
|
|
71
47
|
export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
72
48
|
const resolve = options.tableName ?? toTableName;
|
|
73
49
|
|
|
74
|
-
// Where each named entity lives; anything unnamed is in the
|
|
50
|
+
// Where each named entity lives; anything unnamed is in the one source listing none.
|
|
75
51
|
const home = new Map<string, string>();
|
|
76
|
-
|
|
77
|
-
|
|
52
|
+
let fallback: string | undefined;
|
|
53
|
+
for (const [alias, entry] of Object.entries(options.sources)) {
|
|
54
|
+
if (entry.entities === undefined) fallback = alias;
|
|
55
|
+
for (const entity of entry.entities ?? []) home.set(lowerFirst(entity), alias);
|
|
78
56
|
}
|
|
79
57
|
|
|
80
58
|
// Only what this scope reads is attached — the declaration IS the environment.
|
|
@@ -82,7 +60,8 @@ export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
|
82
60
|
const placed = new Map<Entity, { alias: string; table: string }>();
|
|
83
61
|
for (const shape of options.reads) {
|
|
84
62
|
const name = lowerFirst((shape as { name?: string }).name ?? '');
|
|
85
|
-
const alias = home.get(name) ??
|
|
63
|
+
const alias = home.get(name) ?? fallback;
|
|
64
|
+
if (!alias) throw new Error(`${name} lives in no declared source — no source lists it, and none holds the rest.`);
|
|
86
65
|
wanted.add(alias);
|
|
87
66
|
placed.set(shape, { alias, table: resolve(name) });
|
|
88
67
|
}
|
|
@@ -92,22 +71,25 @@ export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
|
92
71
|
const attached = new Map<string, string>();
|
|
93
72
|
|
|
94
73
|
for (const alias of wanted) {
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
: options.sources?.[alias];
|
|
98
|
-
if (!declaration) throw new Error(`source '${alias}' is read but never declared.`);
|
|
74
|
+
const entry = options.sources[alias];
|
|
75
|
+
if (!entry) throw new Error(`source '${alias}' is read but never declared.`);
|
|
99
76
|
|
|
100
|
-
const type =
|
|
101
|
-
|
|
77
|
+
const type = entry.type ?? 'sqlite';
|
|
78
|
+
const attachable = ATTACHABLE[type];
|
|
79
|
+
if (!attachable) {
|
|
102
80
|
throw new Error(
|
|
103
|
-
`source '${alias}' is a ${type}
|
|
81
|
+
`source '${alias}' is a ${type} source, which DuckDB cannot attach — there is no ` +
|
|
104
82
|
`such extension. Read it by key through its own storage, or mirror it into one of ` +
|
|
105
|
-
`${Object.keys(
|
|
83
|
+
`${Object.keys(ATTACHABLE).join(', ')}.`,
|
|
106
84
|
);
|
|
107
85
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
86
|
+
const target = attachable.target(entry);
|
|
87
|
+
if (!target) {
|
|
88
|
+
throw new Error(`source '${alias}': nothing says where it is — DuckDB is told what to open.`);
|
|
89
|
+
}
|
|
90
|
+
await db.run(`INSTALL ${attachable.extension}; LOAD ${attachable.extension};`);
|
|
91
|
+
await db.run(`ATTACH '${target}' AS ${quote(attachedAs(alias))} (TYPE ${attachable.extension}, READ_ONLY);`);
|
|
92
|
+
attached.set(alias, target);
|
|
111
93
|
}
|
|
112
94
|
|
|
113
95
|
return {
|
|
@@ -133,7 +115,7 @@ export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
|
133
115
|
/** `${Book}` becomes `archive.books` — the alias AND the table, from one declaration. */
|
|
134
116
|
function qualify(ref: unknown, placed: Map<Entity, { alias: string; table: string }>): string {
|
|
135
117
|
const found = placed.get(ref as Entity);
|
|
136
|
-
if (found) return `${quote(found.alias)}.${quote(found.table)}`;
|
|
118
|
+
if (found) return `${quote(attachedAs(found.alias))}.${quote(found.table)}`;
|
|
137
119
|
const name = (ref as { name?: string } | undefined)?.name;
|
|
138
120
|
if (name) {
|
|
139
121
|
throw new Error(
|