1import type * as bigquery from "@distilled.cloud/gcp/bigquery_v2";
2
3/**
4 * Codec between BigQuery REST row payloads and plain JavaScript rows.
5 * NOT exported from `index.ts`.
6 *
7 * Decoding (driven by the table / result schema, with
8 * `formatOptions.useInt64Timestamp` requested): `INTEGER` → `number`
9 * (`bigint` beyond the safe range), `FLOAT` → `number`, `BOOLEAN` →
10 * `boolean`, `TIMESTAMP` → `Date`, `BYTES` → `Uint8Array`, `JSON` → parsed
11 * value, `RECORD` → object, `REPEATED` → array, `NULL` → `null`. `NUMERIC`,
12 * `BIGNUMERIC`, `DATE`, `TIME`, `DATETIME`, `GEOGRAPHY`, and everything else
13 * stay strings.
14 */
15
16const toBase64 = (bytes: Uint8Array): string => {
17 let binary = "";
18 for (let i = 0; i < bytes.length; i++) {
19 binary += String.fromCharCode(bytes[i]!);
20 }
21 return btoa(binary);
22};
23
24const fromBase64 = (value: string): Uint8Array => {
25 const binary = atob(value);
26 const bytes = new Uint8Array(binary.length);
27 for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
28 return bytes;
29};
30
31const toInteger = (value: string): number | bigint => {
32 const big = BigInt(value);
33 return big >= BigInt(Number.MIN_SAFE_INTEGER) &&
34 big <= BigInt(Number.MAX_SAFE_INTEGER)
35 ? Number(big)
36 : big;
37};
38
39const isCell = (value: unknown): value is { v?: unknown } =>
40 typeof value === "object" && value !== null && "v" in value;
41
42const decodeScalar = (
43 field: bigquery.TableFieldSchema,
44 value: unknown,
45): unknown => {
46 if (value === null || value === undefined) return null;
47 switch ((field.type ?? "STRING").toUpperCase()) {
48 case "RECORD":
49 case "STRUCT":
50 return decodeRow(
51 field.fields ?? [],
52 (value as { f?: bigquery.TableCell[] }).f,
53 );
54 }
55 if (typeof value !== "string") return value;
56 switch ((field.type ?? "STRING").toUpperCase()) {
57 case "INTEGER":
58 case "INT64":
59 return toInteger(value);
60 case "FLOAT":
61 case "FLOAT64":
62 return Number(value);
63 case "BOOLEAN":
64 case "BOOL":
65 return value === "true";
66 case "TIMESTAMP":
67 // Int64 microseconds since the epoch (useInt64Timestamp); fall back
68 // to the float-seconds wire format.
69 return /^-?\d+$/.test(value)
70 ? new Date(Number(BigInt(value) / BigInt(1000)))
71 : new Date(Number(value) * 1000);
72 case "BYTES":
73 return fromBase64(value);
74 case "JSON":
75 return JSON.parse(value);
76 default:
77 return value;
78 }
79};
80
81const decodeCell = (field: bigquery.TableFieldSchema, value: unknown) =>
82 (field.mode ?? "").toUpperCase() === "REPEATED"
83 ? (Array.isArray(value) ? value : []).map((item) =>
84 decodeScalar(field, isCell(item) ? item.v : item),
85 )
86 : decodeScalar(field, value);
87
88export const decodeRow = (
89 fields: ReadonlyArray<bigquery.TableFieldSchema>,
90 cells: ReadonlyArray<bigquery.TableCell> | undefined,
91): Record<string, unknown> => {
92 const row: Record<string, unknown> = {};
93 fields.forEach((field, index) => {
94 row[field.name ?? `f${index}`] = decodeCell(field, cells?.[index]?.v);
95 });
96 return row;
97};
98
99export const decodeRows = (
100 schema: bigquery.TableSchema | undefined,
101 rows: ReadonlyArray<bigquery.TableRow> | undefined,
102): Record<string, unknown>[] =>
103 (rows ?? []).map((row) => decodeRow(schema?.fields ?? [], row.f));
104
105/** Encode a plain JS value for `tabledata.insertAll` JSON rows. */
106export const encodeJson = (value: unknown): unknown => {
107 if (value === undefined || value === null) return null;
108 if (typeof value === "bigint") return value.toString();
109 if (value instanceof Date) return value.toISOString();
110 if (value instanceof Uint8Array) return toBase64(value);
111 if (Array.isArray(value)) return value.map(encodeJson);
112 if (typeof value === "object") {
113 const out: Record<string, unknown> = {};
114 for (const [key, item] of Object.entries(value)) {
115 if (item !== undefined) out[key] = encodeJson(item);
116 }
117 return out;
118 }
119 return value;
120};
121
122/** Encode one row for `tabledata.insertAll`; `undefined` keys are dropped. */
123export const encodeRow = (
124 row: Record<string, unknown>,
125): Record<string, unknown> => {
126 const out: Record<string, unknown> = {};
127 for (const [key, value] of Object.entries(row)) {
128 if (value !== undefined) out[key] = encodeJson(value);
129 }
130 return out;
131};
132
133/** Named query parameter for a scalar JS value. */
134export const queryParameter = (
135 name: string,
136 value: string | number | boolean,
137): bigquery.QueryParameter => ({
138 name,
139 parameterType: {
140 type:
141 typeof value === "boolean"
142 ? "BOOL"
143 : typeof value === "number"
144 ? Number.isSafeInteger(value)
145 ? "INT64"
146 : "FLOAT64"
147 : "STRING",
148 },
149 parameterValue: { value: String(value) },
150});