@gscdump/cloudflare 1.5.0 → 1.5.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,5 +1,6 @@
1
1
  import { ArchetypeSqlPlan, BuildArchetypeSqlOptions, TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
2
2
  import { DuckDbIcebergError, DuckDbIcebergExecutor, DuckDbIcebergExecutorConfig, DuckDbIcebergQueryError, DuckDbIcebergResult, DuckDbIcebergRow, DuckDbIcebergTimeoutError, DuckDbSvc, createDuckDbIcebergExecutor } from "./duckdb-iceberg-executor.mjs";
3
- import { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient } from "./r2-sql-client.mjs";
3
+ import { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams } from "./r2-sql-client.mjs";
4
4
  import { ServerTailDispatcher, ServerTailDispatcherConfig, ServerTailEngine, ServerTailRoutingError, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult } from "./dispatcher.mjs";
5
- export { type ArchetypeSqlPlan, type BuildArchetypeSqlOptions, 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 };
5
+ import { R2SqlTransport, R2SqlTransportConfig, R2SqlTransportMetrics, R2SqlTransportResult, R2SqlTransportRow, createR2SqlTransport } from "./r2-sql-transport.mjs";
6
+ export { type ArchetypeSqlPlan, type BuildArchetypeSqlOptions, 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 R2SqlTransport, type R2SqlTransportConfig, type R2SqlTransportMetrics, type R2SqlTransportResult, type R2SqlTransportRow, type ServerTailDispatcher, type ServerTailDispatcherConfig, type ServerTailEngine, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createR2SqlTransport, createServerTailDispatcher, escapeSqlValue, inlineParams, resolveServerTailEngine, resolveServerTailEngineResult };
@@ -1,5 +1,6 @@
1
1
  import { TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
2
2
  import { ServerTailRoutingError, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult } from "./dispatcher.mjs";
3
3
  import { DuckDbIcebergError, DuckDbIcebergTimeoutError, createDuckDbIcebergExecutor } from "./duckdb-iceberg-executor.mjs";
4
- import { R2SqlError, R2SqlTimeoutError, createR2SqlClient } from "./r2-sql-client.mjs";
5
- export { DuckDbIcebergError, DuckDbIcebergTimeoutError, R2SqlError, R2SqlTimeoutError, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createServerTailDispatcher, resolveServerTailEngine, resolveServerTailEngineResult };
4
+ import { createR2SqlTransport } from "./r2-sql-transport.mjs";
5
+ import { R2SqlError, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams } from "./r2-sql-client.mjs";
6
+ export { DuckDbIcebergError, DuckDbIcebergTimeoutError, R2SqlError, R2SqlTimeoutError, ServerTailRoutingError, TABLE_PLACEHOLDER, buildArchetypeSql, createDuckDbIcebergExecutor, createR2SqlClient, createR2SqlTransport, createServerTailDispatcher, escapeSqlValue, inlineParams, resolveServerTailEngine, resolveServerTailEngineResult };
@@ -63,6 +63,18 @@ declare class R2SqlTimeoutError extends Error {
63
63
  * modelled here; they keep throwing `R2SqlError` synchronously.
64
64
  */
65
65
  type R2SqlQueryError = R2SqlError | R2SqlTimeoutError;
66
+ /**
67
+ * Escape a JS value for inline embedding in R2 SQL. R2 SQL has no bound-param
68
+ * channel, so `buildArchetypeSql`'s `?` placeholders are substituted here.
69
+ * Numbers go in bare; strings are single-quote-escaped; null → `NULL`.
70
+ */
71
+ declare function escapeSqlValue(value: unknown): string;
72
+ /**
73
+ * Inline a plan's `?`-bound params into its SQL, in order. R2 SQL accepts only
74
+ * a literal query string. Quote-aware so a `?` inside a string literal is not
75
+ * mistaken for a placeholder.
76
+ */
77
+ declare function inlineParams(sql: string, params: readonly unknown[]): string;
66
78
  /** A configured R2 SQL client. */
67
79
  interface R2SqlClient {
68
80
  /** Run a raw SQL string (table reference already resolved). */
@@ -86,4 +98,4 @@ interface R2SqlClient {
86
98
  * production; tests inject `fetchImpl` returning a recorded envelope.
87
99
  */
88
100
  declare function createR2SqlClient(config: R2SqlClientConfig): R2SqlClient;
89
- export { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient };
101
+ export { R2SqlClient, R2SqlClientConfig, R2SqlError, R2SqlQueryError, R2SqlResult, R2SqlRow, R2SqlTimeoutError, createR2SqlClient, escapeSqlValue, inlineParams };
@@ -1,4 +1,5 @@
1
1
  import { TABLE_PLACEHOLDER, buildArchetypeSql } from "./archetype-sql.mjs";
2
+ import { createR2SqlTransport } from "./r2-sql-transport.mjs";
2
3
  import { err, ok, unwrapResult } from "gscdump/result";
3
4
  import { SEARCH_TYPE_INT } from "@gscdump/engine/iceberg";
4
5
  function r2TableRef(namespace, table) {
@@ -21,8 +22,6 @@ var R2SqlTimeoutError = class extends Error {
21
22
  function r2SqlErrorToException(error) {
22
23
  return error;
23
24
  }
24
- const DEFAULT_API_BASE = "https://api.sql.cloudflarestorage.com/api/v1";
25
- const DEFAULT_TIMEOUT_MS = 25e3;
26
25
  const PARTITION_PREDICATE_RE = /\b(site_id|search_type)(\s*=)/g;
27
26
  function workaroundPartitionEquality(sql) {
28
27
  return sql.replace(PARTITION_PREDICATE_RE, (_m, col, eq) => `CONCAT(${col}, '')${eq}`);
@@ -63,21 +62,6 @@ function inlineParams(sql, params) {
63
62
  if (paramIndex !== params.length) throw new R2SqlError(`SQL has ${paramIndex} ? placeholders but ${params.length} params supplied`);
64
63
  return out;
65
64
  }
66
- function normalizeRows(result) {
67
- if (!result) return [];
68
- if (Array.isArray(result.rows)) return result.rows;
69
- if (Array.isArray(result.columns) && Array.isArray(result.data)) {
70
- const cols = result.columns;
71
- return result.data.map((tuple) => {
72
- const row = {};
73
- cols.forEach((col, idx) => {
74
- row[col] = tuple[idx] ?? null;
75
- });
76
- return row;
77
- });
78
- }
79
- return [];
80
- }
81
65
  function coerceIntSiteId(siteId, mapper) {
82
66
  const mapped = mapper ? mapper(siteId) : siteId;
83
67
  const n = Number(mapped);
@@ -96,43 +80,24 @@ function withEncodedPartitions(query, encoding, siteIdMapper) {
96
80
  };
97
81
  }
98
82
  function createR2SqlClient(config) {
99
- const fetchImpl = config.fetchImpl ?? globalThis.fetch;
100
- const apiBase = config.apiBase ?? DEFAULT_API_BASE;
101
- const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
83
+ const timeoutMs = config.timeoutMs ?? 25e3;
102
84
  const partitionKeyEncoding = config.partitionKeyEncoding ?? "int";
103
- const endpoint = `${apiBase}/accounts/${config.accountId}/r2-sql/query/${config.bucket}`;
85
+ const transport = createR2SqlTransport({
86
+ accountId: config.accountId,
87
+ bucket: config.bucket,
88
+ token: config.token,
89
+ apiBase: config.apiBase,
90
+ fetchImpl: config.fetchImpl,
91
+ timeoutMs
92
+ });
104
93
  async function queryResult(sql) {
105
- const started = Date.now();
106
- const controller = new AbortController();
107
- const timer = setTimeout(() => controller.abort(new R2SqlTimeoutError(timeoutMs)), timeoutMs);
108
- let response;
109
- try {
110
- response = await fetchImpl(endpoint, {
111
- method: "POST",
112
- headers: {
113
- "authorization": `Bearer ${config.token}`,
114
- "content-type": "application/json",
115
- "user-agent": "gscdump-cloudflare-r2sql/1.0"
116
- },
117
- body: JSON.stringify({ query: sql }),
118
- signal: controller.signal
119
- });
120
- } catch (error) {
121
- if (error instanceof R2SqlTimeoutError || error?.name === "AbortError") return err(new R2SqlTimeoutError(timeoutMs));
122
- return err(new R2SqlError(`R2 SQL request failed: ${error.message}`));
123
- } finally {
124
- clearTimeout(timer);
125
- }
126
- if (!response.ok) {
127
- const text = await response.text().catch(() => "");
128
- return err(new R2SqlError(`R2 SQL HTTP ${response.status}: ${text}`, response.status));
129
- }
130
- const envelope = await response.json();
131
- if (!envelope.success) return err(new R2SqlError(`R2 SQL query rejected: ${envelope.errors?.map((e) => e.message).join("; ") ?? "unknown R2 SQL error"}`));
94
+ const result = await transport.query(sql);
95
+ if (result._tag === "timeout") return err(new R2SqlTimeoutError(result.timeoutMs));
96
+ if (result._tag === "error") return err(new R2SqlError(result.message, result.status));
132
97
  return ok({
133
- rows: normalizeRows(envelope.result),
98
+ rows: result.rows,
134
99
  sql,
135
- queryMs: Date.now() - started
100
+ queryMs: result.queryMs
136
101
  });
137
102
  }
138
103
  async function query(sql) {
@@ -0,0 +1,35 @@
1
+ type R2SqlTransportRow = Record<string, unknown>;
2
+ type R2SqlTransportMetrics = Record<string, number | undefined>;
3
+ type R2SqlTransportResult = {
4
+ _tag: 'ok';
5
+ rows: R2SqlTransportRow[];
6
+ metrics: R2SqlTransportMetrics | null;
7
+ sql: string;
8
+ queryMs: number;
9
+ } | {
10
+ _tag: 'timeout';
11
+ timeoutMs: number;
12
+ } | {
13
+ _tag: 'error';
14
+ kind: 'network' | 'http' | 'rejected' | 'invalid_response';
15
+ message: string;
16
+ status?: number;
17
+ body?: string;
18
+ retryAfterMs?: number;
19
+ };
20
+ interface R2SqlTransportConfig {
21
+ accountId: string;
22
+ bucket: string;
23
+ token: string;
24
+ apiBase?: string;
25
+ fetchImpl?: typeof fetch;
26
+ timeoutMs?: number;
27
+ userAgent?: string;
28
+ now?: () => number;
29
+ }
30
+ interface R2SqlTransport {
31
+ endpoint: string;
32
+ query: (sql: string) => Promise<R2SqlTransportResult>;
33
+ }
34
+ declare function createR2SqlTransport(config: R2SqlTransportConfig): R2SqlTransport;
35
+ export { R2SqlTransport, R2SqlTransportConfig, R2SqlTransportMetrics, R2SqlTransportResult, R2SqlTransportRow, createR2SqlTransport };
@@ -0,0 +1,132 @@
1
+ const DEFAULT_API_BASE = "https://api.sql.cloudflarestorage.com/api/v1";
2
+ const DEFAULT_TIMEOUT_MS = 25e3;
3
+ const MAX_RETRY_AFTER_MS = 6e4;
4
+ function errorMessage(error) {
5
+ return error instanceof Error ? error.message : String(error);
6
+ }
7
+ function parseEnvelope(value) {
8
+ return value && typeof value === "object" ? value : null;
9
+ }
10
+ function normalizeRows(result) {
11
+ if (Array.isArray(result?.rows)) return result.rows;
12
+ if (!Array.isArray(result?.columns) || !Array.isArray(result.data)) return [];
13
+ return result.data.map((tuple) => {
14
+ const row = {};
15
+ for (let index = 0; index < result.columns.length; index++) row[result.columns[index]] = tuple[index] ?? null;
16
+ return row;
17
+ });
18
+ }
19
+ function retryAfterMs(response, now) {
20
+ const raw = response.headers?.get?.("retry-after");
21
+ if (!raw) return void 0;
22
+ const seconds = Number(raw.trim());
23
+ const value = Number.isFinite(seconds) ? seconds * 1e3 : Date.parse(raw) - now();
24
+ if (!Number.isFinite(value) || value <= 0) return void 0;
25
+ return Math.min(value, MAX_RETRY_AFTER_MS);
26
+ }
27
+ function createR2SqlTransport(config) {
28
+ const fetchImpl = config.fetchImpl ?? globalThis.fetch;
29
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
30
+ const now = config.now ?? Date.now;
31
+ const endpoint = `${(config.apiBase ?? DEFAULT_API_BASE).replace(/\/+$/, "")}/accounts/${config.accountId}/r2-sql/query/${config.bucket}`;
32
+ async function query(sql) {
33
+ const startedAt = now();
34
+ const controller = new AbortController();
35
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
36
+ try {
37
+ const responseResult = await fetchImpl(endpoint, {
38
+ method: "POST",
39
+ headers: {
40
+ "authorization": `Bearer ${config.token}`,
41
+ "content-type": "application/json",
42
+ "user-agent": config.userAgent ?? "gscdump-cloudflare-r2sql/1.0"
43
+ },
44
+ body: JSON.stringify({ query: sql }),
45
+ signal: controller.signal
46
+ }).then((response) => ({
47
+ _tag: "ok",
48
+ response
49
+ }), (error) => ({
50
+ _tag: "error",
51
+ error
52
+ }));
53
+ if (responseResult._tag === "error") {
54
+ if (controller.signal.aborted || responseResult.error?.name === "AbortError") return {
55
+ _tag: "timeout",
56
+ timeoutMs
57
+ };
58
+ return {
59
+ _tag: "error",
60
+ kind: "network",
61
+ message: `R2 SQL request failed: ${errorMessage(responseResult.error)}`
62
+ };
63
+ }
64
+ const { response } = responseResult;
65
+ if (!response.ok) {
66
+ const bodyResult = await response.text().then((body) => ({
67
+ _tag: "ok",
68
+ body
69
+ }), (error) => ({
70
+ _tag: "error",
71
+ error
72
+ }));
73
+ if (bodyResult._tag === "error") return {
74
+ _tag: "error",
75
+ kind: "network",
76
+ message: `R2 SQL response read failed: ${errorMessage(bodyResult.error)}`,
77
+ status: response.status
78
+ };
79
+ const retryAfter = retryAfterMs(response, now);
80
+ return {
81
+ _tag: "error",
82
+ kind: "http",
83
+ message: `R2 SQL HTTP ${response.status}: ${bodyResult.body}`,
84
+ status: response.status,
85
+ body: bodyResult.body,
86
+ ...retryAfter !== void 0 ? { retryAfterMs: retryAfter } : {}
87
+ };
88
+ }
89
+ const envelopeResult = await response.json().then((value) => ({
90
+ _tag: "ok",
91
+ value
92
+ }), (error) => ({
93
+ _tag: "error",
94
+ error
95
+ }));
96
+ if (envelopeResult._tag === "error") return {
97
+ _tag: "error",
98
+ kind: "invalid_response",
99
+ message: `R2 SQL returned invalid JSON: ${errorMessage(envelopeResult.error)}`,
100
+ status: response.status
101
+ };
102
+ const envelope = parseEnvelope(envelopeResult.value);
103
+ if (!envelope) return {
104
+ _tag: "error",
105
+ kind: "invalid_response",
106
+ message: "R2 SQL returned an invalid response",
107
+ status: response.status
108
+ };
109
+ if (!envelope.success) return {
110
+ _tag: "error",
111
+ kind: "rejected",
112
+ message: `R2 SQL query rejected: ${envelope.errors?.map((error) => error.message).filter(Boolean).join("; ") || "unknown R2 SQL error"}`,
113
+ status: response.status,
114
+ body: JSON.stringify(envelope)
115
+ };
116
+ return {
117
+ _tag: "ok",
118
+ rows: normalizeRows(envelope.result),
119
+ metrics: envelope.result?.metrics ?? null,
120
+ sql,
121
+ queryMs: now() - startedAt
122
+ };
123
+ } finally {
124
+ clearTimeout(timer);
125
+ }
126
+ }
127
+ return {
128
+ endpoint,
129
+ query
130
+ };
131
+ }
132
+ export { createR2SqlTransport };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cloudflare",
3
3
  "type": "module",
4
- "version": "1.5.0",
4
+ "version": "1.5.1",
5
5
  "description": "Cloudflare-Workers-flavored helpers for the gscdump analytics stack: AnalyticsEnv binding contract, R2 SigV4 presigner, DuckDB Workers shims, engine factory.",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -47,10 +47,10 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@uwdata/flechette": "^2.5.0",
50
- "@gscdump/contracts": "^1.5.0",
51
- "@gscdump/engine": "^1.5.0",
52
- "@gscdump/engine-sqlite": "^1.5.0",
53
- "gscdump": "^1.5.0"
50
+ "@gscdump/contracts": "^1.5.1",
51
+ "@gscdump/engine": "^1.5.1",
52
+ "gscdump": "^1.5.1",
53
+ "@gscdump/engine-sqlite": "^1.5.1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@cloudflare/vitest-pool-workers": "^0.18.8",