@fougere/adapter-duckdb 0.6.0-alpha.0 → 0.7.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 +4 -47
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -46
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +10 -62
package/dist/index.d.ts
CHANGED
|
@@ -1,26 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read across an app's sources — one SQL query over what can be attached.
|
|
3
|
-
*
|
|
4
|
-
* The by-key path (`findByKeys` and its dual) ENRICHES a page: it answers "I hold
|
|
5
|
-
* these rows, give me the related ones". It cannot SELECT one — filtering, sorting,
|
|
6
|
-
* paginating or counting on the other side collapses into reading that side whole.
|
|
7
|
-
* That is the hole this closes, and it is not a reporting nicety: "my loans, newest
|
|
8
|
-
* book first" crosses.
|
|
9
|
-
*
|
|
10
|
-
* What it is NOT is a query builder. A cross-source builder would be a Calcite in
|
|
11
|
-
* TypeScript, and it would promise a composability the sources have not got. The query
|
|
12
|
-
* stays SQL; what Fougere contributes is the three derivations a hand-written one would
|
|
13
|
-
* duplicate — where each entity lives, what its table and columns are called, and the
|
|
14
|
-
* shape of the answer.
|
|
15
|
-
*
|
|
16
|
-
* Measured before writing any of it (2026-08-15):
|
|
17
|
-
*
|
|
18
|
-
* - attaching Postgres pushes the filter down — `Filters: lang='fr'` reaches it over
|
|
19
|
-
* 100 000 rows, so a real database is queried where it is and never copied;
|
|
20
|
-
* - at PAGE size DuckDB is ~100× slower than two indexed reads (7 ms of floor per
|
|
21
|
-
* query), so this must never sit on the ordinary read path;
|
|
22
|
-
* - SQL Server cannot be attached at all — no `sqlserver` extension exists.
|
|
23
|
-
*/
|
|
1
|
+
/** Read across an app's sources — one SQL query over what can be attached. */
|
|
24
2
|
import { type DuckDBConnection } from '@duckdb/node-api';
|
|
25
3
|
type ShapeClass = abstract new (...args: any[]) => any;
|
|
26
4
|
/** The engines DuckDB can attach — and the reason the list is short. */
|
|
@@ -44,41 +22,20 @@ export interface ConnectOptions {
|
|
|
44
22
|
};
|
|
45
23
|
/** The other places, exactly as `fougere.config.ts` states them. */
|
|
46
24
|
sources?: Record<string, SourceDeclaration>;
|
|
47
|
-
/**
|
|
48
|
-
* What this scope may read — and therefore what gets attached at all.
|
|
49
|
-
*
|
|
50
|
-
* Not a check performed after the fact: a source holding none of these is never
|
|
51
|
-
* attached, so its tables do not exist in this connection. `facadeFor` excludes an
|
|
52
|
-
* entity with no door on purpose ("it would publish the auth tables to anyone who
|
|
53
|
-
* asks"), and a SQL door at app scope would hand them over — this is what stops it.
|
|
54
|
-
*/
|
|
25
|
+
/** What this scope may read — and therefore what gets attached at all. */
|
|
55
26
|
reads: readonly ShapeClass[];
|
|
56
27
|
/** Same resolver the storage uses, when an app renames tables. */
|
|
57
28
|
tableName?: (name: string) => string;
|
|
58
29
|
}
|
|
59
30
|
/** A query's answer: the rows, projected onto the shape that was named. */
|
|
60
31
|
export interface Reads {
|
|
61
|
-
/**
|
|
62
|
-
* Name the shape the answer takes, then write the query.
|
|
63
|
-
*
|
|
64
|
-
* The tag is only reachable THROUGH the shape, so "no query without a declared
|
|
65
|
-
* output" is held by the types rather than asked for. It matters: a raw `select *`
|
|
66
|
-
* would return the fields `boundary.out: 'closed'` promises never leave — the one
|
|
67
|
-
* strong guarantee the framework makes, walked around. The projection is the fence.
|
|
68
|
-
*/
|
|
32
|
+
/** Name the shape the answer takes, then write the query. */
|
|
69
33
|
read<E extends ShapeClass>(shape: E): (sql: TemplateStringsArray, ...refs: unknown[]) => Promise<InstanceType<E>[]>;
|
|
70
34
|
/** The attached sources, by alias — what the scope actually opened. */
|
|
71
35
|
attached: ReadonlyMap<string, string>;
|
|
72
36
|
close(): Promise<void>;
|
|
73
37
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Open a read-only view of the app's sources.
|
|
76
|
-
*
|
|
77
|
-
* `READ_ONLY` on every attach, and it is the engine that enforces it rather than a rule
|
|
78
|
-
* anyone here remembers: this door reads across an app's whole storage, so it is
|
|
79
|
-
* strictly more reachable than `storage.client` — which at least keeps the scope of its
|
|
80
|
-
* entity — and a write through it would meet no judge at all.
|
|
81
|
-
*/
|
|
38
|
+
/** Open a read-only view of the app's sources. */
|
|
82
39
|
export declare function connectSources(options: ConnectOptions): Promise<Reads>;
|
|
83
40
|
export type { DuckDBConnection };
|
|
84
41
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAKzE,KAAK,UAAU,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,CAAC;AAEvD,wEAAwE;AACxE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,uEAAuE;IACvE,EAAE,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,UAAU,CAAA;KAAE,CAAC;IAC1D,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAC5C,0EAA0E;IAC1E,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IAC7B,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,UAAU,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;IACpH,uEAAuE;IACvE,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA8BD,kDAAkD;AAClD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,CA2D5E;AAiDD,YAAY,EAAE,gBAAgB,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read across an app's sources — one SQL query over what can be attached.
|
|
3
|
-
*
|
|
4
|
-
* The by-key path (`findByKeys` and its dual) ENRICHES a page: it answers "I hold
|
|
5
|
-
* these rows, give me the related ones". It cannot SELECT one — filtering, sorting,
|
|
6
|
-
* paginating or counting on the other side collapses into reading that side whole.
|
|
7
|
-
* That is the hole this closes, and it is not a reporting nicety: "my loans, newest
|
|
8
|
-
* book first" crosses.
|
|
9
|
-
*
|
|
10
|
-
* What it is NOT is a query builder. A cross-source builder would be a Calcite in
|
|
11
|
-
* TypeScript, and it would promise a composability the sources have not got. The query
|
|
12
|
-
* stays SQL; what Fougere contributes is the three derivations a hand-written one would
|
|
13
|
-
* duplicate — where each entity lives, what its table and columns are called, and the
|
|
14
|
-
* shape of the answer.
|
|
15
|
-
*
|
|
16
|
-
* Measured before writing any of it (2026-08-15):
|
|
17
|
-
*
|
|
18
|
-
* - attaching Postgres pushes the filter down — `Filters: lang='fr'` reaches it over
|
|
19
|
-
* 100 000 rows, so a real database is queried where it is and never copied;
|
|
20
|
-
* - at PAGE size DuckDB is ~100× slower than two indexed reads (7 ms of floor per
|
|
21
|
-
* query), so this must never sit on the ordinary read path;
|
|
22
|
-
* - SQL Server cannot be attached at all — no `sqlserver` extension exists.
|
|
23
|
-
*/
|
|
1
|
+
/** Read across an app's sources — one SQL query over what can be attached. */
|
|
24
2
|
import { DuckDBInstance } from '@duckdb/node-api';
|
|
25
|
-
import { lowerFirst
|
|
3
|
+
import { lowerFirst } from '@fougere/schema';
|
|
26
4
|
import { toTable, toTableName, toSnakeCase, codecsOf } from '@fougere/adapter-sql';
|
|
27
5
|
const EXTENSION = {
|
|
28
6
|
sqlite: 'sqlite',
|
|
@@ -34,11 +12,7 @@ const TYPE_CLAUSE = {
|
|
|
34
12
|
postgres: 'postgres',
|
|
35
13
|
mysql: 'mysql',
|
|
36
14
|
};
|
|
37
|
-
/**
|
|
38
|
-
* The default source's alias — the same word `fougere.config.ts` uses for it.
|
|
39
|
-
*
|
|
40
|
-
* Not `main`: DuckDB reserves it, and an attach under that name is refused outright.
|
|
41
|
-
*/
|
|
15
|
+
/** The default source's alias — the same word `fougere.config.ts` uses for it. */
|
|
42
16
|
const DEFAULT_ALIAS = 'db';
|
|
43
17
|
/** 'Book' and 'book' name the same rows — the spelling `sources:` accepts either way. */
|
|
44
18
|
function targetOf(declaration, alias) {
|
|
@@ -49,14 +23,7 @@ function targetOf(declaration, alias) {
|
|
|
49
23
|
}
|
|
50
24
|
return target;
|
|
51
25
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Open a read-only view of the app's sources.
|
|
54
|
-
*
|
|
55
|
-
* `READ_ONLY` on every attach, and it is the engine that enforces it rather than a rule
|
|
56
|
-
* anyone here remembers: this door reads across an app's whole storage, so it is
|
|
57
|
-
* strictly more reachable than `storage.client` — which at least keeps the scope of its
|
|
58
|
-
* entity — and a write through it would meet no judge at all.
|
|
59
|
-
*/
|
|
26
|
+
/** Open a read-only view of the app's sources. */
|
|
60
27
|
export async function connectSources(options) {
|
|
61
28
|
const resolve = options.tableName ?? toTableName;
|
|
62
29
|
// Where each named entity lives; anything unnamed is in the default source.
|
|
@@ -97,8 +64,9 @@ export async function connectSources(options) {
|
|
|
97
64
|
attached,
|
|
98
65
|
close: async () => { db.closeSync(); },
|
|
99
66
|
read(shape) {
|
|
100
|
-
const
|
|
101
|
-
const
|
|
67
|
+
const schema = shape;
|
|
68
|
+
const fields = schema.getFields();
|
|
69
|
+
const codecs = codecsOf(toTable('x', schema).columns);
|
|
102
70
|
const names = Object.keys(fields);
|
|
103
71
|
return async (parts, ...refs) => {
|
|
104
72
|
const sql = parts.reduce((out, part, i) => out + part + (i < refs.length ? qualify(refs[i], placed) : ''), '');
|
|
@@ -126,13 +94,7 @@ function qualify(ref, placed) {
|
|
|
126
94
|
`query text you write, or filter the result.`);
|
|
127
95
|
}
|
|
128
96
|
const quote = (identifier) => `"${identifier.replace(/"/g, '""')}"`;
|
|
129
|
-
/**
|
|
130
|
-
* The column set, checked ONCE against the shape.
|
|
131
|
-
*
|
|
132
|
-
* Not the JSON Schema judge per row: an aggregate is not a client's input, and the
|
|
133
|
-
* caller here is the query its own author wrote. What must not pass silently is the two
|
|
134
|
-
* disagreeing — a renamed alias yielding a column of nulls under the right type.
|
|
135
|
-
*/
|
|
97
|
+
/** The column set, checked ONCE against the shape. */
|
|
136
98
|
function refuseMismatch(shape, names, row, sql) {
|
|
137
99
|
const missing = names.filter((name) => !(toSnakeCase(name) in row) && !(name in row));
|
|
138
100
|
if (missing.length === 0)
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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.7.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/
|
|
33
|
-
"@fougere/
|
|
32
|
+
"@fougere/adapter-sql": "0.7.0-alpha.0",
|
|
33
|
+
"@fougere/schema": "0.7.0-alpha.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@duckdb/node-api": ">=1.4.0-0"
|
package/src/index.ts
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read across an app's sources — one SQL query over what can be attached.
|
|
3
|
-
*
|
|
4
|
-
* The by-key path (`findByKeys` and its dual) ENRICHES a page: it answers "I hold
|
|
5
|
-
* these rows, give me the related ones". It cannot SELECT one — filtering, sorting,
|
|
6
|
-
* paginating or counting on the other side collapses into reading that side whole.
|
|
7
|
-
* That is the hole this closes, and it is not a reporting nicety: "my loans, newest
|
|
8
|
-
* book first" crosses.
|
|
9
|
-
*
|
|
10
|
-
* What it is NOT is a query builder. A cross-source builder would be a Calcite in
|
|
11
|
-
* TypeScript, and it would promise a composability the sources have not got. The query
|
|
12
|
-
* stays SQL; what Fougere contributes is the three derivations a hand-written one would
|
|
13
|
-
* duplicate — where each entity lives, what its table and columns are called, and the
|
|
14
|
-
* shape of the answer.
|
|
15
|
-
*
|
|
16
|
-
* Measured before writing any of it (2026-08-15):
|
|
17
|
-
*
|
|
18
|
-
* - attaching Postgres pushes the filter down — `Filters: lang='fr'` reaches it over
|
|
19
|
-
* 100 000 rows, so a real database is queried where it is and never copied;
|
|
20
|
-
* - at PAGE size DuckDB is ~100× slower than two indexed reads (7 ms of floor per
|
|
21
|
-
* query), so this must never sit on the ordinary read path;
|
|
22
|
-
* - SQL Server cannot be attached at all — no `sqlserver` extension exists.
|
|
23
|
-
*/
|
|
1
|
+
/** Read across an app's sources — one SQL query over what can be attached. */
|
|
24
2
|
import { DuckDBInstance, type DuckDBConnection } from '@duckdb/node-api';
|
|
25
|
-
import { lowerFirst,
|
|
3
|
+
import { lowerFirst, type SchemaView } from '@fougere/schema';
|
|
26
4
|
import { toTable, toTableName, toSnakeCase, codecsOf } from '@fougere/adapter-sql';
|
|
27
5
|
|
|
28
6
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -47,14 +25,7 @@ export interface ConnectOptions {
|
|
|
47
25
|
db: { path?: string; attach?: string; type?: Attachable };
|
|
48
26
|
/** The other places, exactly as `fougere.config.ts` states them. */
|
|
49
27
|
sources?: Record<string, SourceDeclaration>;
|
|
50
|
-
/**
|
|
51
|
-
* What this scope may read — and therefore what gets attached at all.
|
|
52
|
-
*
|
|
53
|
-
* Not a check performed after the fact: a source holding none of these is never
|
|
54
|
-
* attached, so its tables do not exist in this connection. `facadeFor` excludes an
|
|
55
|
-
* entity with no door on purpose ("it would publish the auth tables to anyone who
|
|
56
|
-
* asks"), and a SQL door at app scope would hand them over — this is what stops it.
|
|
57
|
-
*/
|
|
28
|
+
/** What this scope may read — and therefore what gets attached at all. */
|
|
58
29
|
reads: readonly ShapeClass[];
|
|
59
30
|
/** Same resolver the storage uses, when an app renames tables. */
|
|
60
31
|
tableName?: (name: string) => string;
|
|
@@ -62,14 +33,7 @@ export interface ConnectOptions {
|
|
|
62
33
|
|
|
63
34
|
/** A query's answer: the rows, projected onto the shape that was named. */
|
|
64
35
|
export interface Reads {
|
|
65
|
-
/**
|
|
66
|
-
* Name the shape the answer takes, then write the query.
|
|
67
|
-
*
|
|
68
|
-
* The tag is only reachable THROUGH the shape, so "no query without a declared
|
|
69
|
-
* output" is held by the types rather than asked for. It matters: a raw `select *`
|
|
70
|
-
* would return the fields `boundary.out: 'closed'` promises never leave — the one
|
|
71
|
-
* strong guarantee the framework makes, walked around. The projection is the fence.
|
|
72
|
-
*/
|
|
36
|
+
/** Name the shape the answer takes, then write the query. */
|
|
73
37
|
read<E extends ShapeClass>(shape: E): (sql: TemplateStringsArray, ...refs: unknown[]) => Promise<InstanceType<E>[]>;
|
|
74
38
|
/** The attached sources, by alias — what the scope actually opened. */
|
|
75
39
|
attached: ReadonlyMap<string, string>;
|
|
@@ -88,11 +52,7 @@ const TYPE_CLAUSE: Record<Attachable, string> = {
|
|
|
88
52
|
mysql: 'mysql',
|
|
89
53
|
};
|
|
90
54
|
|
|
91
|
-
/**
|
|
92
|
-
* The default source's alias — the same word `fougere.config.ts` uses for it.
|
|
93
|
-
*
|
|
94
|
-
* Not `main`: DuckDB reserves it, and an attach under that name is refused outright.
|
|
95
|
-
*/
|
|
55
|
+
/** The default source's alias — the same word `fougere.config.ts` uses for it. */
|
|
96
56
|
const DEFAULT_ALIAS = 'db';
|
|
97
57
|
|
|
98
58
|
/** 'Book' and 'book' name the same rows — the spelling `sources:` accepts either way. */
|
|
@@ -108,14 +68,7 @@ function targetOf(declaration: { path?: string; attach?: string }, alias: string
|
|
|
108
68
|
return target;
|
|
109
69
|
}
|
|
110
70
|
|
|
111
|
-
/**
|
|
112
|
-
* Open a read-only view of the app's sources.
|
|
113
|
-
*
|
|
114
|
-
* `READ_ONLY` on every attach, and it is the engine that enforces it rather than a rule
|
|
115
|
-
* anyone here remembers: this door reads across an app's whole storage, so it is
|
|
116
|
-
* strictly more reachable than `storage.client` — which at least keeps the scope of its
|
|
117
|
-
* entity — and a write through it would meet no judge at all.
|
|
118
|
-
*/
|
|
71
|
+
/** Open a read-only view of the app's sources. */
|
|
119
72
|
export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
120
73
|
const resolve = options.tableName ?? toTableName;
|
|
121
74
|
|
|
@@ -162,8 +115,9 @@ export async function connectSources(options: ConnectOptions): Promise<Reads> {
|
|
|
162
115
|
attached,
|
|
163
116
|
close: async () => { db.closeSync(); },
|
|
164
117
|
read<E extends ShapeClass>(shape: E) {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
118
|
+
const schema = shape as unknown as SchemaView;
|
|
119
|
+
const fields = schema.getFields();
|
|
120
|
+
const codecs = codecsOf(toTable('x', schema).columns);
|
|
167
121
|
const names = Object.keys(fields);
|
|
168
122
|
|
|
169
123
|
return async (parts: TemplateStringsArray, ...refs: unknown[]) => {
|
|
@@ -197,13 +151,7 @@ function qualify(ref: unknown, placed: Map<ShapeClass, { alias: string; table: s
|
|
|
197
151
|
|
|
198
152
|
const quote = (identifier: string): string => `"${identifier.replace(/"/g, '""')}"`;
|
|
199
153
|
|
|
200
|
-
/**
|
|
201
|
-
* The column set, checked ONCE against the shape.
|
|
202
|
-
*
|
|
203
|
-
* Not the JSON Schema judge per row: an aggregate is not a client's input, and the
|
|
204
|
-
* caller here is the query its own author wrote. What must not pass silently is the two
|
|
205
|
-
* disagreeing — a renamed alias yielding a column of nulls under the right type.
|
|
206
|
-
*/
|
|
154
|
+
/** The column set, checked ONCE against the shape. */
|
|
207
155
|
function refuseMismatch(shape: ShapeClass, names: string[], row: Record<string, unknown>, sql: string): void {
|
|
208
156
|
const missing = names.filter((name) => !(toSnakeCase(name) in row) && !(name in row));
|
|
209
157
|
if (missing.length === 0) return;
|