1import * as athena from "@distilled.cloud/aws/athena";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import { createPhysicalName } from "../../PhysicalName.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import type { Providers } from "../Providers.ts";
9
10export interface NamedQueryProps {
11 /**
12 * Display name of the saved query. If omitted, a unique name is generated.
13 * Up to 128 characters.
14 */
15 name?: string;
16 /**
17 * The SQL text of the saved query.
18 */
19 queryString: string;
20 /**
21 * Glue/Athena database the query runs against. Changing this replaces the
22 * named query (Athena cannot re-point a saved query's database in place).
23 */
24 database: string;
25 /**
26 * Optional human-readable description.
27 */
28 description?: string;
29 /**
30 * Workgroup the query is saved in. Changing this replaces the named query.
31 * @default "primary"
32 */
33 workGroup?: string;
34}
35
36export interface NamedQuery extends Resource<
37 "AWS.Athena.NamedQuery",
38 NamedQueryProps,
39 {
40 /**
41 * Unique ID of the named query.
42 */
43 namedQueryId: string;
44 /**
45 * Name of the saved query.
46 */
47 name: string;
48 /**
49 * The saved SQL statement.
50 */
51 queryString: string;
52 /**
53 * Database the query runs against.
54 */
55 database: string;
56 /**
57 * Description of the query.
58 */
59 description: string | undefined;
60 /**
61 * Workgroup the query is saved in.
62 */
63 workGroup: string;
64 },
65 never,
66 Providers
67> {}
68
69/**
70 * An Amazon Athena named (saved) query — a reusable SQL statement stored
71 * against a Glue database inside a workgroup. Its identity is the
72 * server-assigned `NamedQueryId`; the `name`, `description`, and `queryString`
73 * are updatable in place, while changing `database` or `workGroup` replaces it.
74 *
75 * ### Saving Queries
76 * **Example:** Save a query against a Glue database
77 * ```typescript
78 * const query = yield* AWS.Athena.NamedQuery("TopCustomers", {
79 * database: "analytics",
80 * queryString: "SELECT customer_id, SUM(amount) AS total " +
81 * "FROM orders GROUP BY customer_id ORDER BY total DESC LIMIT 10",
82 * workGroup: wg.workGroupName,
83 * });
84 * ```
85 *
86 * @resource
87 */
88export const NamedQuery = Resource<NamedQuery>("AWS.Athena.NamedQuery");
89
90const toAttributes = (nq: athena.NamedQuery) => ({
91 namedQueryId: nq.NamedQueryId!,
92 name: nq.Name,
93 queryString: nq.QueryString,
94 database: nq.Database,
95 description: nq.Description,
96 workGroup: nq.WorkGroup ?? "primary",
97});
98
99export const NamedQueryProvider = () =>
100 Provider.effect(
101 NamedQuery,
102 Effect.gen(function* () {
103 const toName = (id: string, props: NamedQueryProps) =>
104 props.name
105 ? Effect.succeed(props.name)
106 : createPhysicalName({ id, maxLength: 128 });
107
108 // Deterministic per-instance idempotency token so a create that crashes
109 // before state persists re-returns the SAME NamedQueryId on re-run
110 // instead of creating a duplicate saved query.
111 const toToken = (id: string) =>
112 createPhysicalName({ id, maxLength: 90 }).pipe(
113 Effect.map((n) => `${n}-alchemy-namedquery-idempotency`),
114 );
115
116 const getOne = (namedQueryId: string) =>
117 athena.getNamedQuery({ NamedQueryId: namedQueryId }).pipe(
118 Effect.map((res) => res.NamedQuery),
119 Effect.catchTag("NamedQueryNotFound", () =>
120 Effect.succeed(undefined),
121 ),
122 );
123
124 return {
125 stables: ["namedQueryId"],
126 diff: Effect.fn(function* ({ olds, news }) {
127 if (!isResolved(news)) return;
128 if ((olds?.database ?? undefined) !== news.database) {
129 return { action: "replace" } as const;
130 }
131 if (
132 (olds?.workGroup ?? "primary") !== (news.workGroup ?? "primary")
133 ) {
134 return { action: "replace" } as const;
135 }
136 }),
137 read: Effect.fn(function* ({ output }) {
138 // Named queries have no deterministic name-based lookup — identity is
139 // the server-assigned id. Without a cached id there's nothing to read.
140 if (!output?.namedQueryId) return undefined;
141 const nq = yield* getOne(output.namedQueryId);
142 return nq ? toAttributes(nq) : undefined;
143 }),
144 list: () =>
145 Effect.gen(function* () {
146 const wgPages = yield* athena.listWorkGroups
147 .pages({})
148 .pipe(Stream.runCollect);
149 const workGroups = Array.from(wgPages)
150 .flatMap((page) => page.WorkGroups ?? [])
151 .flatMap((wg) => (wg.Name ? [wg.Name] : []));
152 const attrs: ReturnType<typeof toAttributes>[] = [];
153 for (const wg of workGroups) {
154 const idPages = yield* athena.listNamedQueries
155 .pages({ WorkGroup: wg })
156 .pipe(Stream.runCollect);
157 const ids = Array.from(idPages).flatMap(
158 (page) => page.NamedQueryIds ?? [],
159 );
160 for (let i = 0; i < ids.length; i += 50) {
161 const res = yield* athena.batchGetNamedQuery({
162 NamedQueryIds: ids.slice(i, i + 50),
163 });
164 for (const nq of res.NamedQueries ?? []) {
165 if (nq.NamedQueryId) attrs.push(toAttributes(nq));
166 }
167 }
168 }
169 return attrs;
170 }),
171 reconcile: Effect.fn(function* ({ id, news, output, session }) {
172 const name = yield* toName(id, news);
173 const workGroup = news.workGroup ?? "primary";
174
175 // Observe — cloud state (by cached id) is authoritative.
176 let nq = output?.namedQueryId
177 ? yield* getOne(output.namedQueryId)
178 : undefined;
179
180 if (nq) {
181 // Sync — Name/Description/QueryString are updatable in place.
182 if (
183 nq.Name !== name ||
184 nq.Description !== news.description ||
185 nq.QueryString !== news.queryString
186 ) {
187 yield* athena.updateNamedQuery({
188 NamedQueryId: nq.NamedQueryId!,
189 Name: name,
190 Description: news.description,
191 QueryString: news.queryString,
192 });
193 }
194 } else {
195 // Ensure — create with a deterministic idempotency token.
196 const token = yield* toToken(id);
197 const created = yield* athena.createNamedQuery({
198 Name: name,
199 Description: news.description,
200 Database: news.database,
201 QueryString: news.queryString,
202 WorkGroup: workGroup,
203 ClientRequestToken: token,
204 });
205 nq = yield* getOne(created.NamedQueryId!);
206 }
207
208 if (!nq?.NamedQueryId) {
209 return yield* Effect.die(
210 new Error(`NamedQuery ${name} did not materialize`),
211 );
212 }
213
214 yield* session.note(nq.NamedQueryId);
215 return toAttributes(nq);
216 }),
217 delete: Effect.fn(function* ({ output }) {
218 yield* athena
219 .deleteNamedQuery({ NamedQueryId: output.namedQueryId })
220 .pipe(Effect.catchTag("NamedQueryNotFound", () => Effect.void));
221 }),
222 };
223 }),
224 );