@gscdump/engine-duckdb-wasm 1.3.2 → 1.4.1
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/archetype-sql.d.mts +15 -0
- package/dist/archetype-sql.mjs +373 -0
- package/dist/drizzle-adapter/client.d.mts +9 -0
- package/dist/drizzle-adapter/client.mjs +20 -0
- package/dist/drizzle-adapter/driver.d.mts +14 -0
- package/dist/drizzle-adapter/driver.mjs +21 -0
- package/dist/drizzle-adapter/index.d.mts +3 -0
- package/dist/drizzle-adapter/index.mjs +3 -0
- package/dist/drizzle-adapter/session.d.mts +8 -0
- package/dist/drizzle-adapter/session.mjs +28 -0
- package/dist/index.d.mts +9 -493
- package/dist/index.mjs +9 -1555
- package/dist/opfs-registry.mjs +89 -0
- package/dist/opfs.d.mts +195 -0
- package/dist/opfs.mjs +584 -0
- package/dist/overlay-view.d.mts +33 -0
- package/dist/overlay-view.mjs +10 -0
- package/dist/runner.d.mts +28 -0
- package/dist/runner.mjs +19 -0
- package/dist/runtime.d.mts +210 -0
- package/dist/runtime.mjs +429 -0
- package/dist/schema.d.mts +2 -0
- package/dist/schema.mjs +2 -0
- package/package.json +4 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ArchetypeQuery } from "@gscdump/contracts/archetypes";
|
|
2
|
+
/** A compiled, parameterised statement. */
|
|
3
|
+
interface CompiledArchetypeSql {
|
|
4
|
+
sql: string;
|
|
5
|
+
params: unknown[];
|
|
6
|
+
/** The fact-table view the query reads. */
|
|
7
|
+
table: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Compile one archetype query to DuckDB SQL. Throws for `aux-cloud-only`.
|
|
11
|
+
*/
|
|
12
|
+
declare function compileArchetypeSql(query: ArchetypeQuery): CompiledArchetypeSql;
|
|
13
|
+
/** The fact-table view an archetype reads — drives per-table browser routing. */
|
|
14
|
+
declare function tableForArchetype(query: ArchetypeQuery): string | null;
|
|
15
|
+
export { CompiledArchetypeSql, compileArchetypeSql, tableForArchetype };
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
const METRIC_SQL = {
|
|
2
|
+
clicks: "SUM(clicks)",
|
|
3
|
+
impressions: "SUM(impressions)",
|
|
4
|
+
ctr: "CASE WHEN SUM(impressions) = 0 THEN 0 ELSE SUM(clicks) * 1.0 / SUM(impressions) END",
|
|
5
|
+
position: "CASE WHEN SUM(impressions) = 0 THEN NULL ELSE SUM(sum_position) * 1.0 / SUM(impressions) + 1 END"
|
|
6
|
+
};
|
|
7
|
+
const DIM_COLUMN = {
|
|
8
|
+
page: "url",
|
|
9
|
+
query: "query",
|
|
10
|
+
country: "country",
|
|
11
|
+
date: "date",
|
|
12
|
+
searchAppearance: "search_appearance"
|
|
13
|
+
};
|
|
14
|
+
function quoteIdent(value) {
|
|
15
|
+
return `"${value.replace(/"/g, "\"\"")}"`;
|
|
16
|
+
}
|
|
17
|
+
function qualifiedColumn(table, column) {
|
|
18
|
+
return `${quoteIdent(table)}.${quoteIdent(column)}`;
|
|
19
|
+
}
|
|
20
|
+
function queryCanonicalExpr(table) {
|
|
21
|
+
const queryRef = qualifiedColumn(table, "query");
|
|
22
|
+
return `COALESCE((SELECT qd.query_canonical FROM query_dim qd WHERE qd.query = ${queryRef} LIMIT 1), ${queryRef})`;
|
|
23
|
+
}
|
|
24
|
+
function dimExpr(dim, table) {
|
|
25
|
+
return dim === "queryCanonical" ? queryCanonicalExpr(table) : DIM_COLUMN[dim] ?? dim;
|
|
26
|
+
}
|
|
27
|
+
function tableForDimensions(dims) {
|
|
28
|
+
const set = new Set(dims.filter((d) => d !== "date"));
|
|
29
|
+
if (set.has("page") && (set.has("query") || set.has("queryCanonical"))) return "page_queries";
|
|
30
|
+
if (set.has("query") || set.has("queryCanonical")) return "queries";
|
|
31
|
+
if (set.has("country")) return "countries";
|
|
32
|
+
if (set.has("device")) return "dates";
|
|
33
|
+
return "pages";
|
|
34
|
+
}
|
|
35
|
+
function tableForTopNBreakdown(query) {
|
|
36
|
+
const dims = [query.dimension];
|
|
37
|
+
for (const facet of query.facets ?? []) {
|
|
38
|
+
const hasPage = query.dimension === "page" || facet.column === "page";
|
|
39
|
+
const hasQuery = query.dimension === "query" || query.dimension === "queryCanonical" || facet.column === "query" || facet.column === "queryCanonical";
|
|
40
|
+
if (hasPage && hasQuery) dims.push(facet.column);
|
|
41
|
+
}
|
|
42
|
+
return tableForDimensions(dims);
|
|
43
|
+
}
|
|
44
|
+
function metricExpr(metric) {
|
|
45
|
+
const expr = METRIC_SQL[metric];
|
|
46
|
+
if (!expr) throw new Error(`[archetype-sql] unknown metric: ${metric}`);
|
|
47
|
+
return expr;
|
|
48
|
+
}
|
|
49
|
+
function metricSelectList(metrics) {
|
|
50
|
+
return metrics.map((m) => `${metricExpr(m)} AS ${m}`).join(", ");
|
|
51
|
+
}
|
|
52
|
+
const DEVICE_VALUES = [
|
|
53
|
+
"DESKTOP",
|
|
54
|
+
"MOBILE",
|
|
55
|
+
"TABLET"
|
|
56
|
+
];
|
|
57
|
+
const DEVICE_SUFFIX = {
|
|
58
|
+
DESKTOP: "desktop",
|
|
59
|
+
MOBILE: "mobile",
|
|
60
|
+
TABLET: "tablet"
|
|
61
|
+
};
|
|
62
|
+
function deviceMetricExpr(metric, suffix) {
|
|
63
|
+
switch (metric) {
|
|
64
|
+
case "clicks": return `SUM(clicks_${suffix})`;
|
|
65
|
+
case "impressions": return `SUM(impressions_${suffix})`;
|
|
66
|
+
case "ctr": return `CASE WHEN SUM(impressions_${suffix}) = 0 THEN 0 ELSE SUM(clicks_${suffix}) * 1.0 / SUM(impressions_${suffix}) END`;
|
|
67
|
+
case "position": return `CASE WHEN SUM(impressions_${suffix}) = 0 THEN NULL ELSE SUM(sum_position_${suffix}) * 1.0 / SUM(impressions_${suffix}) + 1 END`;
|
|
68
|
+
default: throw new Error(`[archetype-sql] unknown metric: ${metric}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function deviceUnpivotSql(metrics, whereSql, byDate) {
|
|
72
|
+
const dateCol = byDate ? "date, " : "";
|
|
73
|
+
const dateGroup = byDate ? " GROUP BY date" : "";
|
|
74
|
+
return DEVICE_VALUES.map((dv) => {
|
|
75
|
+
const suffix = DEVICE_SUFFIX[dv];
|
|
76
|
+
const mcols = metrics.map((m) => `${deviceMetricExpr(m, suffix)} AS ${m}`).join(", ");
|
|
77
|
+
return `SELECT ${dateCol}'${dv}' AS device, ${mcols} FROM dates WHERE ${whereSql}${dateGroup}`;
|
|
78
|
+
}).join(" UNION ALL ");
|
|
79
|
+
}
|
|
80
|
+
function rangePredicate(q) {
|
|
81
|
+
return {
|
|
82
|
+
sql: "date BETWEEN ? AND ?",
|
|
83
|
+
params: [q.range.start, q.range.end]
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function compareRangePredicate(q) {
|
|
87
|
+
if (!q.compareRange) return null;
|
|
88
|
+
return {
|
|
89
|
+
sql: "date BETWEEN ? AND ?",
|
|
90
|
+
params: [q.compareRange.start, q.compareRange.end]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const STD_METRICS = [
|
|
94
|
+
"clicks",
|
|
95
|
+
"impressions",
|
|
96
|
+
"ctr",
|
|
97
|
+
"position"
|
|
98
|
+
];
|
|
99
|
+
function coalesceMetric(metric, src, alias) {
|
|
100
|
+
const ref = `${src}.${metric}`;
|
|
101
|
+
return metric === "clicks" || metric === "impressions" ? `CAST(COALESCE(${ref}, 0) AS DOUBLE) AS ${alias}` : `COALESCE(${ref}, 0) AS ${alias}`;
|
|
102
|
+
}
|
|
103
|
+
function prevAlias(metric) {
|
|
104
|
+
return `prev${metric.charAt(0).toUpperCase()}${metric.slice(1)}`;
|
|
105
|
+
}
|
|
106
|
+
function moverClause(movers) {
|
|
107
|
+
const curClicks = "COALESCE(c.clicks, 0)";
|
|
108
|
+
const prevClicks = "COALESCE(p.clicks, 0)";
|
|
109
|
+
const curImpr = "COALESCE(c.impressions, 0)";
|
|
110
|
+
const prevImpr = "COALESCE(p.impressions, 0)";
|
|
111
|
+
switch (movers) {
|
|
112
|
+
case "improving": return {
|
|
113
|
+
where: `${curClicks} > ${prevClicks}`,
|
|
114
|
+
order: `(${curClicks} - ${prevClicks}) DESC`
|
|
115
|
+
};
|
|
116
|
+
case "declining": return {
|
|
117
|
+
where: `${curClicks} < ${prevClicks}`,
|
|
118
|
+
order: `(${curClicks} - ${prevClicks}) ASC`
|
|
119
|
+
};
|
|
120
|
+
case "new": return {
|
|
121
|
+
where: `${prevImpr} = 0 AND ${curImpr} > 0`,
|
|
122
|
+
order: `${curClicks} DESC, ${curImpr} DESC`
|
|
123
|
+
};
|
|
124
|
+
case "lost": return {
|
|
125
|
+
where: `${curImpr} = 0 AND ${prevImpr} > 0`,
|
|
126
|
+
order: `${prevImpr} DESC`
|
|
127
|
+
};
|
|
128
|
+
default: throw new Error(`[archetype-sql] unknown movers mode: ${movers}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function facetPredicate(query, table) {
|
|
132
|
+
const facets = query.facets;
|
|
133
|
+
if (!facets?.length) return {
|
|
134
|
+
sql: "",
|
|
135
|
+
params: []
|
|
136
|
+
};
|
|
137
|
+
const parts = [];
|
|
138
|
+
const params = [];
|
|
139
|
+
for (const f of facets) {
|
|
140
|
+
const col = dimExpr(f.column, table);
|
|
141
|
+
switch (f.op) {
|
|
142
|
+
case "eq":
|
|
143
|
+
parts.push(`${col} = ?`);
|
|
144
|
+
params.push(f.value);
|
|
145
|
+
break;
|
|
146
|
+
case "regex":
|
|
147
|
+
parts.push(`regexp_matches(LOWER(${col}), ?)`);
|
|
148
|
+
params.push(f.value);
|
|
149
|
+
break;
|
|
150
|
+
case "notRegex":
|
|
151
|
+
parts.push(`NOT regexp_matches(LOWER(${col}), ?)`);
|
|
152
|
+
params.push(f.value);
|
|
153
|
+
break;
|
|
154
|
+
default: throw new Error(`[archetype-sql] unknown facet op: ${f.op}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
sql: parts.length ? ` AND ${parts.join(" AND ")}` : "",
|
|
159
|
+
params
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function compileArchetypeSql(query) {
|
|
163
|
+
switch (query.archetype) {
|
|
164
|
+
case "site-daily-timeseries": {
|
|
165
|
+
const table = "dates";
|
|
166
|
+
const where = rangePredicate(query);
|
|
167
|
+
return {
|
|
168
|
+
table,
|
|
169
|
+
sql: `SELECT date, ${query.metrics.map((m) => `${metricExpr(m)} AS ${m}`).join(", ")} FROM ${table} WHERE ${where.sql} GROUP BY date ORDER BY date`,
|
|
170
|
+
params: where.params
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
case "entity-daily-timeseries": {
|
|
174
|
+
const table = tableForDimensions([query.entity.dimension]);
|
|
175
|
+
const col = dimExpr(query.entity.dimension, table);
|
|
176
|
+
const where = rangePredicate(query);
|
|
177
|
+
return {
|
|
178
|
+
table,
|
|
179
|
+
sql: `SELECT date, ${metricSelectList(query.metrics)} FROM ${table} WHERE ${where.sql} AND ${col} = ? GROUP BY date ORDER BY date`,
|
|
180
|
+
params: [...where.params, query.entity.value]
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
case "entity-daily-sparkline": {
|
|
184
|
+
const table = tableForDimensions([query.dimension]);
|
|
185
|
+
const col = dimExpr(query.dimension, table);
|
|
186
|
+
const where = rangePredicate(query);
|
|
187
|
+
if (query.entities.length === 0) throw new Error("[archetype-sql] entity-daily-sparkline requires resolved entities");
|
|
188
|
+
const placeholders = query.entities.map(() => "?").join(", ");
|
|
189
|
+
return {
|
|
190
|
+
table,
|
|
191
|
+
sql: `SELECT ${col} AS entity, date, ${metricExpr(query.metric)} AS ${query.metric} FROM ${table} WHERE ${where.sql} AND ${col} IN (${placeholders}) GROUP BY ${col}, date ORDER BY ${col}, date`,
|
|
192
|
+
params: [...where.params, ...query.entities]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
case "top-n-breakdown": {
|
|
196
|
+
const table = tableForTopNBreakdown(query);
|
|
197
|
+
const where = rangePredicate(query);
|
|
198
|
+
const cmp = compareRangePredicate(query);
|
|
199
|
+
const dir = query.orderBy.dir === "asc" ? "ASC" : "DESC";
|
|
200
|
+
const compareOrder = `COALESCE(c.${query.orderBy.metric}, 0) ${dir}`;
|
|
201
|
+
const metricList = query.metrics.includes(query.orderBy.metric) ? query.metrics : [...query.metrics, query.orderBy.metric];
|
|
202
|
+
const variantSel = query.dimension === "queryCanonical" ? ", COUNT(DISTINCT query) AS variantCount" : "";
|
|
203
|
+
if (query.dimension === "device") {
|
|
204
|
+
if (cmp) {
|
|
205
|
+
const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
|
|
206
|
+
const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
|
|
207
|
+
let sql = `WITH cur AS (${deviceUnpivotSql(metricList, where.sql, false)}), prev AS (${deviceUnpivotSql(STD_METRICS, cmp.sql, false)}) SELECT COALESCE(c.device, p.device) AS device, ${curCols}, ${prevCols} FROM cur c FULL OUTER JOIN prev p ON c.device = p.device ORDER BY ${compareOrder} LIMIT ?`;
|
|
208
|
+
const params = [
|
|
209
|
+
...where.params,
|
|
210
|
+
...where.params,
|
|
211
|
+
...where.params,
|
|
212
|
+
...cmp.params,
|
|
213
|
+
...cmp.params,
|
|
214
|
+
...cmp.params,
|
|
215
|
+
query.limit
|
|
216
|
+
];
|
|
217
|
+
if (query.offset && query.offset > 0) {
|
|
218
|
+
sql += " OFFSET ?";
|
|
219
|
+
params.push(query.offset);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
table,
|
|
223
|
+
sql,
|
|
224
|
+
params
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
let sql = `SELECT device, ${metricList.join(", ")} FROM (${deviceUnpivotSql(metricList, where.sql, false)}) ORDER BY ${query.orderBy.metric} ${dir} LIMIT ?`;
|
|
228
|
+
const params = [
|
|
229
|
+
...where.params,
|
|
230
|
+
...where.params,
|
|
231
|
+
...where.params,
|
|
232
|
+
query.limit
|
|
233
|
+
];
|
|
234
|
+
if (query.offset && query.offset > 0) {
|
|
235
|
+
sql += " OFFSET ?";
|
|
236
|
+
params.push(query.offset);
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
table,
|
|
240
|
+
sql,
|
|
241
|
+
params
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const col = dimExpr(query.dimension, table);
|
|
245
|
+
const facet = facetPredicate(query, table);
|
|
246
|
+
const totalCol = query.includeTotal ? ", COUNT(*) OVER() AS __total" : "";
|
|
247
|
+
if (cmp) {
|
|
248
|
+
const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
|
|
249
|
+
const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
|
|
250
|
+
const variantOut = query.dimension === "queryCanonical" ? ", c.variantCount AS variantCount" : "";
|
|
251
|
+
const mover = query.movers ? moverClause(query.movers) : null;
|
|
252
|
+
const moverWhere = mover ? `WHERE ${mover.where} ` : "";
|
|
253
|
+
const orderSql = mover ? mover.order : compareOrder;
|
|
254
|
+
let sql = `WITH cur AS (SELECT ${col} AS k, ${metricSelectList(metricList)}${variantSel} FROM ${table} WHERE ${where.sql}${facet.sql} GROUP BY ${col}), prev AS (SELECT ${col} AS k, ${metricSelectList(STD_METRICS)} FROM ${table} WHERE ${cmp.sql}${facet.sql} GROUP BY ${col}) SELECT COALESCE(c.k, p.k) AS ${query.dimension}, ${curCols}, ${prevCols}${variantOut}${totalCol} FROM cur c FULL OUTER JOIN prev p ON c.k = p.k ${moverWhere}ORDER BY ${orderSql} LIMIT ?`;
|
|
255
|
+
const params = [
|
|
256
|
+
...where.params,
|
|
257
|
+
...facet.params,
|
|
258
|
+
...cmp.params,
|
|
259
|
+
...facet.params,
|
|
260
|
+
query.limit
|
|
261
|
+
];
|
|
262
|
+
if (query.offset && query.offset > 0) {
|
|
263
|
+
sql += " OFFSET ?";
|
|
264
|
+
params.push(query.offset);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
table,
|
|
268
|
+
sql,
|
|
269
|
+
params
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
let sql = `SELECT ${col} AS ${query.dimension}, ${metricSelectList(query.metrics)}${variantSel}${totalCol} FROM ${table} WHERE ${where.sql}${facet.sql} GROUP BY ${col} ORDER BY ${query.orderBy.metric} ${dir} LIMIT ?`;
|
|
273
|
+
const params = [
|
|
274
|
+
...where.params,
|
|
275
|
+
...facet.params,
|
|
276
|
+
query.limit
|
|
277
|
+
];
|
|
278
|
+
if (query.offset && query.offset > 0) {
|
|
279
|
+
sql += " OFFSET ?";
|
|
280
|
+
params.push(query.offset);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
table,
|
|
284
|
+
sql,
|
|
285
|
+
params
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
case "single-row-lookup": {
|
|
289
|
+
const table = tableForDimensions(Object.keys(query.match));
|
|
290
|
+
const where = rangePredicate(query);
|
|
291
|
+
const matchParts = [];
|
|
292
|
+
const matchParams = [];
|
|
293
|
+
for (const [dim, value] of Object.entries(query.match)) {
|
|
294
|
+
const col = dimExpr(dim, table);
|
|
295
|
+
if (!col) throw new Error(`[archetype-sql] single-row-lookup: unknown dimension ${dim}`);
|
|
296
|
+
matchParts.push(`${col} = ?`);
|
|
297
|
+
matchParams.push(value);
|
|
298
|
+
}
|
|
299
|
+
const matchSql = matchParts.length ? ` AND ${matchParts.join(" AND ")}` : "";
|
|
300
|
+
return {
|
|
301
|
+
table,
|
|
302
|
+
sql: `SELECT ${metricSelectList(query.metrics)} FROM ${table} WHERE ${where.sql}${matchSql}`,
|
|
303
|
+
params: [...where.params, ...matchParams]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
case "multi-series-stacked-daily": {
|
|
307
|
+
const table = tableForDimensions([query.seriesDimension]);
|
|
308
|
+
const where = rangePredicate(query);
|
|
309
|
+
if (query.seriesDimension === "device") return {
|
|
310
|
+
table,
|
|
311
|
+
sql: `SELECT date, device, ${query.metric} FROM (${deviceUnpivotSql([query.metric], where.sql, true)}) ORDER BY date, device`,
|
|
312
|
+
params: [
|
|
313
|
+
...where.params,
|
|
314
|
+
...where.params,
|
|
315
|
+
...where.params
|
|
316
|
+
]
|
|
317
|
+
};
|
|
318
|
+
const col = dimExpr(query.seriesDimension, table);
|
|
319
|
+
return {
|
|
320
|
+
table,
|
|
321
|
+
sql: `SELECT date, ${col} AS ${query.seriesDimension}, ${metricExpr(query.metric)} AS ${query.metric} FROM ${table} WHERE ${where.sql} GROUP BY date, ${col} ORDER BY date, ${col}`,
|
|
322
|
+
params: where.params
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
case "two-dimension-detail": {
|
|
326
|
+
const table = "page_queries";
|
|
327
|
+
const where = rangePredicate(query);
|
|
328
|
+
const facet = facetPredicate(query, table);
|
|
329
|
+
const filterParts = [];
|
|
330
|
+
const filterParams = [];
|
|
331
|
+
if (query.filter?.page) {
|
|
332
|
+
filterParts.push("url = ?");
|
|
333
|
+
filterParams.push(query.filter.page);
|
|
334
|
+
}
|
|
335
|
+
if (query.filter?.query) {
|
|
336
|
+
filterParts.push("query = ?");
|
|
337
|
+
filterParams.push(query.filter.query);
|
|
338
|
+
}
|
|
339
|
+
const filterSql = filterParts.length ? ` AND ${filterParts.join(" AND ")}` : "";
|
|
340
|
+
let sql = `SELECT url AS page, query, ${metricSelectList(query.metrics)} FROM ${table} WHERE ${where.sql}${filterSql}${facet.sql} GROUP BY url, query`;
|
|
341
|
+
const params = [
|
|
342
|
+
...where.params,
|
|
343
|
+
...filterParams,
|
|
344
|
+
...facet.params
|
|
345
|
+
];
|
|
346
|
+
if (query.orderBy) {
|
|
347
|
+
const dir = query.orderBy.dir === "asc" ? "ASC" : "DESC";
|
|
348
|
+
sql += ` ORDER BY ${query.orderBy.metric} ${dir}`;
|
|
349
|
+
}
|
|
350
|
+
if (typeof query.limit === "number") {
|
|
351
|
+
sql += " LIMIT ?";
|
|
352
|
+
params.push(query.limit);
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
table,
|
|
356
|
+
sql,
|
|
357
|
+
params
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
case "arbitrary-sql": return {
|
|
361
|
+
table: "page_queries",
|
|
362
|
+
sql: query.sql,
|
|
363
|
+
params: [...query.params ?? []]
|
|
364
|
+
};
|
|
365
|
+
case "aux-cloud-only": throw new Error("[archetype-sql] aux-cloud-only is not an Iceberg query — route to the cloud endpoint");
|
|
366
|
+
default: throw new Error(`[archetype-sql] unhandled archetype: ${query.archetype}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function tableForArchetype(query) {
|
|
370
|
+
if (query.archetype === "aux-cloud-only") return null;
|
|
371
|
+
return compileArchetypeSql(query).table;
|
|
372
|
+
}
|
|
373
|
+
export { compileArchetypeSql, tableForArchetype };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { AsyncDuckDB, AsyncDuckDBConnection } from "@duckdb/duckdb-wasm";
|
|
2
|
+
interface DuckDBWasmClient {
|
|
3
|
+
db: AsyncDuckDB;
|
|
4
|
+
conn: AsyncDuckDBConnection;
|
|
5
|
+
query: (sql: string, params?: unknown[]) => Promise<Record<string, unknown>[]>;
|
|
6
|
+
close: () => Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
declare function createClient(db: AsyncDuckDB, conn: AsyncDuckDBConnection): Promise<DuckDBWasmClient>;
|
|
9
|
+
export { DuckDBWasmClient, createClient };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { arrowToRows } from "@gscdump/engine/arrow";
|
|
2
|
+
async function createClient(db, conn) {
|
|
3
|
+
return {
|
|
4
|
+
db,
|
|
5
|
+
conn,
|
|
6
|
+
async query(sql, params = []) {
|
|
7
|
+
if (params.length === 0) return arrowToRows(await conn.query(sql));
|
|
8
|
+
const stmt = await conn.prepare(sql);
|
|
9
|
+
try {
|
|
10
|
+
return arrowToRows(await stmt.query(...params));
|
|
11
|
+
} finally {
|
|
12
|
+
await stmt.close();
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
async close() {
|
|
16
|
+
await conn.close();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export { createClient };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DuckDBWasmClient } from "./client.mjs";
|
|
2
|
+
import { DuckDBWasmQueryResultHKT } from "./session.mjs";
|
|
3
|
+
import { DrizzleConfig, entityKind } from "drizzle-orm";
|
|
4
|
+
import { PgAsyncDatabase } from "drizzle-orm/pg-core";
|
|
5
|
+
import { AnyRelations, EmptyRelations, ExtractTablesWithRelations, Schema } from "drizzle-orm/relations";
|
|
6
|
+
type SchemaRelations<TSchema extends Schema> = ExtractTablesWithRelations<Record<string, never>, TSchema>;
|
|
7
|
+
declare class DuckDBWasmDatabase<TRelations extends AnyRelations = EmptyRelations> extends PgAsyncDatabase<DuckDBWasmQueryResultHKT, TRelations> {
|
|
8
|
+
static readonly [entityKind]: string;
|
|
9
|
+
}
|
|
10
|
+
interface DuckDBWasmDrizzleDatabase<TSchema extends Schema = Record<string, never>, TRelations extends AnyRelations = SchemaRelations<TSchema>> extends DuckDBWasmDatabase<TRelations> {
|
|
11
|
+
$client: Promise<DuckDBWasmClient>;
|
|
12
|
+
}
|
|
13
|
+
declare function drizzle<TSchema extends Schema = Record<string, never>, TRelations extends AnyRelations = SchemaRelations<TSchema>>(client: Promise<DuckDBWasmClient> | DuckDBWasmClient, config?: DrizzleConfig<TSchema, TRelations>): DuckDBWasmDrizzleDatabase<TSchema, TRelations>;
|
|
14
|
+
export { DuckDBWasmDatabase, DuckDBWasmDrizzleDatabase, drizzle };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DuckDBWasmSession } from "./session.mjs";
|
|
2
|
+
import { DefaultLogger, entityKind } from "drizzle-orm";
|
|
3
|
+
import { PgAsyncDatabase, PgDialect } from "drizzle-orm/pg-core";
|
|
4
|
+
import { buildRelations } from "drizzle-orm/relations";
|
|
5
|
+
var DuckDBWasmDatabase = class extends PgAsyncDatabase {
|
|
6
|
+
static [entityKind] = "DuckDBWasmDatabase";
|
|
7
|
+
};
|
|
8
|
+
function drizzle(client, config = {}) {
|
|
9
|
+
const dialect = new PgDialect();
|
|
10
|
+
const clientPromise = Promise.resolve(client);
|
|
11
|
+
let logger;
|
|
12
|
+
if (config.logger === true) logger = new DefaultLogger();
|
|
13
|
+
else if (config.logger !== false) logger = config.logger;
|
|
14
|
+
const db = new DuckDBWasmDatabase(dialect, new DuckDBWasmSession(clientPromise, dialect, {
|
|
15
|
+
logger,
|
|
16
|
+
cache: config.cache
|
|
17
|
+
}), config.relations ?? (config.schema ? buildRelations(config.schema, {}) : {}));
|
|
18
|
+
db.$client = clientPromise;
|
|
19
|
+
return db;
|
|
20
|
+
}
|
|
21
|
+
export { DuckDBWasmDatabase, drizzle };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "./client.mjs";
|
|
2
|
+
import { Assume, entityKind } from "drizzle-orm";
|
|
3
|
+
import { PgAsyncPreparedQuery, PgAsyncSession, PgDialect, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
interface DuckDBWasmQueryResultHKT extends PgQueryResultHKT {
|
|
6
|
+
type: Assume<this['row'], Row>[];
|
|
7
|
+
}
|
|
8
|
+
export { DuckDBWasmQueryResultHKT, Row };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NoopLogger, entityKind } from "drizzle-orm";
|
|
2
|
+
import { PgAsyncPreparedQuery, PgAsyncSession } from "drizzle-orm/pg-core";
|
|
3
|
+
var DuckDBWasmSession = class extends PgAsyncSession {
|
|
4
|
+
client;
|
|
5
|
+
options;
|
|
6
|
+
static [entityKind] = "DuckDBWasmSession";
|
|
7
|
+
logger;
|
|
8
|
+
cache;
|
|
9
|
+
constructor(client, dialect, options = {}) {
|
|
10
|
+
super(dialect);
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
14
|
+
this.cache = options.cache;
|
|
15
|
+
}
|
|
16
|
+
prepareQuery(query, mode, _name, mapper, queryMetadata, cacheConfig) {
|
|
17
|
+
const client = this.client;
|
|
18
|
+
const executor = async (params = []) => {
|
|
19
|
+
const rows = await (await client).query(query.sql, params);
|
|
20
|
+
return mode === "arrays" ? rows.map((row) => Object.values(row)) : rows;
|
|
21
|
+
};
|
|
22
|
+
return new PgAsyncPreparedQuery(executor, query, mapper, mode, this.logger, this.cache, queryMetadata, cacheConfig);
|
|
23
|
+
}
|
|
24
|
+
transaction() {
|
|
25
|
+
throw new Error("Transactions are not supported by the DuckDB-WASM drizzle adapter. The analytics workload is read-only; if transactions become necessary, implement PgAsyncSession.transaction() in @gscdump/engine-duckdb-wasm.");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
export { DuckDBWasmSession };
|