@gscdump/cloudflare 0.25.14 → 0.26.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/server-tail/index.d.mts +53 -2
- package/dist/server-tail/index.mjs +149 -36
- package/package.json +9 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ArchetypeQuery, ArchetypeResult, ArchetypeResultRow } from "@gscdump/sdk";
|
|
2
|
+
import { Result } from "gscdump/result";
|
|
2
3
|
import { IcebergTableName } from "@gscdump/engine/iceberg";
|
|
3
4
|
import { ServerTailDirective } from "@gscdump/contracts";
|
|
4
5
|
/** Placeholder substituted for the engine-specific table reference. */
|
|
@@ -69,6 +70,16 @@ declare class DuckDbIcebergTimeoutError extends Error {
|
|
|
69
70
|
name: string;
|
|
70
71
|
constructor(timeoutMs: number);
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* The modelled, caller-actionable failure channel for a DuckDB-over-Iceberg
|
|
75
|
+
* query. As with the R2 SQL client, callers branch on which class came back: a
|
|
76
|
+
* `DuckDbIcebergTimeoutError` is the retry-able deadline overrun, a
|
|
77
|
+
* `DuckDbIcebergError` is a hard sibling-RPC failure (or the `aux-cloud-only`
|
|
78
|
+
* routing reject). The error variant IS the existing throwable class, so the
|
|
79
|
+
* throwing wrappers preserve the identity/message tests assert
|
|
80
|
+
* (`rejects.toThrow(/OOM in sibling/)`, `rejects.toThrow(DuckDbIcebergError)`).
|
|
81
|
+
*/
|
|
82
|
+
type DuckDbIcebergQueryError = DuckDbIcebergError | DuckDbIcebergTimeoutError;
|
|
72
83
|
/** A configured DuckDB-over-Iceberg executor. */
|
|
73
84
|
interface DuckDbIcebergExecutor {
|
|
74
85
|
/** Run a raw SQL string with `{{TABLE_<name>}}` placeholders resolved. */
|
|
@@ -77,6 +88,16 @@ interface DuckDbIcebergExecutor {
|
|
|
77
88
|
runPlan: (plan: ArchetypeSqlPlan) => Promise<DuckDbIcebergResult>;
|
|
78
89
|
/** Translate + run an archetype query. Handles `arbitrary-sql` verbatim. */
|
|
79
90
|
runArchetype: (query: ArchetypeQuery) => Promise<DuckDbIcebergResult>;
|
|
91
|
+
/**
|
|
92
|
+
* Errors-as-values core for {@link DuckDbIcebergExecutor.runArchetype}:
|
|
93
|
+
* returns the modelled timeout-vs-hard-fail `DuckDbIcebergQueryError` instead
|
|
94
|
+
* of throwing, so the dispatcher can branch on retry-ability (a timeout may be
|
|
95
|
+
* worth a fallback) without `instanceof` over a `catch`. Optional so a
|
|
96
|
+
* hand-rolled executor (e.g. a host app's own service-binding executor) can
|
|
97
|
+
* implement only the throwing surface; {@link createDuckDbIcebergExecutor}
|
|
98
|
+
* always provides it.
|
|
99
|
+
*/
|
|
100
|
+
runArchetypeResult?: (query: ArchetypeQuery) => Promise<Result<DuckDbIcebergResult, DuckDbIcebergQueryError>>;
|
|
80
101
|
}
|
|
81
102
|
/**
|
|
82
103
|
* Create a DuckDB-over-Iceberg-files executor.
|
|
@@ -124,10 +145,31 @@ declare class R2SqlTimeoutError extends Error {
|
|
|
124
145
|
name: string;
|
|
125
146
|
constructor(timeoutMs: number);
|
|
126
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* The modelled, caller-actionable failure channel for an R2 SQL query. Callers
|
|
150
|
+
* branch on which class came back — a `R2SqlTimeoutError` is a transient retry
|
|
151
|
+
* candidate (the query outran the per-query deadline), while a `R2SqlError`
|
|
152
|
+
* (HTTP 4xx/5xx, a rejected envelope, a transport blow-up) is a hard failure.
|
|
153
|
+
* The error variant IS the existing throwable class, so the throwing wrapper
|
|
154
|
+
* preserves the exact identity/message tests assert (`rejects.toThrow(/HTTP 403/)`,
|
|
155
|
+
* `rejects.toBeInstanceOf(R2SqlTimeoutError)`). Defects — a programmer handing
|
|
156
|
+
* the client malformed params (`escapeSqlValue` / `inlineParams`) — are NOT
|
|
157
|
+
* modelled here; they keep throwing `R2SqlError` synchronously.
|
|
158
|
+
*/
|
|
159
|
+
type R2SqlQueryError = R2SqlError | R2SqlTimeoutError;
|
|
127
160
|
/** A configured R2 SQL client. */
|
|
128
161
|
interface R2SqlClient {
|
|
129
162
|
/** Run a raw SQL string (table reference already resolved). */
|
|
130
163
|
query: (sql: string) => Promise<R2SqlResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Errors-as-values core for {@link R2SqlClient.query}: returns the modelled
|
|
166
|
+
* timeout-vs-hard-fail `R2SqlQueryError` instead of throwing, so callers can
|
|
167
|
+
* branch on retry-ability without `instanceof` over a `catch`. Optional so a
|
|
168
|
+
* hand-rolled `R2SqlClient` (e.g. a host app's own endpoint-backed client) can
|
|
169
|
+
* implement only the throwing surface; {@link createR2SqlClient} always
|
|
170
|
+
* provides it.
|
|
171
|
+
*/
|
|
172
|
+
queryResult?: (sql: string) => Promise<Result<R2SqlResult, R2SqlQueryError>>;
|
|
131
173
|
/** Run a dialect-neutral plan: resolve `{{TABLE}}`, inline params, send. */
|
|
132
174
|
runPlan: (plan: ArchetypeSqlPlan) => Promise<R2SqlResult>;
|
|
133
175
|
/** Translate + run an archetype query end to end. */
|
|
@@ -148,10 +190,19 @@ interface ServerTailDispatcherConfig {
|
|
|
148
190
|
declare class ServerTailRoutingError extends Error {
|
|
149
191
|
name: string;
|
|
150
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Errors-as-values core for {@link resolveServerTailEngine}: returns a
|
|
195
|
+
* `ServerTailRoutingError` instead of throwing when an archetype is `cloud-only`
|
|
196
|
+
* (the one caller-actionable routing failure — the consumer must route that
|
|
197
|
+
* query through the cloud endpoints, not the server tail). Pure — no I/O.
|
|
198
|
+
*/
|
|
199
|
+
declare function resolveServerTailEngineResult(query: ArchetypeQuery): Result<ServerTailEngine, ServerTailRoutingError>;
|
|
151
200
|
/**
|
|
152
201
|
* Decide which engine answers an archetype query. Pure — no I/O. Exposed so
|
|
153
202
|
* the file-resolution endpoint can compute the `ServerTailDirective.engine`
|
|
154
|
-
* with the SAME logic the dispatcher uses at execution time.
|
|
203
|
+
* with the SAME logic the dispatcher uses at execution time. Throws
|
|
204
|
+
* `ServerTailRoutingError` for a `cloud-only` archetype; see
|
|
205
|
+
* {@link resolveServerTailEngineResult} for the errors-as-values core.
|
|
155
206
|
*/
|
|
156
207
|
declare function resolveServerTailEngine(query: ArchetypeQuery): ServerTailEngine;
|
|
157
208
|
/** A configured server-tail dispatcher. */
|
|
@@ -170,4 +221,4 @@ interface ServerTailDispatcher {
|
|
|
170
221
|
* executor and routes every `ArchetypeQuery` to one of them.
|
|
171
222
|
*/
|
|
172
223
|
declare function createServerTailDispatcher(config: ServerTailDispatcherConfig): ServerTailDispatcher;
|
|
173
|
-
export { type ArchetypeSqlPlan, DuckDbIcebergError, type DuckDbIcebergExecutor, type DuckDbIcebergExecutorConfig, type DuckDbIcebergResult, type DuckDbIcebergRow, DuckDbIcebergTimeoutError, type DuckDbSvc, type R2SqlClient, type R2SqlClientConfig, R2SqlError, type R2SqlResult, type R2SqlRow, R2SqlTimeoutError, type ServerTailDispatcher, type ServerTailDispatcherConfig, type ServerTailEngine, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createServerTailDispatcher, resolveServerTailEngine };
|
|
224
|
+
export { type ArchetypeSqlPlan, DuckDbIcebergError, type DuckDbIcebergExecutor, type DuckDbIcebergExecutorConfig, type DuckDbIcebergQueryError, type DuckDbIcebergResult, type DuckDbIcebergRow, DuckDbIcebergTimeoutError, type DuckDbSvc, type R2SqlClient, type R2SqlClientConfig, R2SqlError, type R2SqlQueryError, type R2SqlResult, type R2SqlRow, R2SqlTimeoutError, type ServerTailDispatcher, type ServerTailDispatcherConfig, type ServerTailEngine, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bindLiterals, inferTable } from "@gscdump/engine";
|
|
2
2
|
import { ARCHETYPE_EXECUTION_CLASS } from "@gscdump/sdk";
|
|
3
|
+
import { err, ok, unwrapResult } from "gscdump/result";
|
|
3
4
|
const TABLE_PLACEHOLDER = "{{TABLE}}";
|
|
4
5
|
function dimColumn(dim) {
|
|
5
6
|
if (dim === "page") return "url";
|
|
@@ -34,6 +35,44 @@ function deviceSource(suffix) {
|
|
|
34
35
|
sumPosition: `sum_position_${suffix}`
|
|
35
36
|
};
|
|
36
37
|
}
|
|
38
|
+
const STD_METRICS = [
|
|
39
|
+
"clicks",
|
|
40
|
+
"impressions",
|
|
41
|
+
"ctr",
|
|
42
|
+
"position"
|
|
43
|
+
];
|
|
44
|
+
function coalesceMetric(metric, src, alias) {
|
|
45
|
+
const ref = `${src}.${metric}`;
|
|
46
|
+
return metric === "clicks" || metric === "impressions" ? `CAST(COALESCE(${ref}, 0) AS DOUBLE) AS ${alias}` : `COALESCE(${ref}, 0) AS ${alias}`;
|
|
47
|
+
}
|
|
48
|
+
function prevAlias(metric) {
|
|
49
|
+
return `prev${metric.charAt(0).toUpperCase()}${metric.slice(1)}`;
|
|
50
|
+
}
|
|
51
|
+
function moverClause(movers) {
|
|
52
|
+
const curClicks = "COALESCE(c.clicks, 0)";
|
|
53
|
+
const prevClicks = "COALESCE(p.clicks, 0)";
|
|
54
|
+
const curImpr = "COALESCE(c.impressions, 0)";
|
|
55
|
+
const prevImpr = "COALESCE(p.impressions, 0)";
|
|
56
|
+
switch (movers) {
|
|
57
|
+
case "improving": return {
|
|
58
|
+
where: `${curClicks} > ${prevClicks}`,
|
|
59
|
+
order: `(${curClicks} - ${prevClicks}) DESC`
|
|
60
|
+
};
|
|
61
|
+
case "declining": return {
|
|
62
|
+
where: `${curClicks} < ${prevClicks}`,
|
|
63
|
+
order: `(${curClicks} - ${prevClicks}) ASC`
|
|
64
|
+
};
|
|
65
|
+
case "new": return {
|
|
66
|
+
where: `${prevImpr} = 0 AND ${curImpr} > 0`,
|
|
67
|
+
order: `${curClicks} DESC, ${curImpr} DESC`
|
|
68
|
+
};
|
|
69
|
+
case "lost": return {
|
|
70
|
+
where: `${curImpr} = 0 AND ${prevImpr} > 0`,
|
|
71
|
+
order: `${prevImpr} DESC`
|
|
72
|
+
};
|
|
73
|
+
default: throw new Error(`[archetype-sql] unknown movers mode: ${movers}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
37
76
|
function sqlStringLiteral(value) {
|
|
38
77
|
return `'${value.replace(/'/g, "''")}'`;
|
|
39
78
|
}
|
|
@@ -114,15 +153,36 @@ function buildEntityDailySparkline(q) {
|
|
|
114
153
|
function buildTopNBreakdown(q) {
|
|
115
154
|
const table = inferTable([q.dimension]);
|
|
116
155
|
const w = partitionWhere(q);
|
|
156
|
+
const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
|
|
157
|
+
const limit = `LIMIT ${Math.max(0, Math.floor(q.limit))}`;
|
|
158
|
+
const offset = q.offset && q.offset > 0 ? ` OFFSET ${Math.floor(q.offset)}` : "";
|
|
159
|
+
const metricList = q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric];
|
|
160
|
+
const variantSel = q.dimension === "queryCanonical" ? ", COUNT(DISTINCT query) AS variantCount" : "";
|
|
117
161
|
if (q.dimension === "device") {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
162
|
+
if (q.compareRange) {
|
|
163
|
+
const wPrev = partitionWhere({
|
|
164
|
+
...q,
|
|
165
|
+
range: q.compareRange
|
|
166
|
+
});
|
|
167
|
+
const deviceSelects = (clause, ml) => DEVICE_SUFFIXES.map((suffix) => {
|
|
168
|
+
const source = deviceSource(suffix);
|
|
169
|
+
const metrics = ml.map((m) => metricExprForSource(m, source)).join(", ");
|
|
170
|
+
return `SELECT '${suffix.toUpperCase()}' AS device, ${metrics} FROM ${TABLE_PLACEHOLDER} WHERE ${clause}`;
|
|
171
|
+
}).join(" UNION ALL ");
|
|
172
|
+
const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
|
|
173
|
+
const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
|
|
174
|
+
const sql = `WITH cur AS (${deviceSelects(w.clause, metricList)}), prev AS (${deviceSelects(wPrev.clause, STD_METRICS)}) 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 ${order} ${limit}${offset}`;
|
|
175
|
+
return {
|
|
176
|
+
table,
|
|
177
|
+
params: [...DEVICE_SUFFIXES.flatMap(() => w.params), ...DEVICE_SUFFIXES.flatMap(() => wPrev.params)],
|
|
178
|
+
sql
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const sql = `${DEVICE_SUFFIXES.map((suffix) => {
|
|
121
182
|
const source = deviceSource(suffix);
|
|
122
183
|
const metrics = metricList.map((m) => metricExprForSource(m, source)).join(", ");
|
|
123
184
|
return `SELECT '${suffix.toUpperCase()}' AS device, ${metrics} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}`;
|
|
124
|
-
}).join(" UNION ALL ")} ORDER BY ${order}
|
|
125
|
-
if (q.offset && q.offset > 0) sql += ` OFFSET ${Math.floor(q.offset)}`;
|
|
185
|
+
}).join(" UNION ALL ")} ORDER BY ${order} ${limit}${offset}`;
|
|
126
186
|
return {
|
|
127
187
|
table,
|
|
128
188
|
params: DEVICE_SUFFIXES.flatMap(() => w.params),
|
|
@@ -130,11 +190,34 @@ function buildTopNBreakdown(q) {
|
|
|
130
190
|
};
|
|
131
191
|
}
|
|
132
192
|
const col = dimColumn(q.dimension);
|
|
133
|
-
const metrics = (q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric]).map(metricExpr).join(", ");
|
|
134
|
-
const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
|
|
135
193
|
const facet = facetPredicate(q);
|
|
136
|
-
|
|
137
|
-
if (q.
|
|
194
|
+
const totalCol = q.includeTotal ? ", COUNT(*) OVER() AS __total" : "";
|
|
195
|
+
if (q.compareRange) {
|
|
196
|
+
const wPrev = partitionWhere({
|
|
197
|
+
...q,
|
|
198
|
+
range: q.compareRange
|
|
199
|
+
});
|
|
200
|
+
const curMetrics = metricList.map(metricExpr).join(", ");
|
|
201
|
+
const prevMetrics = STD_METRICS.map((m) => metricExpr(m)).join(", ");
|
|
202
|
+
const curCols = metricList.map((m) => coalesceMetric(m, "c", m)).join(", ");
|
|
203
|
+
const prevCols = STD_METRICS.map((m) => coalesceMetric(m, "p", prevAlias(m))).join(", ");
|
|
204
|
+
const variantOut = q.dimension === "queryCanonical" ? ", c.variantCount AS variantCount" : "";
|
|
205
|
+
const mover = q.movers ? moverClause(q.movers) : null;
|
|
206
|
+
const moverWhere = mover ? `WHERE ${mover.where} ` : "";
|
|
207
|
+
const orderSql = mover ? `ORDER BY ${mover.order}` : `ORDER BY ${order}`;
|
|
208
|
+
const sql = `WITH cur AS (SELECT ${col} AS k, ${curMetrics}${variantSel} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col}), prev AS (SELECT ${col} AS k, ${prevMetrics} FROM ${TABLE_PLACEHOLDER} WHERE ${wPrev.clause}${facet.sql} GROUP BY ${col}) SELECT COALESCE(c.k, p.k) AS ${q.dimension}, ${curCols}, ${prevCols}${variantOut}${totalCol} FROM cur c FULL OUTER JOIN prev p ON c.k = p.k ${moverWhere}${orderSql} ${limit}${offset}`;
|
|
209
|
+
return {
|
|
210
|
+
table,
|
|
211
|
+
params: [
|
|
212
|
+
...w.params,
|
|
213
|
+
...facet.params,
|
|
214
|
+
...wPrev.params,
|
|
215
|
+
...facet.params
|
|
216
|
+
],
|
|
217
|
+
sql
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const sql = `SELECT ${col}, ${metricList.map(metricExpr).join(", ")}${variantSel}${totalCol} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col} ORDER BY ${order} ${limit}${offset}`;
|
|
138
221
|
return {
|
|
139
222
|
table,
|
|
140
223
|
params: [...w.params, ...facet.params],
|
|
@@ -220,15 +303,23 @@ function buildArchetypeSql(query) {
|
|
|
220
303
|
var ServerTailRoutingError = class extends Error {
|
|
221
304
|
name = "ServerTailRoutingError";
|
|
222
305
|
};
|
|
223
|
-
function
|
|
306
|
+
function routingErrorToException(error) {
|
|
307
|
+
return error;
|
|
308
|
+
}
|
|
309
|
+
function resolveServerTailEngineResult(query) {
|
|
224
310
|
const cls = ARCHETYPE_EXECUTION_CLASS[query.archetype];
|
|
225
|
-
if (cls === "cloud-only")
|
|
226
|
-
if (cls === "duckdb") return "duckdb";
|
|
227
|
-
if (query.archetype === "top-n-breakdown" && query.offset && query.offset > 0) return "duckdb";
|
|
228
|
-
if (query.archetype === "top-n-breakdown" && query.includeTotal) return "duckdb";
|
|
311
|
+
if (cls === "cloud-only") return err(new ServerTailRoutingError(`archetype '${query.archetype}' is cloud-only — not a server-tail query`));
|
|
312
|
+
if (cls === "duckdb") return ok("duckdb");
|
|
313
|
+
if (query.archetype === "top-n-breakdown" && query.offset && query.offset > 0) return ok("duckdb");
|
|
314
|
+
if (query.archetype === "top-n-breakdown" && query.includeTotal) return ok("duckdb");
|
|
315
|
+
if (query.archetype === "top-n-breakdown" && query.compareRange) return ok("duckdb");
|
|
316
|
+
if (query.archetype === "top-n-breakdown" && query.dimension === "queryCanonical") return ok("duckdb");
|
|
229
317
|
const facets = query.facets;
|
|
230
|
-
if (facets && facets.length > 0) return "duckdb";
|
|
231
|
-
return "r2-sql";
|
|
318
|
+
if (facets && facets.length > 0) return ok("duckdb");
|
|
319
|
+
return ok("r2-sql");
|
|
320
|
+
}
|
|
321
|
+
function resolveServerTailEngine(query) {
|
|
322
|
+
return unwrapResult(resolveServerTailEngineResult(query), routingErrorToException);
|
|
232
323
|
}
|
|
233
324
|
function sourceFor(engine) {
|
|
234
325
|
return engine === "r2-sql" ? "server-r2-sql" : "server-duckdb";
|
|
@@ -293,6 +384,9 @@ var DuckDbIcebergTimeoutError = class extends Error {
|
|
|
293
384
|
super(`DuckDB-over-Iceberg query exceeded ${timeoutMs}ms deadline`);
|
|
294
385
|
}
|
|
295
386
|
};
|
|
387
|
+
function duckDbIcebergErrorToException(error) {
|
|
388
|
+
return error;
|
|
389
|
+
}
|
|
296
390
|
const DEFAULT_TIMEOUT_MS$1 = 25e3;
|
|
297
391
|
function icebergTableRef(config, table) {
|
|
298
392
|
if (config.tableRefStyle === "catalog") return `${config.namespace}.${table}`;
|
|
@@ -309,17 +403,22 @@ function resolveTablePlaceholders(sql, config) {
|
|
|
309
403
|
}
|
|
310
404
|
function createDuckDbIcebergExecutor(config) {
|
|
311
405
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS$1;
|
|
312
|
-
async function
|
|
406
|
+
async function sendResult(sql) {
|
|
313
407
|
const started = Date.now();
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return {
|
|
408
|
+
const raced = await withDeadline(config.svc.runSQL({ sql }), timeoutMs).then((value) => ok(value)).catch((error) => error instanceof DuckDbIcebergTimeoutError ? err(error) : err(new DuckDbIcebergError(`DUCKDB_SVC.runSQL failed: ${error.message}`)));
|
|
409
|
+
if (!raced.ok) return raced;
|
|
410
|
+
const result = raced.value;
|
|
411
|
+
return ok({
|
|
319
412
|
rows: result.rows ?? [],
|
|
320
413
|
sql: result.sql ?? sql,
|
|
321
414
|
queryMs: Date.now() - started
|
|
322
|
-
};
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async function send(sql) {
|
|
418
|
+
return unwrapResult(await sendResult(sql), duckDbIcebergErrorToException);
|
|
419
|
+
}
|
|
420
|
+
function runSqlResult(sql, params = []) {
|
|
421
|
+
return sendResult(bindLiterals(resolveTablePlaceholders(sql, config), params));
|
|
323
422
|
}
|
|
324
423
|
function runSql(sql, params = []) {
|
|
325
424
|
return send(bindLiterals(resolveTablePlaceholders(sql, config), params));
|
|
@@ -327,15 +426,22 @@ function createDuckDbIcebergExecutor(config) {
|
|
|
327
426
|
function runPlan(plan) {
|
|
328
427
|
return send(bindLiterals(plan.sql.split(TABLE_PLACEHOLDER).join(icebergTableRef(config, plan.table)), plan.params));
|
|
329
428
|
}
|
|
429
|
+
function runPlanResult(plan) {
|
|
430
|
+
return sendResult(bindLiterals(plan.sql.split(TABLE_PLACEHOLDER).join(icebergTableRef(config, plan.table)), plan.params));
|
|
431
|
+
}
|
|
432
|
+
function runArchetypeResult(query) {
|
|
433
|
+
if (query.archetype === "arbitrary-sql") return runSqlResult(query.sql, query.params ?? []);
|
|
434
|
+
if (query.archetype === "aux-cloud-only") return Promise.resolve(err(new DuckDbIcebergError("aux-cloud-only is not an Iceberg query")));
|
|
435
|
+
return runPlanResult(buildArchetypeSql(query));
|
|
436
|
+
}
|
|
330
437
|
async function runArchetype(query) {
|
|
331
|
-
|
|
332
|
-
if (query.archetype === "aux-cloud-only") throw new DuckDbIcebergError("aux-cloud-only is not an Iceberg query");
|
|
333
|
-
return runPlan(buildArchetypeSql(query));
|
|
438
|
+
return unwrapResult(await runArchetypeResult(query), duckDbIcebergErrorToException);
|
|
334
439
|
}
|
|
335
440
|
return {
|
|
336
441
|
runSql,
|
|
337
442
|
runPlan,
|
|
338
|
-
runArchetype
|
|
443
|
+
runArchetype,
|
|
444
|
+
runArchetypeResult
|
|
339
445
|
};
|
|
340
446
|
}
|
|
341
447
|
function r2TableRef(namespace, table) {
|
|
@@ -355,6 +461,9 @@ var R2SqlTimeoutError = class extends Error {
|
|
|
355
461
|
super(`R2 SQL query exceeded ${timeoutMs}ms deadline`);
|
|
356
462
|
}
|
|
357
463
|
};
|
|
464
|
+
function r2SqlErrorToException(error) {
|
|
465
|
+
return error;
|
|
466
|
+
}
|
|
358
467
|
const DEFAULT_API_BASE = "https://api.sql.cloudflarestorage.com/api/v1";
|
|
359
468
|
const DEFAULT_TIMEOUT_MS = 25e3;
|
|
360
469
|
const PARTITION_PREDICATE_RE = /\b(site_id|search_type)(\s*=)/g;
|
|
@@ -417,7 +526,7 @@ function createR2SqlClient(config) {
|
|
|
417
526
|
const apiBase = config.apiBase ?? DEFAULT_API_BASE;
|
|
418
527
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
419
528
|
const endpoint = `${apiBase}/accounts/${config.accountId}/r2-sql/query/${config.bucket}`;
|
|
420
|
-
async function
|
|
529
|
+
async function queryResult(sql) {
|
|
421
530
|
const started = Date.now();
|
|
422
531
|
const controller = new AbortController();
|
|
423
532
|
const timer = setTimeout(() => controller.abort(new R2SqlTimeoutError(timeoutMs)), timeoutMs);
|
|
@@ -433,23 +542,26 @@ function createR2SqlClient(config) {
|
|
|
433
542
|
body: JSON.stringify({ query: sql }),
|
|
434
543
|
signal: controller.signal
|
|
435
544
|
});
|
|
436
|
-
} catch (
|
|
437
|
-
if (
|
|
438
|
-
|
|
545
|
+
} catch (error) {
|
|
546
|
+
if (error instanceof R2SqlTimeoutError || error?.name === "AbortError") return err(new R2SqlTimeoutError(timeoutMs));
|
|
547
|
+
return err(new R2SqlError(`R2 SQL request failed: ${error.message}`));
|
|
439
548
|
} finally {
|
|
440
549
|
clearTimeout(timer);
|
|
441
550
|
}
|
|
442
551
|
if (!response.ok) {
|
|
443
552
|
const text = await response.text().catch(() => "");
|
|
444
|
-
|
|
553
|
+
return err(new R2SqlError(`R2 SQL HTTP ${response.status}: ${text}`, response.status));
|
|
445
554
|
}
|
|
446
555
|
const envelope = await response.json();
|
|
447
|
-
if (!envelope.success)
|
|
448
|
-
return {
|
|
556
|
+
if (!envelope.success) return err(new R2SqlError(`R2 SQL query rejected: ${envelope.errors?.map((e) => e.message).join("; ") ?? "unknown R2 SQL error"}`));
|
|
557
|
+
return ok({
|
|
449
558
|
rows: normalizeRows(envelope.result),
|
|
450
559
|
sql,
|
|
451
560
|
queryMs: Date.now() - started
|
|
452
|
-
};
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
async function query(sql) {
|
|
564
|
+
return unwrapResult(await queryResult(sql), r2SqlErrorToException);
|
|
453
565
|
}
|
|
454
566
|
function runPlan(plan) {
|
|
455
567
|
const tableRef = r2TableRef(config.namespace, plan.table);
|
|
@@ -460,8 +572,9 @@ function createR2SqlClient(config) {
|
|
|
460
572
|
}
|
|
461
573
|
return {
|
|
462
574
|
query,
|
|
575
|
+
queryResult,
|
|
463
576
|
runPlan,
|
|
464
577
|
runArchetype
|
|
465
578
|
};
|
|
466
579
|
}
|
|
467
|
-
export { DuckDbIcebergError, DuckDbIcebergTimeoutError, R2SqlError, R2SqlTimeoutError, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createServerTailDispatcher, resolveServerTailEngine };
|
|
580
|
+
export { DuckDbIcebergError, DuckDbIcebergTimeoutError, R2SqlError, R2SqlTimeoutError, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/cloudflare",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.1",
|
|
5
5
|
"description": "Cloudflare-Workers-flavored helpers for the gscdump analytics stack: AnalyticsEnv binding contract, R2 SigV4 presigner, size-hint HMAC, DuckDB Workers shims, engine factory.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -46,18 +46,18 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@uwdata/flechette": "^2.5.0",
|
|
48
48
|
"aws4fetch": "^1.0.20",
|
|
49
|
-
"@gscdump/contracts": "0.
|
|
50
|
-
"@gscdump/engine": "0.
|
|
51
|
-
"@gscdump/engine-sqlite": "0.
|
|
52
|
-
"@gscdump/sdk": "0.
|
|
53
|
-
"gscdump": "0.
|
|
49
|
+
"@gscdump/contracts": "0.26.1",
|
|
50
|
+
"@gscdump/engine": "0.26.1",
|
|
51
|
+
"@gscdump/engine-sqlite": "0.26.1",
|
|
52
|
+
"@gscdump/sdk": "0.26.1",
|
|
53
|
+
"gscdump": "0.26.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@cloudflare/vitest-pool-workers": "^0.16.
|
|
57
|
-
"@cloudflare/workers-types": "^4.
|
|
56
|
+
"@cloudflare/vitest-pool-workers": "^0.16.13",
|
|
57
|
+
"@cloudflare/workers-types": "^4.20260608.1",
|
|
58
58
|
"h3": "^1.15.11",
|
|
59
59
|
"typescript": "^6.0.3",
|
|
60
|
-
"wrangler": "^4.
|
|
60
|
+
"wrangler": "^4.98.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build": "obuild",
|