1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as data from "@distilled.cloud/aws/redshift-data";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import RedshiftDataApiFunctionLive, {
10 RedshiftDataApiFunction,
11} from "./fixtures/data-api-handler";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15const sharedStack = Core.scratchStack(testOptions, "RedshiftDataClient");
16
17// ---------------------------------------------------------------------------
18// Ungated typed-error probes: prove the distilled redshift-data error union
19// carries the tags the Statements client depends on, at near-zero cost.
20// ---------------------------------------------------------------------------
21
22test.provider(
23 "describeStatement on a nonexistent statement fails with a typed error",
24 () =>
25 Effect.gen(function* () {
26 const error = yield* Effect.flip(
27 data.describeStatement({
28 Id: "d9b6c0c9-0747-4bf4-b142-e8883122f766",
29 }),
30 );
31 expect(["ResourceNotFoundException", "ValidationException"]).toContain(
32 error._tag,
33 );
34 }),
35 { tags: ["provider:aws", "provider:aws:redshiftdata", "live"] },
36);
37
38test.provider(
39 "listDatabases against a nonexistent workgroup fails with a typed error",
40 () =>
41 Effect.gen(function* () {
42 const error = yield* Effect.flip(
43 data.listDatabases({
44 Database: "dev",
45 WorkgroupName: "alchemy-test-rsd-does-not-exist",
46 }),
47 );
48 expect([
49 "ValidationException",
50 "ResourceNotFoundException",
51 "DatabaseConnectionException",
52 ]).toContain(error._tag);
53 }),
54 { tags: ["provider:aws", "provider:aws:redshiftdata", "live"] },
55);
56
57// ---------------------------------------------------------------------------
58// Full live suite: deploys a Redshift Serverless namespace + workgroup (a
59// real-money RPU floor while it exists) plus a Lambda, so it is gated behind
60// AWS_TEST_SLOW=1 and destroys everything in afterAll. A run without the env
61// var is skip-clean: no AWS calls, no side effects.
62// ---------------------------------------------------------------------------
63
64let baseUrl: string;
65
66const get = (route: string) =>
67 HttpClient.get(`${baseUrl}${route}`).pipe(
68 Effect.flatMap((res) =>
69 res.status === 200
70 ? Effect.succeed(res)
71 : res.text.pipe(
72 Effect.flatMap((body) =>
73 Effect.fail(
74 new Error(`${route} returned ${res.status}: ${body}`),
75 ),
76 ),
77 ),
78 ),
79 Effect.retry({
80 schedule: Schedule.max([
81 Schedule.exponential("1 second"),
82 Schedule.recurs(8),
83 ]),
84 }),
85 Effect.flatMap((res) => res.json),
86 );
87
88describe.skipIf(!process.env.AWS_TEST_SLOW)(
89 "RedshiftData client",
90 {
91 tags: [
92 "provider:aws",
93 "provider:aws:lambda",
94 "provider:aws:redshiftdata",
95 "provider:aws:redshiftserverless",
96 "live",
97 ],
98 },
99 () => {
100 beforeAll(
101 Effect.gen(function* () {
102 yield* Effect.logInfo(
103 "RedshiftData client setup: destroying previous run",
104 );
105 yield* sharedStack.destroy();
106
107 yield* Effect.logInfo("RedshiftData client setup: deploying fixture");
108 const { functionUrl } = yield* sharedStack.deploy(
109 Effect.gen(function* () {
110 return yield* RedshiftDataApiFunction;
111 }).pipe(Effect.provide(RedshiftDataApiFunctionLive)),
112 );
113
114 expect(functionUrl).toBeTruthy();
115 baseUrl = functionUrl!.replace(/\/+$/, "");
116 yield* Effect.logInfo(
117 `RedshiftData client setup: function URL ready (${functionUrl})`,
118 );
119 }),
120 // namespace (~1 min) + workgroup create (~2-5 min) + Lambda deploy.
121 { timeout: 900_000 },
122 );
123
124 afterAll(sharedStack.destroy(), { timeout: 600_000 });
125
126 test.provider(
127 "query: execute + describe + getResult round-trip",
128 (_stack) =>
129 Effect.gen(function* () {
130 const body = (yield* get("/query")) as {
131 columns: (string | undefined)[];
132 records: { longValue?: number }[][];
133 totalNumRows: number;
134 };
135 expect(body.records).toHaveLength(1);
136 expect(body.records[0][0]).toMatchObject({ longValue: 1 });
137 }),
138 { timeout: 240_000 },
139 );
140
141 test.provider(
142 "executeBatch: runs sub-statements and fetches a sub-statement result",
143 (_stack) =>
144 Effect.gen(function* () {
145 const body = (yield* get("/batch")) as {
146 status: string;
147 subStatementCount: number;
148 secondRecords: { longValue?: number }[][] | undefined;
149 };
150 expect(body.status).toBe("FINISHED");
151 expect(body.subStatementCount).toBe(2);
152 expect(body.secondRecords?.[0]?.[0]).toMatchObject({ longValue: 2 });
153 }),
154 { timeout: 240_000 },
155 );
156
157 test.provider(
158 "metadata: listDatabases + listSchemas + listTables + describeTable",
159 (_stack) =>
160 Effect.gen(function* () {
161 const body = (yield* get("/metadata")) as {
162 databases: string[];
163 schemas: string[];
164 tables: string[];
165 columnCount: number;
166 };
167 expect(body.databases).toContain("dev");
168 expect(body.schemas).toContain("pg_catalog");
169 expect(body.tables).toContain("pg_class");
170 expect(body.columnCount).toBeGreaterThan(0);
171 }),
172 { timeout: 240_000 },
173 );
174
175 test.provider(
176 "listStatements: sees the caller's submitted statement",
177 (_stack) =>
178 Effect.gen(function* () {
179 const body = (yield* get("/statements")) as {
180 count: number;
181 hasSubmitted: boolean;
182 };
183 expect(body.count).toBeGreaterThan(0);
184 expect(body.hasSubmitted).toBe(true);
185 }),
186 { timeout: 240_000 },
187 );
188
189 test.provider(
190 "cancel: cancellation request round-trips",
191 (_stack) =>
192 Effect.gen(function* () {
193 const body = (yield* get("/cancel")) as { canceled: boolean };
194 // Cancellation races completion; either outcome proves the wiring.
195 expect(typeof body.canceled).toBe("boolean");
196 }),
197 { timeout: 240_000 },
198 );
199
200 test.provider(
201 "getResultV2: CSV result format round-trips",
202 (_stack) =>
203 Effect.gen(function* () {
204 const body = (yield* get("/result-v2")) as {
205 status: string;
206 resultFormat: string | undefined;
207 records: { CSVRecords?: string }[];
208 };
209 expect(body.status).toBe("FINISHED");
210 expect(body.records.length).toBeGreaterThan(0);
211 expect(body.records[0]?.CSVRecords).toContain("7");
212 }),
213 { timeout: 240_000 },
214 );
215 },
216);