1import { Action } from "@/Action";
2import * as GCP from "@/GCP";
3import * as Test from "@/Test/Alchemy";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16/** JSON-safe view of a decoded row so Action output round-trips state. */
17const describeRow = (
18 row: Record<string, unknown>,
19): Record<string, unknown> => ({
20 ...row,
21 at: row.at instanceof Date ? row.at.toISOString() : row.at,
22 payload: row.payload instanceof Uint8Array ? [...row.payload] : row.payload,
23});
24
25test.provider(
26 "ReadTable, WriteTable, and ReadWriteTable clients",
27 (stack) =>
28 Effect.gen(function* () {
29 yield* stack.destroy();
30
31 const out = yield* stack.deploy(
32 Effect.gen(function* () {
33 const dataset = yield* GCP.BigQuery.Dataset("Access", {
34 location: "US-CENTRAL1",
35 forceDestroy: true,
36 });
37 const table = yield* GCP.BigQuery.Table("Scores", {
38 datasetId: dataset.datasetId,
39 schema: [
40 { name: "id", type: "STRING", mode: "REQUIRED" },
41 { name: "score", type: "INTEGER" },
42 { name: "ratio", type: "FLOAT" },
43 { name: "ok", type: "BOOLEAN" },
44 { name: "at", type: "TIMESTAMP" },
45 { name: "payload", type: "BYTES" },
46 { name: "tags", type: "STRING", mode: "REPEATED" },
47 {
48 name: "meta",
49 type: "RECORD",
50 fields: [{ name: "k", type: "STRING" }],
51 },
52 ],
53 });
54 const Probe = Action(
55 "Probe",
56 Effect.gen(function* () {
57 const tableId = yield* table.tableId;
58 const reader = yield* GCP.BigQuery.ReadTable(table);
59 const writer = yield* GCP.BigQuery.WriteTable(table);
60 const both = yield* GCP.BigQuery.ReadWriteTable(table);
61 return Effect.fn(function* () {
62 yield* writer.insert(
63 [
64 {
65 id: "a",
66 score: 5,
67 ratio: 0.5,
68 ok: true,
69 at: new Date("2026-01-02T03:04:05.678Z"),
70 payload: new Uint8Array([0, 1, 255]),
71 tags: ["x", "y"],
72 meta: { k: "v" },
73 },
74 { id: "b", score: 20, tags: [] },
75 ],
76 { insertIds: ["a", "b"] },
77 );
78 yield* both.insert([{ id: "c", score: 30, ok: false }]);
79 const rejected = yield* writer
80 .insert([{ id: "bad", score: "not-a-number" }])
81 .pipe(
82 Effect.map(() => "inserted"),
83 Effect.catchTag("GCP.BigQuery.InsertRowsFailed", (error) =>
84 Effect.succeed(
85 `${error.insertErrors.length}:${error.insertErrors[0]?.index}`,
86 ),
87 ),
88 );
89
90 // Streaming-buffer rows become readable shortly after insert.
91 const listed = yield* reader.list().pipe(
92 Effect.repeat({
93 schedule: Schedule.spaced("2 seconds"),
94 until: (page) => page.rows.length >= 3,
95 times: 30,
96 }),
97 );
98 const page = yield* both.list({ maxResults: 1 });
99
100 const sql = `SELECT id, score FROM \`${yield* tableId}\` WHERE score >= @min AND id != @skip ORDER BY id`;
101 const queried = yield* reader
102 .query(sql, { min: 10, skip: "none" })
103 .pipe(
104 Effect.repeat({
105 schedule: Schedule.spaced("2 seconds"),
106 until: (rows) => rows.length >= 2,
107 times: 30,
108 }),
109 );
110 const typed = yield* both.query(
111 "SELECT @n AS n, @f AS f, @b AS b, @s AS s, CURRENT_TIMESTAMP() IS NOT NULL AS now",
112 { n: 7, f: 1.5, b: true, s: "str" },
113 );
114
115 return {
116 rejected,
117 rows: listed.rows
118 .map(describeRow)
119 .sort((l, r) => String(l.id).localeCompare(String(r.id))),
120 page: page.rows.length,
121 hasNextPage: page.nextPageToken !== undefined,
122 queried,
123 typed,
124 };
125 });
126 }).pipe(
127 Effect.provide(GCP.BigQuery.ReadTableHttp),
128 Effect.provide(GCP.BigQuery.WriteTableHttp),
129 Effect.provide(GCP.BigQuery.ReadWriteTableHttp),
130 ),
131 );
132 return { probe: yield* Probe({}) };
133 }),
134 );
135
136 expect(out.probe).toEqual({
137 rejected: "1:0",
138 rows: [
139 {
140 id: "a",
141 score: 5,
142 ratio: 0.5,
143 ok: true,
144 at: "2026-01-02T03:04:05.678Z",
145 payload: [0, 1, 255],
146 tags: ["x", "y"],
147 meta: { k: "v" },
148 },
149 {
150 id: "b",
151 score: 20,
152 ratio: null,
153 ok: null,
154 at: null,
155 payload: null,
156 tags: [],
157 meta: null,
158 },
159 {
160 id: "c",
161 score: 30,
162 ratio: null,
163 ok: false,
164 at: null,
165 payload: null,
166 tags: [],
167 meta: null,
168 },
169 ],
170 page: 1,
171 hasNextPage: true,
172 queried: [
173 { id: "b", score: 20 },
174 { id: "c", score: 30 },
175 ],
176 typed: [{ n: 7, f: 1.5, b: true, s: "str", now: true }],
177 });
178
179 yield* stack.destroy();
180 }).pipe(logLevel),
181 { tags: ["provider:gcp", "provider:gcp:bigquery", "live"], timeout: 240_000 },
182);