@lpdjs/firestore-repo-service 2.3.7 → 2.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.
@@ -131,8 +131,7 @@ interface SqlAdapter {
131
131
  * adapters caching schema-derived state — e.g. Storage Write proto
132
132
  * descriptors / writer connections — can invalidate their cache.
133
133
  *
134
- * Optional: adapters with no cache (legacy `BigQueryAdapter`) do not need
135
- * to implement this.
134
+ * Optional: adapters with no cache do not need to implement this.
136
135
  */
137
136
  onSchemaChange?(tableName: string): void | Promise<void>;
138
137
  /**
@@ -131,8 +131,7 @@ interface SqlAdapter {
131
131
  * adapters caching schema-derived state — e.g. Storage Write proto
132
132
  * descriptors / writer connections — can invalidate their cache.
133
133
  *
134
- * Optional: adapters with no cache (legacy `BigQueryAdapter`) do not need
135
- * to implement this.
134
+ * Optional: adapters with no cache do not need to implement this.
136
135
  */
137
136
  onSchemaChange?(tableName: string): void | Promise<void>;
138
137
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpdjs/firestore-repo-service",
3
- "version": "2.3.7",
3
+ "version": "2.4.1",
4
4
  "workspaces": [
5
5
  "test/functions"
6
6
  ],
@@ -40,16 +40,6 @@
40
40
  "default": "./dist/sync/bigquery.js"
41
41
  }
42
42
  },
