@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.
@@ -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
- const metricList = q.metrics.includes(q.orderBy.metric) ? q.metrics : [...q.metrics, q.orderBy.metric];
119
- const order = `${q.orderBy.metric} ${q.orderBy.dir.toUpperCase()}`;
120
- let sql = `${DEVICE_SUFFIXES.map((suffix) => {
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} LIMIT ${Math.max(0, Math.floor(q.limit))}`;
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
- let sql = `SELECT ${col}, ${metrics}${q.includeTotal ? ", COUNT(*) OVER() AS __total" : ""} FROM ${TABLE_PLACEHOLDER} WHERE ${w.clause}${facet.sql} GROUP BY ${col} ORDER BY ${order} LIMIT ${Math.max(0, Math.floor(q.limit))}`;
137
- if (q.offset && q.offset > 0) sql += ` OFFSET ${Math.floor(q.offset)}`;
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 resolveServerTailEngine(query) {
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") throw new ServerTailRoutingError(`archetype '${query.archetype}' is cloud-only — not a server-tail query`);
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 send(sql) {
406
+ async function sendResult(sql) {
313
407
  const started = Date.now();
314
- const result = await withDeadline(config.svc.runSQL({ sql }), timeoutMs).catch((err) => {
315
- if (err instanceof DuckDbIcebergTimeoutError) throw err;
316
- throw new DuckDbIcebergError(`DUCKDB_SVC.runSQL failed: ${err.message}`);
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
- if (query.archetype === "arbitrary-sql") return runSql(query.sql, query.params ?? []);
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 query(sql) {
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 (err) {
437
- if (err instanceof R2SqlTimeoutError || err?.name === "AbortError") throw new R2SqlTimeoutError(timeoutMs);
438
- throw new R2SqlError(`R2 SQL request failed: ${err.message}`);
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
- throw new R2SqlError(`R2 SQL HTTP ${response.status}: ${text}`, response.status);
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) throw new R2SqlError(`R2 SQL query rejected: ${envelope.errors?.map((e) => e.message).join("; ") ?? "unknown R2 SQL error"}`);
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.25.14",
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.25.14",
50
- "@gscdump/engine": "0.25.14",
51
- "@gscdump/engine-sqlite": "0.25.14",
52
- "@gscdump/sdk": "0.25.14",
53
- "gscdump": "0.25.14"
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.11",
57
- "@cloudflare/workers-types": "^4.20260602.1",
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.96.0"
60
+ "wrangler": "^4.98.0"
61
61
  },
62
62
  "scripts": {
63
63
  "build": "obuild",