43
- "./sync/bigquery-storage": {
44
- "require": {
45
- "types": "./dist/sync/bigquery-storage.d.cts",
46
- "default": "./dist/sync/bigquery-storage.cjs"
47
- },
48
- "import": {
49
- "types": "./dist/sync/bigquery-storage.d.ts",
50
- "default": "./dist/sync/bigquery-storage.js"
51
- }
52
- },
53
43
  "./history": {
54
44
  "require": {
55
45
  "types": "./dist/history/index.d.cts",
@@ -134,9 +124,6 @@
134
124
  "sync/bigquery": [
135
125
  "./dist/sync/bigquery.d.ts"
136
126
  ],
137
- "sync/bigquery-storage": [
138
- "./dist/sync/bigquery-storage.d.ts"
139
- ],
140
127
  "history": [
141
128
  "./dist/history/index.d.ts"
142
129
  ]
@@ -1,8 +0,0 @@
1
- 'use strict';var A=(a=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):a)(function(a){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var d="__sync_version";function m(a){let t=a.toUpperCase();switch(t){case "INTEGER":return "INT64";case "FLOAT":return "FLOAT64";case "BOOLEAN":return "BOOL";default:return t}}var S=class{constructor(){this.name="bigquery";}mapType(t){switch(t){case "string":return "STRING";case "number":return "FLOAT64";case "bigint":return "INT64";case "boolean":return "BOOL";case "timestamp":return "TIMESTAMP";case "json":return "JSON";case "text":return "STRING"}}quoteIdentifier(t){return `\`${t}\``}},w=new S;var f=null;function P(){return f||(f=A("@google-cloud/bigquery-storage"),f)}function N(a){let t;return typeof a=="bigint"?t=a:typeof a=="number"||typeof a=="string"?t=BigInt(a):t=0n,t<0n&&(t=0n),t.toString(16).padStart(16,"0")}function $(a){if(!a||typeof a!="object")return false;let t=a;return t.code===4||t.code===10||t.code===13||t.code===14}async function O(a,t=6,r=200){let n=0;for(;;)try{return await a()}catch(e){if(n++,!$(e)||n>t)throw e;let s=r*Math.pow(2,n),i=Math.random()*s;await new Promise(c=>setTimeout(c,i));}}var u=class u{constructor(t){this.writers=new Map;this.bigquery=t.bigquery,this.projectId=t.projectId,this.datasetId=t.datasetId,this.maxStaleness=t.maxStaleness===void 0?"INTERVAL 15 MINUTE":t.maxStaleness,this.writerClient=t.writerClient;}get dialect(){return w}async tableExists(t){let[r]=await this.dataset.table(t).exists();return r}async getTableColumns(t){let[r]=await this.dataset.table(t).getMetadata();return (r.schema?.fields??[]).map(e=>e.name)}async getTableColumnsWithTypes(t){let[r]=await this.dataset.table(t).getMetadata(),n=r.schema?.fields??[],e=new Map;for(let s of n)e.set(s.name,m(s.type));return e}async createTable(t){let r=l=>this.dialect.quoteIdentifier(l),n=t.columns.find(l=>l.isPrimaryKey)?.name;if(!n)throw new Error(`BigQueryStorageAdapter requires a primary key on table \`${t.tableName}\` (Storage Write CDC mode needs PRIMARY KEY NOT ENFORCED).`);let e=t.columns.map(l=>{let g=l.isPrimaryKey?" NOT NULL":"";return ` ${r(l.name)} ${l.sqlType}${g}`}).join(`,
2
- `),s=[];this.maxStaleness!==null&&s.push(`max_staleness = ${this.maxStaleness}`);let i=s.length>0?`
3
- OPTIONS(${s.join(", ")})`:"",c=`CREATE TABLE IF NOT EXISTS ${this.fqn(t.tableName)} (
4
- ${e},
5
- PRIMARY KEY (${r(n)}) NOT ENFORCED
6
- )
7
- CLUSTER BY ${r(n)}${i};`;await this.bigquery.query({query:c});}async addColumns(t,r){let n=e=>this.dialect.quoteIdentifier(e);for(let e of r){let s=`ALTER TABLE ${this.fqn(t)} ADD COLUMN ${n(e.name)} ${e.sqlType};`;await this.bigquery.query({query:s});}}async executeRaw(t){await this.bigquery.query({query:t});}onSchemaChange(t){let r=this.writers.get(t);if(r){try{r.writer.close();}catch{}this.writers.delete(t);}}async insertRows(t,r){if(r.length===0)return;let n=await this.getOrCreateWriter(t),e=r.map(s=>({...this.normalizeRow(s),_CHANGE_TYPE:"UPSERT",_CHANGE_SEQUENCE_NUMBER:N(s[d])}));await this.appendWithRetry(t,n,e);}async upsertRows(t,r,n){if(r.length===0)return;let e=await this.getOrCreateWriter(t,n),s=r.map(i=>({...this.normalizeRow(i),_CHANGE_TYPE:"UPSERT",_CHANGE_SEQUENCE_NUMBER:N(i[d])}));await this.appendWithRetry(t,e,s);}async deleteRows(t,r,n){if(n.length===0)return;let e=await this.getOrCreateWriter(t,r),s=n.map(i=>({[r]:i,_CHANGE_TYPE:"DELETE",_CHANGE_SEQUENCE_NUMBER:"ffffffffffffffff"}));await this.appendWithRetry(t,e,s);}get dataset(){return this.bigquery.dataset(this.datasetId)}fqn(t){return `\`${this.datasetId}.${t}\``}static toEpochMicros(t){let r=t.getTime();return (BigInt(r)*1000n).toString()}normalizeRow(t){let r={};for(let[n,e]of Object.entries(t))if(e!==void 0)if(e instanceof Date)r[n]=u.toEpochMicros(e);else if(typeof e=="string"&&u.ISO_TIMESTAMP_RE.test(e)){let s=new Date(e);Number.isNaN(s.getTime())?r[n]=e:r[n]=u.toEpochMicros(s);}else typeof e=="object"&&e!==null?r[n]=JSON.stringify(e):typeof e=="bigint"?r[n]=e.toString():r[n]=e;return r}async appendWithRetry(t,r,n){await O(async()=>{try{await r.appendRows(n).getResult();}catch(e){throw this.onSchemaChange(t),e}});}async getOrCreateWriter(t,r){let n=this.writers.get(t);if(n)if(r&&n.primaryKey!==r)this.onSchemaChange(t);else return n.writer;let e=P();this.writerClient||(this.writerClient=new e.managedwriter.WriterClient({projectId:this.projectId}));let[s]=await this.dataset.table(t).getMetadata(),i={fields:s.schema?.fields??[]};r||(r=s.tableConstraints?.primaryKey?.columns?.[0]??i.fields[0]?.name);let c=e.adapt.convertBigQuerySchemaToStorageTableSchema(i),l=e.adapt.convertStorageSchemaToProto2Descriptor(c,"Row",e.adapt.withChangeType(),e.adapt.withChangeSequenceNumber()),g=`projects/${this.projectId}/datasets/${this.datasetId}/tables/${t}`,h=await this.writerClient.createStreamConnection({streamId:e.managedwriter.DefaultStream,destinationTable:g}),y=new e.managedwriter.JSONWriter({connection:h,protoDescriptor:l});return this.writers.set(t,{writer:y,primaryKey:r??""}),y}async close(){for(let{writer:t}of this.writers.values())try{t.close();}catch{}if(this.writers.clear(),this.writerClient&&typeof this.writerClient.close=="function")try{this.writerClient.close();}catch{}}};u.ISO_TIMESTAMP_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;var I=u;exports.BigQueryStorageAdapter=I;//# sourceMappingURL=bigquery-storage.cjs.map
8
- //# sourceMappingURL=bigquery-storage.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/sync/constants.ts","../../src/sync/adapters/bigquery-types.ts","../../src/sync/adapters/bigquery.ts","../../src/sync/adapters/bigquery-storage.ts"],"names":["SYNC_VERSION_COLUMN","normalizeBigQueryType","type","upper","BigQueryDialect","logical","id","bigqueryDialect","storageNsCache","loadStorageNs","formatChangeSequenceNumber","version","n","isRetryableStorageError","err","e","withRetry","fn","maxRetries","baseMs","attempt","cap","delay","res","_BigQueryStorageAdapter","options","tableName","exists","metadata","f","fields","result","table","qi","pk","c","cols","notNull","opts","optionsClause","ddl","columns","stmt","sql","cached","rows","writer","cdc","row","primaryKey","ids","d","ms","out","k","v","ns","bqSchema","storageSchema","protoDescriptor","destinationTable","connection","BigQueryStorageAdapter"],"mappings":"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","file":"bigquery-storage.cjs","sourcesContent":["/**\n * Internal constants shared between the worker, queue, schema mapper and\n * SQL adapters.\n */\n\n/**\n * Name of the SQL column that stores the publish-time `version` of each\n * sync event. Used by the worker to discard out-of-order PubSub deliveries\n * (the MERGE only updates rows when the incoming version is strictly\n * greater than the stored one).\n *\n * Two underscores prefix avoids collisions with user-defined fields.\n */\nexport const SYNC_VERSION_COLUMN = \"__sync_version\";\n","/**\n * BigQuery type-name utilities shared by the legacy `BigQueryAdapter` (DML\n * MERGE) and the new `BigQueryStorageAdapter` (Storage Write API).\n *\n * - {@link normalizeBigQueryType} canonicalises BigQuery type strings so that\n * `INTEGER`/`INT64`, `FLOAT`/`FLOAT64`, `BOOLEAN`/`BOOL`, etc. compare\n * equal during schema-drift detection.\n * - {@link isBigQueryTypeCompatible} returns whether a desired type can\n * safely keep an existing column of another type — only widenings that\n * BigQuery accepts via `ALTER COLUMN SET DATA TYPE` are allowed.\n */\n\n/**\n * Canonicalise a BigQuery type name returned by `getMetadata().schema`\n * (which may use legacy aliases like `INTEGER` or `FLOAT`) so that it can\n * be compared against the type produced by `BigQueryDialect.mapType()`.\n */\nexport function normalizeBigQueryType(type: string): string {\n const upper = type.toUpperCase();\n switch (upper) {\n case \"INTEGER\":\n return \"INT64\";\n case \"FLOAT\":\n return \"FLOAT64\";\n case \"BOOLEAN\":\n return \"BOOL\";\n default:\n return upper;\n }\n}\n\n/**\n * Whether `desired` is the same as, or a safe widening of, `existing`.\n * The widenings mirror what BigQuery allows via\n * `ALTER COLUMN x SET DATA TYPE …` — see\n * https://cloud.google.com/bigquery/docs/managing-table-schemas#change_column_types\n */\nexport function isBigQueryTypeCompatible(\n existing: string,\n desired: string,\n): boolean {\n const a = normalizeBigQueryType(existing);\n const b = normalizeBigQueryType(desired);\n if (a === b) return true;\n\n const widenings: Record<string, string[]> = {\n INT64: [\"NUMERIC\", \"BIGNUMERIC\", \"FLOAT64\"],\n NUMERIC: [\"BIGNUMERIC\", \"FLOAT64\"],\n DATE: [\"DATETIME\", \"TIMESTAMP\"],\n DATETIME: [\"TIMESTAMP\"],\n };\n return widenings[a]?.includes(b) ?? false;\n}\n","import { SYNC_VERSION_COLUMN } from \"../constants\";\nimport type {\n LogicalType,\n SqlAdapter,\n SqlColumn,\n SqlDialect,\n SqlTableDef,\n} from \"../types\";\nimport { normalizeBigQueryType } from \"./bigquery-types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true when the BigQuery error is a \"concurrent update\" serialization\n * conflict (code 400, reason \"invalidQuery\" containing \"serialize access\").\n * These are safe to retry after a brief back-off.\n */\nfunction isConcurrentUpdateError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as Record<string, unknown>;\n if (e[\"code\"] !== 400) return false;\n const errors = Array.isArray(e[\"errors\"]) ? e[\"errors\"] : [];\n return errors.some(\n (x: any) =>\n typeof x?.message === \"string\" &&\n x.message.toLowerCase().includes(\"serialize access\"),\n );\n}\n\n/**\n * Execute `fn`, retrying up to `maxRetries` times when BigQuery returns a\n * concurrent-update error. Uses full-jitter exponential back-off.\n */\nasync function withRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 10,\n baseMs = 500,\n): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n attempt++;\n if (!isConcurrentUpdateError(err) || attempt > maxRetries) throw err;\n const cap = baseMs * Math.pow(2, attempt);\n const delay = Math.random() * cap; // full jitter\n await new Promise((res) => setTimeout(res, delay));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dialect (internal — used only by BigQueryAdapter)\n// ---------------------------------------------------------------------------\n\n/** BigQuery SQL dialect mapping. */\nclass BigQueryDialect implements SqlDialect {\n readonly name = \"bigquery\";\n\n mapType(logical: LogicalType): string {\n switch (logical) {\n case \"string\":\n return \"STRING\";\n case \"number\":\n return \"FLOAT64\";\n case \"bigint\":\n return \"INT64\";\n case \"boolean\":\n return \"BOOL\";\n case \"timestamp\":\n return \"TIMESTAMP\";\n case \"json\":\n return \"JSON\";\n case \"text\":\n return \"STRING\";\n }\n }\n\n quoteIdentifier(id: string): string {\n return `\\`${id}\\``;\n }\n}\n\n/** Shared BigQuery dialect singleton. */\nexport const bigqueryDialect: SqlDialect = new BigQueryDialect();\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\n/**\n * BigQuery implementation of {@link SqlAdapter}.\n *\n * Accepts an already-configured BigQuery client so the library does not pull\n * in `@google-cloud/bigquery` as a hard dependency.\n *\n * @example\n * ```ts\n * import { BigQuery } from \"@google-cloud/bigquery\";\n * import { BigQueryAdapter } from \"./adapters/bigquery\";\n *\n * const adapter = new BigQueryAdapter({\n * bigquery: new BigQuery({ projectId: \"my-project\" }),\n * datasetId: \"my_dataset\",\n * });\n * ```\n */\nexport class BigQueryAdapter implements SqlAdapter {\n private readonly bigquery: any;\n private readonly datasetId: string;\n\n constructor(options: { bigquery: any; datasetId: string }) {\n this.bigquery = options.bigquery;\n this.datasetId = options.datasetId;\n }\n\n /** The BigQuery SQL dialect. */\n get dialect(): SqlDialect {\n return bigqueryDialect;\n }\n\n /** Check whether a table exists in the dataset. */\n async tableExists(tableName: string): Promise<boolean> {\n const [exists] = await this.dataset.table(tableName).exists();\n return exists;\n }\n\n /** Return the column names currently present in the table. */\n async getTableColumns(tableName: string): Promise<string[]> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string }> = metadata.schema?.fields ?? [];\n return fields.map((f) => f.name);\n }\n\n /**\n * Return existing columns with their normalized BigQuery type strings.\n * Used by the worker to detect type drift before applying schema changes.\n */\n async getTableColumnsWithTypes(\n tableName: string,\n ): Promise<Map<string, string>> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string; type: string }> =\n metadata.schema?.fields ?? [];\n const result = new Map<string, string>();\n for (const f of fields) {\n result.set(f.name, normalizeBigQueryType(f.type));\n }\n return result;\n }\n\n /** Create a table using a fully-qualified name. */\n async createTable(table: SqlTableDef): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const cols = table.columns\n .map((c) => {\n const notNull = c.isPrimaryKey ? \" NOT NULL\" : \"\";\n return ` ${qi(c.name)} ${c.sqlType}${notNull}`;\n })\n .join(\",\\n\");\n\n const ddl = `CREATE TABLE IF NOT EXISTS ${this.fqn(table.tableName)} (\\n${cols}\\n);`;\n await this.bigquery.query({ query: ddl });\n }\n\n /** Add columns to an existing table using a fully-qualified name. */\n async addColumns(tableName: string, columns: SqlColumn[]): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n for (const c of columns) {\n const stmt = `ALTER TABLE ${this.fqn(tableName)} ADD COLUMN ${qi(c.name)} ${c.sqlType};`;\n await this.bigquery.query({ query: stmt });\n }\n }\n\n /** Append rows via BigQuery streaming insert. */\n async insertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n if (rows.length === 0) return;\n await this.dataset.table(tableName).insert(rows);\n }\n\n /**\n * Upsert rows using a MERGE DML statement.\n *\n * Builds a source table from inline SELECT … UNION ALL rows and merges\n * into the target on the given primary key.\n */\n async upsertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n primaryKey: string,\n ): Promise<void> {\n if (rows.length === 0) return;\n\n const allKeys = Object.keys(rows[0]!);\n const nonPkCols = allKeys.filter((k) => k !== primaryKey);\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n\n // Build inline source: SELECT val AS col, … UNION ALL SELECT …\n const selects = rows.map((row, i) => {\n const values = allKeys\n .map((k) => {\n const aliased =\n i === 0\n ? `${this.escapeValue(row[k])} AS ${qi(k)}`\n : this.escapeValue(row[k]);\n return aliased;\n })\n .join(\", \");\n return `SELECT ${values}`;\n });\n\n const source = selects.join(\" UNION ALL\\n \");\n\n // UPDATE SET clause (non-PK columns).\n // Note: when __sync_version is present we still update it so the row\n // tracks the latest applied version.\n const updateSet = nonPkCols\n .map((c) => `T.${qi(c)} = S.${qi(c)}`)\n .join(\", \");\n\n // INSERT columns / values\n const insertCols = allKeys.map((c) => qi(c)).join(\", \");\n const insertVals = allKeys.map((c) => `S.${qi(c)}`).join(\", \");\n\n // Out-of-order protection: only UPDATE when the incoming version is\n // strictly greater than the stored one (NULL stored version means the\n // row pre-dates versioning → always update).\n const versionGuard = allKeys.includes(SYNC_VERSION_COLUMN)\n ? ` AND (T.${qi(SYNC_VERSION_COLUMN)} IS NULL OR S.${qi(SYNC_VERSION_COLUMN)} > T.${qi(SYNC_VERSION_COLUMN)})`\n : \"\";\n\n const query = [\n `MERGE ${this.fqn(tableName)} AS T`,\n `USING (\\n ${source}\\n ) AS S`,\n `ON T.${qi(primaryKey)} = S.${qi(primaryKey)}`,\n `WHEN MATCHED${versionGuard} THEN UPDATE SET ${updateSet}`,\n `WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals});`,\n ].join(\"\\n\");\n\n await withRetry(() => this.bigquery.query({ query }));\n }\n\n /** Delete rows by primary-key values. */\n async deleteRows(\n tableName: string,\n primaryKey: string,\n ids: string[],\n ): Promise<void> {\n if (ids.length === 0) return;\n\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const escaped = ids.map((v) => this.escapeValue(v)).join(\", \");\n const query = `DELETE FROM ${this.fqn(tableName)} WHERE ${qi(primaryKey)} IN (${escaped});`;\n\n await withRetry(() => this.bigquery.query({ query }));\n }\n\n /** Execute a raw SQL statement (used by the migration manager). */\n async executeRaw(sql: string): Promise<void> {\n await this.bigquery.query({ query: sql });\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n /** The BigQuery Dataset handle. */\n private get dataset() {\n return this.bigquery.dataset(this.datasetId);\n }\n\n /** Return the fully-qualified table reference (`` `dataset.table` ``). */\n private fqn(tableName: string): string {\n return `\\`${this.datasetId}.${tableName}\\``;\n }\n\n /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */\n private static readonly ISO_TIMESTAMP_RE =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n /** Escape a value for use as a SQL literal. */\n private escapeValue(v: unknown): string {\n if (v === null || v === undefined) return \"NULL\";\n if (typeof v === \"boolean\") return v ? \"TRUE\" : \"FALSE\";\n if (typeof v === \"number\" || typeof v === \"bigint\") return String(v);\n if (typeof v === \"string\") {\n // ISO 8601 timestamps → TIMESTAMP literal (keeps type-safety with BQ TIMESTAMP columns)\n if (BigQueryAdapter.ISO_TIMESTAMP_RE.test(v)) {\n return `TIMESTAMP('${v}')`;\n }\n // Detect JSON strings (arrays/objects) → use PARSE_JSON for native JSON columns\n if (\n (v.startsWith(\"[\") && v.endsWith(\"]\")) ||\n (v.startsWith(\"{\") && v.endsWith(\"}\"))\n ) {\n return `PARSE_JSON('${v.replace(/'/g, \"\\\\'\")}')`;\n }\n return `'${v.replace(/'/g, \"\\\\'\")}'`;\n }\n // Objects / arrays → JSON\n return `PARSE_JSON('${JSON.stringify(v).replace(/'/g, \"\\\\'\")}')`;\n }\n}\n","/**\n * BigQuery **Storage Write API** adapter (CDC mode).\n *\n * Unlike {@link BigQueryAdapter} which uses MERGE/DELETE DML, this adapter\n * streams rows through the Storage Write API and lets BigQuery apply the\n * Change Data Capture semantics in the background.\n *\n * Why use this:\n *\n * - **No DML concurrency limit.** The legacy adapter is bounded by ≈ 2\n * concurrent MERGE/DELETE statements per table; busy collections trigger\n * `Could not serialize access … due to concurrent update` errors. The\n * Storage Write API has no such bound, so multiple Cloud Function\n * instances can flush in parallel without conflicts.\n * - **Cheaper at scale.** Storage Write API is roughly ~50% cheaper than\n * legacy streaming inserts, and free for the first 2 TiB / month.\n * - **Same idempotency model.** Each row carries `_CHANGE_SEQUENCE_NUMBER`\n * built from the existing `__sync_version` column, so out-of-order\n * deliveries are merged correctly by BigQuery.\n *\n * Requirements:\n *\n * - The destination tables **must** declare a `PRIMARY KEY (...) NOT\n * ENFORCED` constraint and be clustered on that key.\n * {@link createTable} handles both for tables managed by this library.\n * - Tables should be created with `OPTIONS(max_staleness = INTERVAL …)`\n * so the CDC merge runs in the background and queries see fresh data.\n * - Service account needs `bigquery.tables.updateData` (e.g. via the\n * `roles/bigquery.dataEditor` role).\n *\n * @example\n * ```ts\n * import { BigQuery } from \"@google-cloud/bigquery\";\n * import { BigQueryStorageAdapter } from \"@lpdjs/firestore-repo-service/sync/bigquery-storage\";\n *\n * const adapter = new BigQueryStorageAdapter({\n * projectId: \"my-project\",\n * datasetId: \"firestore_sync\",\n * bigquery: new BigQuery({ projectId: \"my-project\" }),\n * maxStaleness: \"INTERVAL 15 MINUTE\",\n * });\n * ```\n */\n\nimport { SYNC_VERSION_COLUMN } from \"../constants\";\nimport type {\n SqlAdapter,\n SqlColumn,\n SqlDialect,\n SqlTableDef,\n} from \"../types\";\nimport { bigqueryDialect } from \"./bigquery\";\nimport { normalizeBigQueryType } from \"./bigquery-types\";\n\n// ---------------------------------------------------------------------------\n// Types (kept loose so `@google-cloud/bigquery-storage` stays an OPTIONAL peer)\n// ---------------------------------------------------------------------------\n\ntype StorageNs = {\n managedwriter: {\n WriterClient: new (opts?: any) => any;\n JSONWriter: new (params: { connection: any; protoDescriptor: any }) => any;\n DefaultStream: any;\n };\n adapt: {\n convertBigQuerySchemaToStorageTableSchema(schema: any): any;\n convertStorageSchemaToProto2Descriptor(\n schema: any,\n scope: string,\n ...opts: any[]\n ): any;\n withChangeType(): any;\n withChangeSequenceNumber(): any;\n };\n};\n\nlet storageNsCache: StorageNs | null = null;\nfunction loadStorageNs(): StorageNs {\n if (storageNsCache) return storageNsCache;\n // Lazy require so the library does not pull `@google-cloud/bigquery-storage`\n // unless this adapter is actually instantiated.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require(\"@google-cloud/bigquery-storage\");\n storageNsCache = mod as StorageNs;\n return storageNsCache;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format `__sync_version` (Firestore commit timestamp in microseconds since\n * epoch) as the 16-character hex string required by `_CHANGE_SEQUENCE_NUMBER`.\n *\n * BigQuery compares sequence numbers lexicographically, so a fixed-width hex\n * representation is mandatory for correct ordering.\n */\nfunction formatChangeSequenceNumber(version: unknown): string {\n let n: bigint;\n if (typeof version === \"bigint\") n = version;\n else if (typeof version === \"number\") n = BigInt(version);\n else if (typeof version === \"string\") n = BigInt(version);\n else n = 0n;\n if (n < 0n) n = 0n;\n return n.toString(16).padStart(16, \"0\");\n}\n\n/**\n * Returns true when the Storage Write append failed with a transient gRPC\n * status (UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, ABORTED). Caller can\n * safely retry these.\n */\nfunction isRetryableStorageError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: number };\n // gRPC codes: 4=DEADLINE_EXCEEDED, 10=ABORTED, 13=INTERNAL, 14=UNAVAILABLE\n return e.code === 4 || e.code === 10 || e.code === 13 || e.code === 14;\n}\n\nasync function withRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 6,\n baseMs = 200,\n): Promise<T> {\n let attempt = 0;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n return await fn();\n } catch (err) {\n attempt++;\n if (!isRetryableStorageError(err) || attempt > maxRetries) throw err;\n const cap = baseMs * Math.pow(2, attempt);\n const delay = Math.random() * cap;\n await new Promise((res) => setTimeout(res, delay));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nexport interface BigQueryStorageAdapterOptions {\n /** GCP project id that owns the dataset. */\n projectId: string;\n /** BigQuery dataset id. */\n datasetId: string;\n /**\n * BigQuery client used for DDL operations (createTable, addColumns,\n * getMetadata, executeRaw). Storage Write only handles inserts.\n */\n bigquery: any;\n /**\n * Optional pre-built `WriterClient`. When omitted a new one is created on\n * first use.\n */\n writerClient?: any;\n /**\n * Value passed to `OPTIONS(max_staleness = ...)` on `createTable`.\n * Recommended: `INTERVAL 15 MINUTE` in production, `INTERVAL 1 MINUTE`\n * in development. Set to `null` to omit (BigQuery defaults to 0, which\n * forces a merge on every read and is **not** what you want).\n * @default \"INTERVAL 15 MINUTE\"\n */\n maxStaleness?: string | null;\n}\n\n/**\n * Storage Write API implementation of {@link SqlAdapter}.\n */\nexport class BigQueryStorageAdapter implements SqlAdapter {\n private readonly bigquery: any;\n private readonly projectId: string;\n private readonly datasetId: string;\n private readonly maxStaleness: string | null;\n private writerClient: any;\n /** Cache of `{ writer, primaryKey }` per table name. */\n private readonly writers = new Map<\n string,\n { writer: any; primaryKey: string }\n >();\n\n constructor(options: BigQueryStorageAdapterOptions) {\n this.bigquery = options.bigquery;\n this.projectId = options.projectId;\n this.datasetId = options.datasetId;\n this.maxStaleness =\n options.maxStaleness === undefined\n ? \"INTERVAL 15 MINUTE\"\n : options.maxStaleness;\n this.writerClient = options.writerClient;\n }\n\n get dialect(): SqlDialect {\n return bigqueryDialect;\n }\n\n // -------- DDL (delegated to @google-cloud/bigquery) ----------------------\n\n async tableExists(tableName: string): Promise<boolean> {\n const [exists] = await this.dataset.table(tableName).exists();\n return exists;\n }\n\n async getTableColumns(tableName: string): Promise<string[]> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string }> = metadata.schema?.fields ?? [];\n return fields.map((f) => f.name);\n }\n\n async getTableColumnsWithTypes(\n tableName: string,\n ): Promise<Map<string, string>> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string; type: string }> =\n metadata.schema?.fields ?? [];\n const result = new Map<string, string>();\n for (const f of fields) {\n result.set(f.name, normalizeBigQueryType(f.type));\n }\n return result;\n }\n\n async createTable(table: SqlTableDef): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const pk = table.columns.find((c) => c.isPrimaryKey)?.name;\n if (!pk) {\n throw new Error(\n `BigQueryStorageAdapter requires a primary key on table \\`${table.tableName}\\` ` +\n `(Storage Write CDC mode needs PRIMARY KEY NOT ENFORCED).`,\n );\n }\n const cols = table.columns\n .map((c) => {\n const notNull = c.isPrimaryKey ? \" NOT NULL\" : \"\";\n return ` ${qi(c.name)} ${c.sqlType}${notNull}`;\n })\n .join(\",\\n\");\n\n const opts: string[] = [];\n if (this.maxStaleness !== null) {\n opts.push(`max_staleness = ${this.maxStaleness}`);\n }\n const optionsClause =\n opts.length > 0 ? `\\nOPTIONS(${opts.join(\", \")})` : \"\";\n\n const ddl =\n `CREATE TABLE IF NOT EXISTS ${this.fqn(table.tableName)} (\\n${cols},\\n` +\n ` PRIMARY KEY (${qi(pk)}) NOT ENFORCED\\n)` +\n `\\nCLUSTER BY ${qi(pk)}` +\n `${optionsClause};`;\n\n await this.bigquery.query({ query: ddl });\n }\n\n async addColumns(tableName: string, columns: SqlColumn[]): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n for (const c of columns) {\n const stmt = `ALTER TABLE ${this.fqn(tableName)} ADD COLUMN ${qi(c.name)} ${c.sqlType};`;\n await this.bigquery.query({ query: stmt });\n }\n }\n\n async executeRaw(sql: string): Promise<void> {\n await this.bigquery.query({ query: sql });\n }\n\n /**\n * Invalidate the cached writer for a table. Called by the worker after\n * `addColumns` so the next append rebuilds the proto descriptor against\n * the new schema.\n */\n onSchemaChange(tableName: string): void {\n const cached = this.writers.get(tableName);\n if (cached) {\n try {\n cached.writer.close();\n } catch {\n // ignore — connection may already be torn down\n }\n this.writers.delete(tableName);\n }\n }\n\n // -------- Inserts (Storage Write API) -----------------------------------\n\n async insertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n if (rows.length === 0) return;\n // Plain inserts have no PK matching, but in CDC mode every row needs a\n // _CHANGE_TYPE. We treat them as UPSERT.\n const writer = await this.getOrCreateWriter(tableName);\n const cdc = rows.map((row) => ({\n ...this.normalizeRow(row),\n _CHANGE_TYPE: \"UPSERT\",\n _CHANGE_SEQUENCE_NUMBER: formatChangeSequenceNumber(\n row[SYNC_VERSION_COLUMN],\n ),\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n async upsertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n primaryKey: string,\n ): Promise<void> {\n if (rows.length === 0) return;\n const writer = await this.getOrCreateWriter(tableName, primaryKey);\n const cdc = rows.map((row) => ({\n ...this.normalizeRow(row),\n _CHANGE_TYPE: \"UPSERT\",\n _CHANGE_SEQUENCE_NUMBER: formatChangeSequenceNumber(\n row[SYNC_VERSION_COLUMN],\n ),\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n async deleteRows(\n tableName: string,\n primaryKey: string,\n ids: string[],\n ): Promise<void> {\n if (ids.length === 0) return;\n const writer = await this.getOrCreateWriter(tableName, primaryKey);\n // Storage Write CDC requires a value for every NOT-NULL column (the PK)\n // and a `_CHANGE_TYPE`. Other columns may be omitted; missing values are\n // interpreted as NULL by default.\n const cdc = ids.map((id) => ({\n [primaryKey]: id,\n _CHANGE_TYPE: \"DELETE\",\n // No reliable version on plain deletes (the queue does not carry one\n // for tombstones), so we pass an all-zero sequence number meaning\n // \"apply only if newer than every previous UPSERT\" only when explicit\n // versioning is enabled. For tombstones we instead use the maximum\n // value to make sure the DELETE wins over any concurrent UPSERT for\n // the same key.\n _CHANGE_SEQUENCE_NUMBER: \"ffffffffffffffff\",\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n // -------- Internal helpers ----------------------------------------------\n\n private get dataset() {\n return this.bigquery.dataset(this.datasetId);\n }\n\n private fqn(tableName: string): string {\n return `\\`${this.datasetId}.${tableName}\\``;\n }\n\n /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */\n private static readonly ISO_TIMESTAMP_RE =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n /**\n * Convert a Date or epoch-millis number into the epoch-microseconds string\n * required by the Storage Write API for TIMESTAMP columns.\n *\n * Unlike the legacy BigQueryAdapter (which builds SQL `TIMESTAMP('iso')`\n * literals), the JSONWriter encodes through protobuf and a TIMESTAMP field\n * is an `int64`. Passing an ISO string would make `Long.fromString` throw\n * `interior hyphen` on the `-` characters.\n */\n private static toEpochMicros(d: Date): string {\n const ms = d.getTime();\n return (BigInt(ms) * 1000n).toString();\n }\n\n /** Convert JS values into shapes accepted by JSONWriter. */\n private normalizeRow(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(row)) {\n if (v === undefined) continue;\n if (v instanceof Date) {\n // TIMESTAMP column → int64 epoch micros (as string to preserve precision)\n out[k] = BigQueryStorageAdapter.toEpochMicros(v);\n } else if (\n typeof v === \"string\" &&\n BigQueryStorageAdapter.ISO_TIMESTAMP_RE.test(v)\n ) {\n // ISO timestamp produced upstream by `serializeDocument` (Firestore\n // Timestamp → ISO string) — convert to epoch micros for protobuf.\n const d = new Date(v);\n if (!Number.isNaN(d.getTime())) {\n out[k] = BigQueryStorageAdapter.toEpochMicros(d);\n } else {\n out[k] = v;\n }\n } else if (typeof v === \"object\" && v !== null) {\n // JSON columns: serialise nested objects/arrays\n out[k] = JSON.stringify(v);\n } else if (typeof v === \"bigint\") {\n out[k] = v.toString();\n } else {\n out[k] = v;\n }\n }\n return out;\n }\n\n private async appendWithRetry(\n tableName: string,\n writer: any,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n await withRetry(async () => {\n try {\n const pending = writer.appendRows(rows);\n await pending.getResult();\n } catch (err) {\n // On certain errors (schema drift, broken connection) drop the\n // cached writer so the next call rebuilds it.\n this.onSchemaChange(tableName);\n throw err;\n }\n });\n }\n\n private async getOrCreateWriter(\n tableName: string,\n primaryKey?: string,\n ): Promise<any> {\n const cached = this.writers.get(tableName);\n if (cached) {\n if (primaryKey && cached.primaryKey !== primaryKey) {\n // PK changed — invalidate and rebuild\n this.onSchemaChange(tableName);\n } else {\n return cached.writer;\n }\n }\n\n const ns = loadStorageNs();\n if (!this.writerClient) {\n this.writerClient = new ns.managedwriter.WriterClient({\n projectId: this.projectId,\n });\n }\n\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const bqSchema = { fields: metadata.schema?.fields ?? [] };\n if (!primaryKey) {\n // Try to recover PK from table constraints if available\n const tableConstraintsPK =\n metadata.tableConstraints?.primaryKey?.columns?.[0];\n primaryKey = tableConstraintsPK ?? bqSchema.fields[0]?.name;\n }\n\n const storageSchema =\n ns.adapt.convertBigQuerySchemaToStorageTableSchema(bqSchema);\n const protoDescriptor = ns.adapt.convertStorageSchemaToProto2Descriptor(\n storageSchema,\n \"Row\",\n ns.adapt.withChangeType(),\n ns.adapt.withChangeSequenceNumber(),\n );\n\n const destinationTable = `projects/${this.projectId}/datasets/${this.datasetId}/tables/${tableName}`;\n // Pass `DefaultStream` as `streamId` (not `streamType`): the `_default`\n // stream is implicit on every table, so the client must short-circuit to\n // `${destinationTable}/streams/_default` instead of calling\n // `createWriteStream({ type: DEFAULT })` which BigQuery rejects with\n // `Unable to create a stream with type TYPE_UNSPECIFIED`.\n const connection = await this.writerClient.createStreamConnection({\n streamId: ns.managedwriter.DefaultStream,\n destinationTable,\n });\n\n const writer = new ns.managedwriter.JSONWriter({\n connection,\n protoDescriptor,\n });\n\n this.writers.set(tableName, { writer, primaryKey: primaryKey ?? \"\" });\n return writer;\n }\n\n /** Close all open writer connections. Useful for graceful shutdown. */\n async close(): Promise<void> {\n for (const { writer } of this.writers.values()) {\n try {\n writer.close();\n } catch {\n /* ignore */\n }\n }\n this.writers.clear();\n if (this.writerClient && typeof this.writerClient.close === \"function\") {\n try {\n this.writerClient.close();\n } catch {\n /* ignore */\n }\n }\n }\n}\n"]}
@@ -1,125 +0,0 @@
1
- import { a as SqlAdapter, b as SqlDialect, c as SqlTableDef, d as SqlColumn } from '../types-_JSH59Iy.cjs';
2
- import '../firebase-auth-D1APf9PA.cjs';
3
- import 'firebase-functions/v2/firestore';
4
- import 'firebase-functions/v2/https';
5
- import 'firebase-functions/v2/pubsub';
6
-
7
- /**
8
- * BigQuery **Storage Write API** adapter (CDC mode).
9
- *
10
- * Unlike {@link BigQueryAdapter} which uses MERGE/DELETE DML, this adapter
11
- * streams rows through the Storage Write API and lets BigQuery apply the
12
- * Change Data Capture semantics in the background.
13
- *
14
- * Why use this:
15
- *
16
- * - **No DML concurrency limit.** The legacy adapter is bounded by ≈ 2
17
- * concurrent MERGE/DELETE statements per table; busy collections trigger
18
- * `Could not serialize access … due to concurrent update` errors. The
19
- * Storage Write API has no such bound, so multiple Cloud Function
20
- * instances can flush in parallel without conflicts.
21
- * - **Cheaper at scale.** Storage Write API is roughly ~50% cheaper than
22
- * legacy streaming inserts, and free for the first 2 TiB / month.
23
- * - **Same idempotency model.** Each row carries `_CHANGE_SEQUENCE_NUMBER`
24
- * built from the existing `__sync_version` column, so out-of-order
25
- * deliveries are merged correctly by BigQuery.
26
- *
27
- * Requirements:
28
- *
29
- * - The destination tables **must** declare a `PRIMARY KEY (...) NOT
30
- * ENFORCED` constraint and be clustered on that key.
31
- * {@link createTable} handles both for tables managed by this library.
32
- * - Tables should be created with `OPTIONS(max_staleness = INTERVAL …)`
33
- * so the CDC merge runs in the background and queries see fresh data.
34
- * - Service account needs `bigquery.tables.updateData` (e.g. via the
35
- * `roles/bigquery.dataEditor` role).
36
- *
37
- * @example
38
- * ```ts
39
- * import { BigQuery } from "@google-cloud/bigquery";
40
- * import { BigQueryStorageAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery-storage";
41
- *
42
- * const adapter = new BigQueryStorageAdapter({
43
- * projectId: "my-project",
44
- * datasetId: "firestore_sync",
45
- * bigquery: new BigQuery({ projectId: "my-project" }),
46
- * maxStaleness: "INTERVAL 15 MINUTE",
47
- * });
48
- * ```
49
- */
50
-
51
- interface BigQueryStorageAdapterOptions {
52
- /** GCP project id that owns the dataset. */
53
- projectId: string;
54
- /** BigQuery dataset id. */
55
- datasetId: string;
56
- /**
57
- * BigQuery client used for DDL operations (createTable, addColumns,
58
- * getMetadata, executeRaw). Storage Write only handles inserts.
59
- */
60
- bigquery: any;
61
- /**
62
- * Optional pre-built `WriterClient`. When omitted a new one is created on
63
- * first use.
64
- */
65
- writerClient?: any;
66
- /**
67
- * Value passed to `OPTIONS(max_staleness = ...)` on `createTable`.
68
- * Recommended: `INTERVAL 15 MINUTE` in production, `INTERVAL 1 MINUTE`
69
- * in development. Set to `null` to omit (BigQuery defaults to 0, which
70
- * forces a merge on every read and is **not** what you want).
71
- * @default "INTERVAL 15 MINUTE"
72
- */
73
- maxStaleness?: string | null;
74
- }
75
- /**
76
- * Storage Write API implementation of {@link SqlAdapter}.
77
- */
78
- declare class BigQueryStorageAdapter implements SqlAdapter {
79
- private readonly bigquery;
80
- private readonly projectId;
81
- private readonly datasetId;
82
- private readonly maxStaleness;
83
- private writerClient;
84
- /** Cache of `{ writer, primaryKey }` per table name. */
85
- private readonly writers;
86
- constructor(options: BigQueryStorageAdapterOptions);
87
- get dialect(): SqlDialect;
88
- tableExists(tableName: string): Promise<boolean>;
89
- getTableColumns(tableName: string): Promise<string[]>;
90
- getTableColumnsWithTypes(tableName: string): Promise<Map<string, string>>;
91
- createTable(table: SqlTableDef): Promise<void>;
92
- addColumns(tableName: string, columns: SqlColumn[]): Promise<void>;
93
- executeRaw(sql: string): Promise<void>;
94
- /**
95
- * Invalidate the cached writer for a table. Called by the worker after
96
- * `addColumns` so the next append rebuilds the proto descriptor against
97
- * the new schema.
98
- */
99
- onSchemaChange(tableName: string): void;
100
- insertRows(tableName: string, rows: Record<string, unknown>[]): Promise<void>;
101
- upsertRows(tableName: string, rows: Record<string, unknown>[], primaryKey: string): Promise<void>;
102
- deleteRows(tableName: string, primaryKey: string, ids: string[]): Promise<void>;
103
- private get dataset();
104
- private fqn;
105
- /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */
106
- private static readonly ISO_TIMESTAMP_RE;
107
- /**
108
- * Convert a Date or epoch-millis number into the epoch-microseconds string
109
- * required by the Storage Write API for TIMESTAMP columns.
110
- *
111
- * Unlike the legacy BigQueryAdapter (which builds SQL `TIMESTAMP('iso')`
112
- * literals), the JSONWriter encodes through protobuf and a TIMESTAMP field
113
- * is an `int64`. Passing an ISO string would make `Long.fromString` throw
114
- * `interior hyphen` on the `-` characters.
115
- */
116
- private static toEpochMicros;
117
- /** Convert JS values into shapes accepted by JSONWriter. */
118
- private normalizeRow;
119
- private appendWithRetry;
120
- private getOrCreateWriter;
121
- /** Close all open writer connections. Useful for graceful shutdown. */
122
- close(): Promise<void>;
123
- }
124
-
125
- export { BigQueryStorageAdapter, type BigQueryStorageAdapterOptions };
@@ -1,125 +0,0 @@
1
- import { a as SqlAdapter, b as SqlDialect, c as SqlTableDef, d as SqlColumn } from '../types-Bhy7MfCj.js';
2
- import '../firebase-auth-D1APf9PA.js';
3
- import 'firebase-functions/v2/firestore';
4
- import 'firebase-functions/v2/https';
5
- import 'firebase-functions/v2/pubsub';
6
-
7
- /**
8
- * BigQuery **Storage Write API** adapter (CDC mode).
9
- *
10
- * Unlike {@link BigQueryAdapter} which uses MERGE/DELETE DML, this adapter
11
- * streams rows through the Storage Write API and lets BigQuery apply the
12
- * Change Data Capture semantics in the background.
13
- *
14
- * Why use this:
15
- *
16
- * - **No DML concurrency limit.** The legacy adapter is bounded by ≈ 2
17
- * concurrent MERGE/DELETE statements per table; busy collections trigger
18
- * `Could not serialize access … due to concurrent update` errors. The
19
- * Storage Write API has no such bound, so multiple Cloud Function
20
- * instances can flush in parallel without conflicts.
21
- * - **Cheaper at scale.** Storage Write API is roughly ~50% cheaper than
22
- * legacy streaming inserts, and free for the first 2 TiB / month.
23
- * - **Same idempotency model.** Each row carries `_CHANGE_SEQUENCE_NUMBER`
24
- * built from the existing `__sync_version` column, so out-of-order
25
- * deliveries are merged correctly by BigQuery.
26
- *
27
- * Requirements:
28
- *
29
- * - The destination tables **must** declare a `PRIMARY KEY (...) NOT
30
- * ENFORCED` constraint and be clustered on that key.
31
- * {@link createTable} handles both for tables managed by this library.
32
- * - Tables should be created with `OPTIONS(max_staleness = INTERVAL …)`
33
- * so the CDC merge runs in the background and queries see fresh data.
34
- * - Service account needs `bigquery.tables.updateData` (e.g. via the
35
- * `roles/bigquery.dataEditor` role).
36
- *
37
- * @example
38
- * ```ts
39
- * import { BigQuery } from "@google-cloud/bigquery";
40
- * import { BigQueryStorageAdapter } from "@lpdjs/firestore-repo-service/sync/bigquery-storage";
41
- *
42
- * const adapter = new BigQueryStorageAdapter({
43
- * projectId: "my-project",
44
- * datasetId: "firestore_sync",
45
- * bigquery: new BigQuery({ projectId: "my-project" }),
46
- * maxStaleness: "INTERVAL 15 MINUTE",
47
- * });
48
- * ```
49
- */
50
-
51
- interface BigQueryStorageAdapterOptions {
52
- /** GCP project id that owns the dataset. */
53
- projectId: string;
54
- /** BigQuery dataset id. */
55
- datasetId: string;
56
- /**
57
- * BigQuery client used for DDL operations (createTable, addColumns,
58
- * getMetadata, executeRaw). Storage Write only handles inserts.
59
- */
60
- bigquery: any;
61
- /**
62
- * Optional pre-built `WriterClient`. When omitted a new one is created on
63
- * first use.
64
- */
65
- writerClient?: any;
66
- /**
67
- * Value passed to `OPTIONS(max_staleness = ...)` on `createTable`.
68
- * Recommended: `INTERVAL 15 MINUTE` in production, `INTERVAL 1 MINUTE`
69
- * in development. Set to `null` to omit (BigQuery defaults to 0, which
70
- * forces a merge on every read and is **not** what you want).
71
- * @default "INTERVAL 15 MINUTE"
72
- */
73
- maxStaleness?: string | null;
74
- }
75
- /**
76
- * Storage Write API implementation of {@link SqlAdapter}.
77
- */
78
- declare class BigQueryStorageAdapter implements SqlAdapter {
79
- private readonly bigquery;
80
- private readonly projectId;
81
- private readonly datasetId;
82
- private readonly maxStaleness;
83
- private writerClient;
84
- /** Cache of `{ writer, primaryKey }` per table name. */
85
- private readonly writers;
86
- constructor(options: BigQueryStorageAdapterOptions);
87
- get dialect(): SqlDialect;
88
- tableExists(tableName: string): Promise<boolean>;
89
- getTableColumns(tableName: string): Promise<string[]>;
90
- getTableColumnsWithTypes(tableName: string): Promise<Map<string, string>>;
91
- createTable(table: SqlTableDef): Promise<void>;
92
- addColumns(tableName: string, columns: SqlColumn[]): Promise<void>;
93
- executeRaw(sql: string): Promise<void>;
94
- /**
95
- * Invalidate the cached writer for a table. Called by the worker after
96
- * `addColumns` so the next append rebuilds the proto descriptor against
97
- * the new schema.
98
- */
99
- onSchemaChange(tableName: string): void;
100
- insertRows(tableName: string, rows: Record<string, unknown>[]): Promise<void>;
101
- upsertRows(tableName: string, rows: Record<string, unknown>[], primaryKey: string): Promise<void>;
102
- deleteRows(tableName: string, primaryKey: string, ids: string[]): Promise<void>;
103
- private get dataset();
104
- private fqn;
105
- /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */
106
- private static readonly ISO_TIMESTAMP_RE;
107
- /**
108
- * Convert a Date or epoch-millis number into the epoch-microseconds string
109
- * required by the Storage Write API for TIMESTAMP columns.
110
- *
111
- * Unlike the legacy BigQueryAdapter (which builds SQL `TIMESTAMP('iso')`
112
- * literals), the JSONWriter encodes through protobuf and a TIMESTAMP field
113
- * is an `int64`. Passing an ISO string would make `Long.fromString` throw
114
- * `interior hyphen` on the `-` characters.
115
- */
116
- private static toEpochMicros;
117
- /** Convert JS values into shapes accepted by JSONWriter. */
118
- private normalizeRow;
119
- private appendWithRetry;
120
- private getOrCreateWriter;
121
- /** Close all open writer connections. Useful for graceful shutdown. */
122
- close(): Promise<void>;
123
- }
124
-
125
- export { BigQueryStorageAdapter, type BigQueryStorageAdapterOptions };
@@ -1,8 +0,0 @@
1
- var A=(a=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(a,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):a)(function(a){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+a+'" is not supported')});var d="__sync_version";function m(a){let t=a.toUpperCase();switch(t){case "INTEGER":return "INT64";case "FLOAT":return "FLOAT64";case "BOOLEAN":return "BOOL";default:return t}}var S=class{constructor(){this.name="bigquery";}mapType(t){switch(t){case "string":return "STRING";case "number":return "FLOAT64";case "bigint":return "INT64";case "boolean":return "BOOL";case "timestamp":return "TIMESTAMP";case "json":return "JSON";case "text":return "STRING"}}quoteIdentifier(t){return `\`${t}\``}},w=new S;var f=null;function P(){return f||(f=A("@google-cloud/bigquery-storage"),f)}function N(a){let t;return typeof a=="bigint"?t=a:typeof a=="number"||typeof a=="string"?t=BigInt(a):t=0n,t<0n&&(t=0n),t.toString(16).padStart(16,"0")}function $(a){if(!a||typeof a!="object")return false;let t=a;return t.code===4||t.code===10||t.code===13||t.code===14}async function O(a,t=6,r=200){let n=0;for(;;)try{return await a()}catch(e){if(n++,!$(e)||n>t)throw e;let s=r*Math.pow(2,n),i=Math.random()*s;await new Promise(c=>setTimeout(c,i));}}var u=class u{constructor(t){this.writers=new Map;this.bigquery=t.bigquery,this.projectId=t.projectId,this.datasetId=t.datasetId,this.maxStaleness=t.maxStaleness===void 0?"INTERVAL 15 MINUTE":t.maxStaleness,this.writerClient=t.writerClient;}get dialect(){return w}async tableExists(t){let[r]=await this.dataset.table(t).exists();return r}async getTableColumns(t){let[r]=await this.dataset.table(t).getMetadata();return (r.schema?.fields??[]).map(e=>e.name)}async getTableColumnsWithTypes(t){let[r]=await this.dataset.table(t).getMetadata(),n=r.schema?.fields??[],e=new Map;for(let s of n)e.set(s.name,m(s.type));return e}async createTable(t){let r=l=>this.dialect.quoteIdentifier(l),n=t.columns.find(l=>l.isPrimaryKey)?.name;if(!n)throw new Error(`BigQueryStorageAdapter requires a primary key on table \`${t.tableName}\` (Storage Write CDC mode needs PRIMARY KEY NOT ENFORCED).`);let e=t.columns.map(l=>{let g=l.isPrimaryKey?" NOT NULL":"";return ` ${r(l.name)} ${l.sqlType}${g}`}).join(`,
2
- `),s=[];this.maxStaleness!==null&&s.push(`max_staleness = ${this.maxStaleness}`);let i=s.length>0?`
3
- OPTIONS(${s.join(", ")})`:"",c=`CREATE TABLE IF NOT EXISTS ${this.fqn(t.tableName)} (
4
- ${e},
5
- PRIMARY KEY (${r(n)}) NOT ENFORCED
6
- )
7
- CLUSTER BY ${r(n)}${i};`;await this.bigquery.query({query:c});}async addColumns(t,r){let n=e=>this.dialect.quoteIdentifier(e);for(let e of r){let s=`ALTER TABLE ${this.fqn(t)} ADD COLUMN ${n(e.name)} ${e.sqlType};`;await this.bigquery.query({query:s});}}async executeRaw(t){await this.bigquery.query({query:t});}onSchemaChange(t){let r=this.writers.get(t);if(r){try{r.writer.close();}catch{}this.writers.delete(t);}}async insertRows(t,r){if(r.length===0)return;let n=await this.getOrCreateWriter(t),e=r.map(s=>({...this.normalizeRow(s),_CHANGE_TYPE:"UPSERT",_CHANGE_SEQUENCE_NUMBER:N(s[d])}));await this.appendWithRetry(t,n,e);}async upsertRows(t,r,n){if(r.length===0)return;let e=await this.getOrCreateWriter(t,n),s=r.map(i=>({...this.normalizeRow(i),_CHANGE_TYPE:"UPSERT",_CHANGE_SEQUENCE_NUMBER:N(i[d])}));await this.appendWithRetry(t,e,s);}async deleteRows(t,r,n){if(n.length===0)return;let e=await this.getOrCreateWriter(t,r),s=n.map(i=>({[r]:i,_CHANGE_TYPE:"DELETE",_CHANGE_SEQUENCE_NUMBER:"ffffffffffffffff"}));await this.appendWithRetry(t,e,s);}get dataset(){return this.bigquery.dataset(this.datasetId)}fqn(t){return `\`${this.datasetId}.${t}\``}static toEpochMicros(t){let r=t.getTime();return (BigInt(r)*1000n).toString()}normalizeRow(t){let r={};for(let[n,e]of Object.entries(t))if(e!==void 0)if(e instanceof Date)r[n]=u.toEpochMicros(e);else if(typeof e=="string"&&u.ISO_TIMESTAMP_RE.test(e)){let s=new Date(e);Number.isNaN(s.getTime())?r[n]=e:r[n]=u.toEpochMicros(s);}else typeof e=="object"&&e!==null?r[n]=JSON.stringify(e):typeof e=="bigint"?r[n]=e.toString():r[n]=e;return r}async appendWithRetry(t,r,n){await O(async()=>{try{await r.appendRows(n).getResult();}catch(e){throw this.onSchemaChange(t),e}});}async getOrCreateWriter(t,r){let n=this.writers.get(t);if(n)if(r&&n.primaryKey!==r)this.onSchemaChange(t);else return n.writer;let e=P();this.writerClient||(this.writerClient=new e.managedwriter.WriterClient({projectId:this.projectId}));let[s]=await this.dataset.table(t).getMetadata(),i={fields:s.schema?.fields??[]};r||(r=s.tableConstraints?.primaryKey?.columns?.[0]??i.fields[0]?.name);let c=e.adapt.convertBigQuerySchemaToStorageTableSchema(i),l=e.adapt.convertStorageSchemaToProto2Descriptor(c,"Row",e.adapt.withChangeType(),e.adapt.withChangeSequenceNumber()),g=`projects/${this.projectId}/datasets/${this.datasetId}/tables/${t}`,h=await this.writerClient.createStreamConnection({streamId:e.managedwriter.DefaultStream,destinationTable:g}),y=new e.managedwriter.JSONWriter({connection:h,protoDescriptor:l});return this.writers.set(t,{writer:y,primaryKey:r??""}),y}async close(){for(let{writer:t}of this.writers.values())try{t.close();}catch{}if(this.writers.clear(),this.writerClient&&typeof this.writerClient.close=="function")try{this.writerClient.close();}catch{}}};u.ISO_TIMESTAMP_RE=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;var I=u;export{I as BigQueryStorageAdapter};//# sourceMappingURL=bigquery-storage.js.map
8
- //# sourceMappingURL=bigquery-storage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/sync/constants.ts","../../src/sync/adapters/bigquery-types.ts","../../src/sync/adapters/bigquery.ts","../../src/sync/adapters/bigquery-storage.ts"],"names":["SYNC_VERSION_COLUMN","normalizeBigQueryType","type","upper","BigQueryDialect","logical","id","bigqueryDialect","storageNsCache","loadStorageNs","formatChangeSequenceNumber","version","n","isRetryableStorageError","err","e","withRetry","fn","maxRetries","baseMs","attempt","cap","delay","res","_BigQueryStorageAdapter","options","tableName","exists","metadata","f","fields","result","table","qi","pk","c","cols","notNull","opts","optionsClause","ddl","columns","stmt","sql","cached","rows","writer","cdc","row","primaryKey","ids","d","ms","out","k","v","ns","bqSchema","storageSchema","protoDescriptor","destinationTable","connection","BigQueryStorageAdapter"],"mappings":"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","file":"bigquery-storage.js","sourcesContent":["/**\n * Internal constants shared between the worker, queue, schema mapper and\n * SQL adapters.\n */\n\n/**\n * Name of the SQL column that stores the publish-time `version` of each\n * sync event. Used by the worker to discard out-of-order PubSub deliveries\n * (the MERGE only updates rows when the incoming version is strictly\n * greater than the stored one).\n *\n * Two underscores prefix avoids collisions with user-defined fields.\n */\nexport const SYNC_VERSION_COLUMN = \"__sync_version\";\n","/**\n * BigQuery type-name utilities shared by the legacy `BigQueryAdapter` (DML\n * MERGE) and the new `BigQueryStorageAdapter` (Storage Write API).\n *\n * - {@link normalizeBigQueryType} canonicalises BigQuery type strings so that\n * `INTEGER`/`INT64`, `FLOAT`/`FLOAT64`, `BOOLEAN`/`BOOL`, etc. compare\n * equal during schema-drift detection.\n * - {@link isBigQueryTypeCompatible} returns whether a desired type can\n * safely keep an existing column of another type — only widenings that\n * BigQuery accepts via `ALTER COLUMN SET DATA TYPE` are allowed.\n */\n\n/**\n * Canonicalise a BigQuery type name returned by `getMetadata().schema`\n * (which may use legacy aliases like `INTEGER` or `FLOAT`) so that it can\n * be compared against the type produced by `BigQueryDialect.mapType()`.\n */\nexport function normalizeBigQueryType(type: string): string {\n const upper = type.toUpperCase();\n switch (upper) {\n case \"INTEGER\":\n return \"INT64\";\n case \"FLOAT\":\n return \"FLOAT64\";\n case \"BOOLEAN\":\n return \"BOOL\";\n default:\n return upper;\n }\n}\n\n/**\n * Whether `desired` is the same as, or a safe widening of, `existing`.\n * The widenings mirror what BigQuery allows via\n * `ALTER COLUMN x SET DATA TYPE …` — see\n * https://cloud.google.com/bigquery/docs/managing-table-schemas#change_column_types\n */\nexport function isBigQueryTypeCompatible(\n existing: string,\n desired: string,\n): boolean {\n const a = normalizeBigQueryType(existing);\n const b = normalizeBigQueryType(desired);\n if (a === b) return true;\n\n const widenings: Record<string, string[]> = {\n INT64: [\"NUMERIC\", \"BIGNUMERIC\", \"FLOAT64\"],\n NUMERIC: [\"BIGNUMERIC\", \"FLOAT64\"],\n DATE: [\"DATETIME\", \"TIMESTAMP\"],\n DATETIME: [\"TIMESTAMP\"],\n };\n return widenings[a]?.includes(b) ?? false;\n}\n","import { SYNC_VERSION_COLUMN } from \"../constants\";\nimport type {\n LogicalType,\n SqlAdapter,\n SqlColumn,\n SqlDialect,\n SqlTableDef,\n} from \"../types\";\nimport { normalizeBigQueryType } from \"./bigquery-types\";\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Returns true when the BigQuery error is a \"concurrent update\" serialization\n * conflict (code 400, reason \"invalidQuery\" containing \"serialize access\").\n * These are safe to retry after a brief back-off.\n */\nfunction isConcurrentUpdateError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as Record<string, unknown>;\n if (e[\"code\"] !== 400) return false;\n const errors = Array.isArray(e[\"errors\"]) ? e[\"errors\"] : [];\n return errors.some(\n (x: any) =>\n typeof x?.message === \"string\" &&\n x.message.toLowerCase().includes(\"serialize access\"),\n );\n}\n\n/**\n * Execute `fn`, retrying up to `maxRetries` times when BigQuery returns a\n * concurrent-update error. Uses full-jitter exponential back-off.\n */\nasync function withRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 10,\n baseMs = 500,\n): Promise<T> {\n let attempt = 0;\n while (true) {\n try {\n return await fn();\n } catch (err) {\n attempt++;\n if (!isConcurrentUpdateError(err) || attempt > maxRetries) throw err;\n const cap = baseMs * Math.pow(2, attempt);\n const delay = Math.random() * cap; // full jitter\n await new Promise((res) => setTimeout(res, delay));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Dialect (internal — used only by BigQueryAdapter)\n// ---------------------------------------------------------------------------\n\n/** BigQuery SQL dialect mapping. */\nclass BigQueryDialect implements SqlDialect {\n readonly name = \"bigquery\";\n\n mapType(logical: LogicalType): string {\n switch (logical) {\n case \"string\":\n return \"STRING\";\n case \"number\":\n return \"FLOAT64\";\n case \"bigint\":\n return \"INT64\";\n case \"boolean\":\n return \"BOOL\";\n case \"timestamp\":\n return \"TIMESTAMP\";\n case \"json\":\n return \"JSON\";\n case \"text\":\n return \"STRING\";\n }\n }\n\n quoteIdentifier(id: string): string {\n return `\\`${id}\\``;\n }\n}\n\n/** Shared BigQuery dialect singleton. */\nexport const bigqueryDialect: SqlDialect = new BigQueryDialect();\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\n/**\n * BigQuery implementation of {@link SqlAdapter}.\n *\n * Accepts an already-configured BigQuery client so the library does not pull\n * in `@google-cloud/bigquery` as a hard dependency.\n *\n * @example\n * ```ts\n * import { BigQuery } from \"@google-cloud/bigquery\";\n * import { BigQueryAdapter } from \"./adapters/bigquery\";\n *\n * const adapter = new BigQueryAdapter({\n * bigquery: new BigQuery({ projectId: \"my-project\" }),\n * datasetId: \"my_dataset\",\n * });\n * ```\n */\nexport class BigQueryAdapter implements SqlAdapter {\n private readonly bigquery: any;\n private readonly datasetId: string;\n\n constructor(options: { bigquery: any; datasetId: string }) {\n this.bigquery = options.bigquery;\n this.datasetId = options.datasetId;\n }\n\n /** The BigQuery SQL dialect. */\n get dialect(): SqlDialect {\n return bigqueryDialect;\n }\n\n /** Check whether a table exists in the dataset. */\n async tableExists(tableName: string): Promise<boolean> {\n const [exists] = await this.dataset.table(tableName).exists();\n return exists;\n }\n\n /** Return the column names currently present in the table. */\n async getTableColumns(tableName: string): Promise<string[]> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string }> = metadata.schema?.fields ?? [];\n return fields.map((f) => f.name);\n }\n\n /**\n * Return existing columns with their normalized BigQuery type strings.\n * Used by the worker to detect type drift before applying schema changes.\n */\n async getTableColumnsWithTypes(\n tableName: string,\n ): Promise<Map<string, string>> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string; type: string }> =\n metadata.schema?.fields ?? [];\n const result = new Map<string, string>();\n for (const f of fields) {\n result.set(f.name, normalizeBigQueryType(f.type));\n }\n return result;\n }\n\n /** Create a table using a fully-qualified name. */\n async createTable(table: SqlTableDef): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const cols = table.columns\n .map((c) => {\n const notNull = c.isPrimaryKey ? \" NOT NULL\" : \"\";\n return ` ${qi(c.name)} ${c.sqlType}${notNull}`;\n })\n .join(\",\\n\");\n\n const ddl = `CREATE TABLE IF NOT EXISTS ${this.fqn(table.tableName)} (\\n${cols}\\n);`;\n await this.bigquery.query({ query: ddl });\n }\n\n /** Add columns to an existing table using a fully-qualified name. */\n async addColumns(tableName: string, columns: SqlColumn[]): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n for (const c of columns) {\n const stmt = `ALTER TABLE ${this.fqn(tableName)} ADD COLUMN ${qi(c.name)} ${c.sqlType};`;\n await this.bigquery.query({ query: stmt });\n }\n }\n\n /** Append rows via BigQuery streaming insert. */\n async insertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n if (rows.length === 0) return;\n await this.dataset.table(tableName).insert(rows);\n }\n\n /**\n * Upsert rows using a MERGE DML statement.\n *\n * Builds a source table from inline SELECT … UNION ALL rows and merges\n * into the target on the given primary key.\n */\n async upsertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n primaryKey: string,\n ): Promise<void> {\n if (rows.length === 0) return;\n\n const allKeys = Object.keys(rows[0]!);\n const nonPkCols = allKeys.filter((k) => k !== primaryKey);\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n\n // Build inline source: SELECT val AS col, … UNION ALL SELECT …\n const selects = rows.map((row, i) => {\n const values = allKeys\n .map((k) => {\n const aliased =\n i === 0\n ? `${this.escapeValue(row[k])} AS ${qi(k)}`\n : this.escapeValue(row[k]);\n return aliased;\n })\n .join(\", \");\n return `SELECT ${values}`;\n });\n\n const source = selects.join(\" UNION ALL\\n \");\n\n // UPDATE SET clause (non-PK columns).\n // Note: when __sync_version is present we still update it so the row\n // tracks the latest applied version.\n const updateSet = nonPkCols\n .map((c) => `T.${qi(c)} = S.${qi(c)}`)\n .join(\", \");\n\n // INSERT columns / values\n const insertCols = allKeys.map((c) => qi(c)).join(\", \");\n const insertVals = allKeys.map((c) => `S.${qi(c)}`).join(\", \");\n\n // Out-of-order protection: only UPDATE when the incoming version is\n // strictly greater than the stored one (NULL stored version means the\n // row pre-dates versioning → always update).\n const versionGuard = allKeys.includes(SYNC_VERSION_COLUMN)\n ? ` AND (T.${qi(SYNC_VERSION_COLUMN)} IS NULL OR S.${qi(SYNC_VERSION_COLUMN)} > T.${qi(SYNC_VERSION_COLUMN)})`\n : \"\";\n\n const query = [\n `MERGE ${this.fqn(tableName)} AS T`,\n `USING (\\n ${source}\\n ) AS S`,\n `ON T.${qi(primaryKey)} = S.${qi(primaryKey)}`,\n `WHEN MATCHED${versionGuard} THEN UPDATE SET ${updateSet}`,\n `WHEN NOT MATCHED THEN INSERT (${insertCols}) VALUES (${insertVals});`,\n ].join(\"\\n\");\n\n await withRetry(() => this.bigquery.query({ query }));\n }\n\n /** Delete rows by primary-key values. */\n async deleteRows(\n tableName: string,\n primaryKey: string,\n ids: string[],\n ): Promise<void> {\n if (ids.length === 0) return;\n\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const escaped = ids.map((v) => this.escapeValue(v)).join(\", \");\n const query = `DELETE FROM ${this.fqn(tableName)} WHERE ${qi(primaryKey)} IN (${escaped});`;\n\n await withRetry(() => this.bigquery.query({ query }));\n }\n\n /** Execute a raw SQL statement (used by the migration manager). */\n async executeRaw(sql: string): Promise<void> {\n await this.bigquery.query({ query: sql });\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n /** The BigQuery Dataset handle. */\n private get dataset() {\n return this.bigquery.dataset(this.datasetId);\n }\n\n /** Return the fully-qualified table reference (`` `dataset.table` ``). */\n private fqn(tableName: string): string {\n return `\\`${this.datasetId}.${tableName}\\``;\n }\n\n /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */\n private static readonly ISO_TIMESTAMP_RE =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n /** Escape a value for use as a SQL literal. */\n private escapeValue(v: unknown): string {\n if (v === null || v === undefined) return \"NULL\";\n if (typeof v === \"boolean\") return v ? \"TRUE\" : \"FALSE\";\n if (typeof v === \"number\" || typeof v === \"bigint\") return String(v);\n if (typeof v === \"string\") {\n // ISO 8601 timestamps → TIMESTAMP literal (keeps type-safety with BQ TIMESTAMP columns)\n if (BigQueryAdapter.ISO_TIMESTAMP_RE.test(v)) {\n return `TIMESTAMP('${v}')`;\n }\n // Detect JSON strings (arrays/objects) → use PARSE_JSON for native JSON columns\n if (\n (v.startsWith(\"[\") && v.endsWith(\"]\")) ||\n (v.startsWith(\"{\") && v.endsWith(\"}\"))\n ) {\n return `PARSE_JSON('${v.replace(/'/g, \"\\\\'\")}')`;\n }\n return `'${v.replace(/'/g, \"\\\\'\")}'`;\n }\n // Objects / arrays → JSON\n return `PARSE_JSON('${JSON.stringify(v).replace(/'/g, \"\\\\'\")}')`;\n }\n}\n","/**\n * BigQuery **Storage Write API** adapter (CDC mode).\n *\n * Unlike {@link BigQueryAdapter} which uses MERGE/DELETE DML, this adapter\n * streams rows through the Storage Write API and lets BigQuery apply the\n * Change Data Capture semantics in the background.\n *\n * Why use this:\n *\n * - **No DML concurrency limit.** The legacy adapter is bounded by ≈ 2\n * concurrent MERGE/DELETE statements per table; busy collections trigger\n * `Could not serialize access … due to concurrent update` errors. The\n * Storage Write API has no such bound, so multiple Cloud Function\n * instances can flush in parallel without conflicts.\n * - **Cheaper at scale.** Storage Write API is roughly ~50% cheaper than\n * legacy streaming inserts, and free for the first 2 TiB / month.\n * - **Same idempotency model.** Each row carries `_CHANGE_SEQUENCE_NUMBER`\n * built from the existing `__sync_version` column, so out-of-order\n * deliveries are merged correctly by BigQuery.\n *\n * Requirements:\n *\n * - The destination tables **must** declare a `PRIMARY KEY (...) NOT\n * ENFORCED` constraint and be clustered on that key.\n * {@link createTable} handles both for tables managed by this library.\n * - Tables should be created with `OPTIONS(max_staleness = INTERVAL …)`\n * so the CDC merge runs in the background and queries see fresh data.\n * - Service account needs `bigquery.tables.updateData` (e.g. via the\n * `roles/bigquery.dataEditor` role).\n *\n * @example\n * ```ts\n * import { BigQuery } from \"@google-cloud/bigquery\";\n * import { BigQueryStorageAdapter } from \"@lpdjs/firestore-repo-service/sync/bigquery-storage\";\n *\n * const adapter = new BigQueryStorageAdapter({\n * projectId: \"my-project\",\n * datasetId: \"firestore_sync\",\n * bigquery: new BigQuery({ projectId: \"my-project\" }),\n * maxStaleness: \"INTERVAL 15 MINUTE\",\n * });\n * ```\n */\n\nimport { SYNC_VERSION_COLUMN } from \"../constants\";\nimport type {\n SqlAdapter,\n SqlColumn,\n SqlDialect,\n SqlTableDef,\n} from \"../types\";\nimport { bigqueryDialect } from \"./bigquery\";\nimport { normalizeBigQueryType } from \"./bigquery-types\";\n\n// ---------------------------------------------------------------------------\n// Types (kept loose so `@google-cloud/bigquery-storage` stays an OPTIONAL peer)\n// ---------------------------------------------------------------------------\n\ntype StorageNs = {\n managedwriter: {\n WriterClient: new (opts?: any) => any;\n JSONWriter: new (params: { connection: any; protoDescriptor: any }) => any;\n DefaultStream: any;\n };\n adapt: {\n convertBigQuerySchemaToStorageTableSchema(schema: any): any;\n convertStorageSchemaToProto2Descriptor(\n schema: any,\n scope: string,\n ...opts: any[]\n ): any;\n withChangeType(): any;\n withChangeSequenceNumber(): any;\n };\n};\n\nlet storageNsCache: StorageNs | null = null;\nfunction loadStorageNs(): StorageNs {\n if (storageNsCache) return storageNsCache;\n // Lazy require so the library does not pull `@google-cloud/bigquery-storage`\n // unless this adapter is actually instantiated.\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const mod = require(\"@google-cloud/bigquery-storage\");\n storageNsCache = mod as StorageNs;\n return storageNsCache;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format `__sync_version` (Firestore commit timestamp in microseconds since\n * epoch) as the 16-character hex string required by `_CHANGE_SEQUENCE_NUMBER`.\n *\n * BigQuery compares sequence numbers lexicographically, so a fixed-width hex\n * representation is mandatory for correct ordering.\n */\nfunction formatChangeSequenceNumber(version: unknown): string {\n let n: bigint;\n if (typeof version === \"bigint\") n = version;\n else if (typeof version === \"number\") n = BigInt(version);\n else if (typeof version === \"string\") n = BigInt(version);\n else n = 0n;\n if (n < 0n) n = 0n;\n return n.toString(16).padStart(16, \"0\");\n}\n\n/**\n * Returns true when the Storage Write append failed with a transient gRPC\n * status (UNAVAILABLE, DEADLINE_EXCEEDED, INTERNAL, ABORTED). Caller can\n * safely retry these.\n */\nfunction isRetryableStorageError(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as { code?: number };\n // gRPC codes: 4=DEADLINE_EXCEEDED, 10=ABORTED, 13=INTERNAL, 14=UNAVAILABLE\n return e.code === 4 || e.code === 10 || e.code === 13 || e.code === 14;\n}\n\nasync function withRetry<T>(\n fn: () => Promise<T>,\n maxRetries = 6,\n baseMs = 200,\n): Promise<T> {\n let attempt = 0;\n // eslint-disable-next-line no-constant-condition\n while (true) {\n try {\n return await fn();\n } catch (err) {\n attempt++;\n if (!isRetryableStorageError(err) || attempt > maxRetries) throw err;\n const cap = baseMs * Math.pow(2, attempt);\n const delay = Math.random() * cap;\n await new Promise((res) => setTimeout(res, delay));\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Adapter\n// ---------------------------------------------------------------------------\n\nexport interface BigQueryStorageAdapterOptions {\n /** GCP project id that owns the dataset. */\n projectId: string;\n /** BigQuery dataset id. */\n datasetId: string;\n /**\n * BigQuery client used for DDL operations (createTable, addColumns,\n * getMetadata, executeRaw). Storage Write only handles inserts.\n */\n bigquery: any;\n /**\n * Optional pre-built `WriterClient`. When omitted a new one is created on\n * first use.\n */\n writerClient?: any;\n /**\n * Value passed to `OPTIONS(max_staleness = ...)` on `createTable`.\n * Recommended: `INTERVAL 15 MINUTE` in production, `INTERVAL 1 MINUTE`\n * in development. Set to `null` to omit (BigQuery defaults to 0, which\n * forces a merge on every read and is **not** what you want).\n * @default \"INTERVAL 15 MINUTE\"\n */\n maxStaleness?: string | null;\n}\n\n/**\n * Storage Write API implementation of {@link SqlAdapter}.\n */\nexport class BigQueryStorageAdapter implements SqlAdapter {\n private readonly bigquery: any;\n private readonly projectId: string;\n private readonly datasetId: string;\n private readonly maxStaleness: string | null;\n private writerClient: any;\n /** Cache of `{ writer, primaryKey }` per table name. */\n private readonly writers = new Map<\n string,\n { writer: any; primaryKey: string }\n >();\n\n constructor(options: BigQueryStorageAdapterOptions) {\n this.bigquery = options.bigquery;\n this.projectId = options.projectId;\n this.datasetId = options.datasetId;\n this.maxStaleness =\n options.maxStaleness === undefined\n ? \"INTERVAL 15 MINUTE\"\n : options.maxStaleness;\n this.writerClient = options.writerClient;\n }\n\n get dialect(): SqlDialect {\n return bigqueryDialect;\n }\n\n // -------- DDL (delegated to @google-cloud/bigquery) ----------------------\n\n async tableExists(tableName: string): Promise<boolean> {\n const [exists] = await this.dataset.table(tableName).exists();\n return exists;\n }\n\n async getTableColumns(tableName: string): Promise<string[]> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string }> = metadata.schema?.fields ?? [];\n return fields.map((f) => f.name);\n }\n\n async getTableColumnsWithTypes(\n tableName: string,\n ): Promise<Map<string, string>> {\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const fields: Array<{ name: string; type: string }> =\n metadata.schema?.fields ?? [];\n const result = new Map<string, string>();\n for (const f of fields) {\n result.set(f.name, normalizeBigQueryType(f.type));\n }\n return result;\n }\n\n async createTable(table: SqlTableDef): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n const pk = table.columns.find((c) => c.isPrimaryKey)?.name;\n if (!pk) {\n throw new Error(\n `BigQueryStorageAdapter requires a primary key on table \\`${table.tableName}\\` ` +\n `(Storage Write CDC mode needs PRIMARY KEY NOT ENFORCED).`,\n );\n }\n const cols = table.columns\n .map((c) => {\n const notNull = c.isPrimaryKey ? \" NOT NULL\" : \"\";\n return ` ${qi(c.name)} ${c.sqlType}${notNull}`;\n })\n .join(\",\\n\");\n\n const opts: string[] = [];\n if (this.maxStaleness !== null) {\n opts.push(`max_staleness = ${this.maxStaleness}`);\n }\n const optionsClause =\n opts.length > 0 ? `\\nOPTIONS(${opts.join(\", \")})` : \"\";\n\n const ddl =\n `CREATE TABLE IF NOT EXISTS ${this.fqn(table.tableName)} (\\n${cols},\\n` +\n ` PRIMARY KEY (${qi(pk)}) NOT ENFORCED\\n)` +\n `\\nCLUSTER BY ${qi(pk)}` +\n `${optionsClause};`;\n\n await this.bigquery.query({ query: ddl });\n }\n\n async addColumns(tableName: string, columns: SqlColumn[]): Promise<void> {\n const qi = (id: string) => this.dialect.quoteIdentifier(id);\n for (const c of columns) {\n const stmt = `ALTER TABLE ${this.fqn(tableName)} ADD COLUMN ${qi(c.name)} ${c.sqlType};`;\n await this.bigquery.query({ query: stmt });\n }\n }\n\n async executeRaw(sql: string): Promise<void> {\n await this.bigquery.query({ query: sql });\n }\n\n /**\n * Invalidate the cached writer for a table. Called by the worker after\n * `addColumns` so the next append rebuilds the proto descriptor against\n * the new schema.\n */\n onSchemaChange(tableName: string): void {\n const cached = this.writers.get(tableName);\n if (cached) {\n try {\n cached.writer.close();\n } catch {\n // ignore — connection may already be torn down\n }\n this.writers.delete(tableName);\n }\n }\n\n // -------- Inserts (Storage Write API) -----------------------------------\n\n async insertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n if (rows.length === 0) return;\n // Plain inserts have no PK matching, but in CDC mode every row needs a\n // _CHANGE_TYPE. We treat them as UPSERT.\n const writer = await this.getOrCreateWriter(tableName);\n const cdc = rows.map((row) => ({\n ...this.normalizeRow(row),\n _CHANGE_TYPE: \"UPSERT\",\n _CHANGE_SEQUENCE_NUMBER: formatChangeSequenceNumber(\n row[SYNC_VERSION_COLUMN],\n ),\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n async upsertRows(\n tableName: string,\n rows: Record<string, unknown>[],\n primaryKey: string,\n ): Promise<void> {\n if (rows.length === 0) return;\n const writer = await this.getOrCreateWriter(tableName, primaryKey);\n const cdc = rows.map((row) => ({\n ...this.normalizeRow(row),\n _CHANGE_TYPE: \"UPSERT\",\n _CHANGE_SEQUENCE_NUMBER: formatChangeSequenceNumber(\n row[SYNC_VERSION_COLUMN],\n ),\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n async deleteRows(\n tableName: string,\n primaryKey: string,\n ids: string[],\n ): Promise<void> {\n if (ids.length === 0) return;\n const writer = await this.getOrCreateWriter(tableName, primaryKey);\n // Storage Write CDC requires a value for every NOT-NULL column (the PK)\n // and a `_CHANGE_TYPE`. Other columns may be omitted; missing values are\n // interpreted as NULL by default.\n const cdc = ids.map((id) => ({\n [primaryKey]: id,\n _CHANGE_TYPE: \"DELETE\",\n // No reliable version on plain deletes (the queue does not carry one\n // for tombstones), so we pass an all-zero sequence number meaning\n // \"apply only if newer than every previous UPSERT\" only when explicit\n // versioning is enabled. For tombstones we instead use the maximum\n // value to make sure the DELETE wins over any concurrent UPSERT for\n // the same key.\n _CHANGE_SEQUENCE_NUMBER: \"ffffffffffffffff\",\n }));\n await this.appendWithRetry(tableName, writer, cdc);\n }\n\n // -------- Internal helpers ----------------------------------------------\n\n private get dataset() {\n return this.bigquery.dataset(this.datasetId);\n }\n\n private fqn(tableName: string): string {\n return `\\`${this.datasetId}.${tableName}\\``;\n }\n\n /** ISO 8601 timestamp pattern (e.g. 2026-03-29T20:59:27.394Z) */\n private static readonly ISO_TIMESTAMP_RE =\n /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/;\n\n /**\n * Convert a Date or epoch-millis number into the epoch-microseconds string\n * required by the Storage Write API for TIMESTAMP columns.\n *\n * Unlike the legacy BigQueryAdapter (which builds SQL `TIMESTAMP('iso')`\n * literals), the JSONWriter encodes through protobuf and a TIMESTAMP field\n * is an `int64`. Passing an ISO string would make `Long.fromString` throw\n * `interior hyphen` on the `-` characters.\n */\n private static toEpochMicros(d: Date): string {\n const ms = d.getTime();\n return (BigInt(ms) * 1000n).toString();\n }\n\n /** Convert JS values into shapes accepted by JSONWriter. */\n private normalizeRow(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(row)) {\n if (v === undefined) continue;\n if (v instanceof Date) {\n // TIMESTAMP column → int64 epoch micros (as string to preserve precision)\n out[k] = BigQueryStorageAdapter.toEpochMicros(v);\n } else if (\n typeof v === \"string\" &&\n BigQueryStorageAdapter.ISO_TIMESTAMP_RE.test(v)\n ) {\n // ISO timestamp produced upstream by `serializeDocument` (Firestore\n // Timestamp → ISO string) — convert to epoch micros for protobuf.\n const d = new Date(v);\n if (!Number.isNaN(d.getTime())) {\n out[k] = BigQueryStorageAdapter.toEpochMicros(d);\n } else {\n out[k] = v;\n }\n } else if (typeof v === \"object\" && v !== null) {\n // JSON columns: serialise nested objects/arrays\n out[k] = JSON.stringify(v);\n } else if (typeof v === \"bigint\") {\n out[k] = v.toString();\n } else {\n out[k] = v;\n }\n }\n return out;\n }\n\n private async appendWithRetry(\n tableName: string,\n writer: any,\n rows: Record<string, unknown>[],\n ): Promise<void> {\n await withRetry(async () => {\n try {\n const pending = writer.appendRows(rows);\n await pending.getResult();\n } catch (err) {\n // On certain errors (schema drift, broken connection) drop the\n // cached writer so the next call rebuilds it.\n this.onSchemaChange(tableName);\n throw err;\n }\n });\n }\n\n private async getOrCreateWriter(\n tableName: string,\n primaryKey?: string,\n ): Promise<any> {\n const cached = this.writers.get(tableName);\n if (cached) {\n if (primaryKey && cached.primaryKey !== primaryKey) {\n // PK changed — invalidate and rebuild\n this.onSchemaChange(tableName);\n } else {\n return cached.writer;\n }\n }\n\n const ns = loadStorageNs();\n if (!this.writerClient) {\n this.writerClient = new ns.managedwriter.WriterClient({\n projectId: this.projectId,\n });\n }\n\n const [metadata] = await this.dataset.table(tableName).getMetadata();\n const bqSchema = { fields: metadata.schema?.fields ?? [] };\n if (!primaryKey) {\n // Try to recover PK from table constraints if available\n const tableConstraintsPK =\n metadata.tableConstraints?.primaryKey?.columns?.[0];\n primaryKey = tableConstraintsPK ?? bqSchema.fields[0]?.name;\n }\n\n const storageSchema =\n ns.adapt.convertBigQuerySchemaToStorageTableSchema(bqSchema);\n const protoDescriptor = ns.adapt.convertStorageSchemaToProto2Descriptor(\n storageSchema,\n \"Row\",\n ns.adapt.withChangeType(),\n ns.adapt.withChangeSequenceNumber(),\n );\n\n const destinationTable = `projects/${this.projectId}/datasets/${this.datasetId}/tables/${tableName}`;\n // Pass `DefaultStream` as `streamId` (not `streamType`): the `_default`\n // stream is implicit on every table, so the client must short-circuit to\n // `${destinationTable}/streams/_default` instead of calling\n // `createWriteStream({ type: DEFAULT })` which BigQuery rejects with\n // `Unable to create a stream with type TYPE_UNSPECIFIED`.\n const connection = await this.writerClient.createStreamConnection({\n streamId: ns.managedwriter.DefaultStream,\n destinationTable,\n });\n\n const writer = new ns.managedwriter.JSONWriter({\n connection,\n protoDescriptor,\n });\n\n this.writers.set(tableName, { writer, primaryKey: primaryKey ?? \"\" });\n return writer;\n }\n\n /** Close all open writer connections. Useful for graceful shutdown. */\n async close(): Promise<void> {\n for (const { writer } of this.writers.values()) {\n try {\n writer.close();\n } catch {\n /* ignore */\n }\n }\n this.writers.clear();\n if (this.writerClient && typeof this.writerClient.close === \"function\") {\n try {\n this.writerClient.close();\n } catch {\n /* ignore */\n }\n }\n }\n}\n"]}