1import * as biglake from "@distilled.cloud/gcp/biglake_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { tagRecord } from "../../Tags.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 BiglakeNotResolved,
12 listLocations,
13 createInternalLabels,
14 expandCatalog,
15 hasAlchemyLabelMap,
16 hasAlchemyLabels,
17 ignoreGone,
18 listCatalogs,
19 listChildResources,
20 listDatabases,
21 listTables,
22 locationParent,
23 mergeParameters,
24 missingGet,
25 namedOf,
26 normalizeLocation,
27 parseResourceName,
28 replaceOnIdentity,
29 retryTransient,
30 sameJson,
31 sameText,
32 toPhysicalId,
33 updateMaskOf,
34 userLabels,
35 waitUntilGone,
36} from "./internal.ts";
37
38export type HiveDatabaseOptions = {
39 /**
40 * Cloud Storage folder URI where the database data is stored, starting
41 * with `gs://`.
42 */
43 locationUri?: string;
44 /**
45 * User-supplied Hive database parameters. Alchemy ownership keys
46 * (`alchemy-stack`, `alchemy-stage`, `alchemy-id`) are merged in
47 * automatically — BigLake databases have no labels field.
48 */
49 parameters?: Record<string, string>;
50};
51
52export type CatalogsDatabaseProps = {
53 /**
54 * Parent catalog. Full name
55 * `projects/{project}/locations/{location}/catalogs/{catalog}` or the
56 * catalog id (combined with `location`). Immutable — changing it
57 * replaces the database.
58 */
59 catalog: string;
60 /**
61 * Database id (the `{database}` segment). If omitted, a unique id is
62 * generated. Immutable — changing it replaces the database.
63 */
64 databaseId?: string;
65 /**
66 * Location used when `catalog` is a bare id.
67 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
68 */
69 location?: string;
70 /**
71 * Database type.
72 * @default "HIVE"
73 */
74 type?: biglake.DatabaseTypeEnum | (string & {});
75 /**
76 * Hive database options. Required when `type` is `HIVE`.
77 */
78 hiveOptions?: HiveDatabaseOptions;
79 /**
80 * Shortcut for `hiveOptions.locationUri`.
81 */
82 locationUri?: string;
83 /**
84 * Shortcut for `hiveOptions.parameters`. Alchemy ownership keys are
85 * merged in automatically.
86 */
87 parameters?: Record<string, string>;
88};
89
90export type CatalogsDatabase = Resource<
91 "GCP.BigLake.CatalogsDatabase",
92 CatalogsDatabaseProps,
93 {
94 /** Full resource name. */
95 name: string;
96 /** Database id (last path segment). */
97 databaseId: string;
98 /** Parent catalog resource name. */
99 catalog: string;
100 /** Project id. */
101 project: string;
102 /** Location id. */
103 location: string;
104 /** Database type. */
105 type: string | undefined;
106 /** Hive location URI. */
107 locationUri: string | undefined;
108 /** User Hive parameters (Alchemy ownership keys stripped). */
109 parameters: Record<string, string>;
110 /** RFC3339 creation timestamp. */
111 createTime: string | undefined;
112 /** RFC3339 last-update timestamp. */
113 updateTime: string | undefined;
114 /** RFC3339 deletion timestamp, if soft-deleted. */
115 deleteTime: string | undefined;
116 /** RFC3339 expire timestamp after delete. */
117 expireTime: string | undefined;
118 },
119 never,
120 Providers
121>;
122
123/**
124 * A Hive database inside a BigLake Metastore catalog.
125 *
126 * Databases have no labels field — Alchemy stamps ownership into
127 * `hiveOptions.parameters` so `list` / nuke can find them. Catalog,
128 * database id, and type are immutable. Hive location URI and parameters
129 * update in place.
130 *
131 * ### Creating a Database
132 * **Example:** Hive database on a GCS prefix
133 * ```typescript
134 * const catalog = yield* GCP.BigLake.Catalog("Lake", {});
135 * const database = yield* GCP.BigLake.CatalogsDatabase("Warehouse", {
136 * catalog: catalog.name,
137 * locationUri: `gs://${bucket.bucketName}/warehouse`,
138 * parameters: { owner: "analytics" },
139 * });
140 * ```
141 *
142 * **Example:** Named database
143 * ```typescript
144 * const database = yield* GCP.BigLake.CatalogsDatabase("Warehouse", {
145 * catalog: catalog.name,
146 * databaseId: "sales",
147 * type: "HIVE",
148 * hiveOptions: {
149 * locationUri: "gs://my-bucket/sales",
150 * },
151 * });
152 * ```
153 *
154 * @resource
155 * @category BigLake
156 */
157export const CatalogsDatabase = Resource<CatalogsDatabase>(
158 "GCP.BigLake.CatalogsDatabase",
159);
160
161const catalogOf = (catalog: string, project: string, location: string) =>
162 expandCatalog(catalog, project, location);
163
164const resourceName = (catalog: string, databaseId: string) =>
165 `${catalog}/databases/${databaseId}`;
166
167const parametersOf = (database: biglake.Database) =>
168 userLabels(database.hiveOptions?.parameters);
169
170const toAttrs = (database: biglake.Database, project: string) => {
171 const name = database.name ?? "";
172 const parsed = parseResourceName(name, "databases");
173 return {
174 name,
175 databaseId: parsed.id,
176 catalog: parsed.parent,
177 project,
178 location: parsed.location,
179 type: database.type,
180 locationUri: database.hiveOptions?.locationUri,
181 parameters: parametersOf(database),
182 createTime: database.createTime,
183 updateTime: database.updateTime,
184 deleteTime: database.deleteTime,
185 expireTime: database.expireTime,
186 };
187};
188
189const getByName = missingGet(biglake.getProjectsLocationsCatalogsDatabases);
190
191const desiredHiveOptions = (
192 news: CatalogsDatabaseProps,
193 parameters: Record<string, string>,
194): biglake.HiveDatabaseOptions => ({
195 locationUri: news.hiveOptions?.locationUri ?? news.locationUri,
196 parameters,
197});
198
199const emptyAndDeleteDatabase = (name: string) =>
200 Effect.gen(function* () {
201 const tables = yield* listTables(name);
202 yield* Effect.forEach(
203 namedOf(tables),
204 (table) =>
205 ignoreGone(
206 biglake.deleteProjectsLocationsCatalogsDatabasesTables({
207 name: table.name!,
208 }),
209 ),
210 { concurrency: 4 },
211 );
212 yield* ignoreGone(
213 biglake.deleteProjectsLocationsCatalogsDatabases({ name }),
214 );
215 });
216
217export const CatalogsDatabaseProvider = () =>
218 Provider.succeed(CatalogsDatabase, {
219 stables: [
220 "name",
221 "databaseId",
222 "catalog",
223 "project",
224 "location",
225 "createTime",
226 ],
227
228 diff: Effect.fn(function* ({ news, olds, output }) {
229 if (!isResolved(news)) return undefined;
230 const env = yield* GcpEnvironment.current;
231 const location = normalizeLocation(
232 news.location ?? olds?.location ?? output?.location,
233 env.region,
234 );
235 const typeChanged =
236 (olds?.type ?? output?.type) !== undefined &&
237 (news.type ?? "HIVE") !== (olds?.type ?? output?.type);
238 if (typeChanged) {
239 return { action: "replace" as const, deleteFirst: true };
240 }
241 return replaceOnIdentity({
242 previousId: olds?.databaseId ?? output?.databaseId,
243 nextId: news.databaseId,
244 previousLocation: normalizeLocation(
245 olds?.location ?? output?.location,
246 env.region,
247 ),
248 nextLocation: location,
249 previousParent: olds?.catalog ?? output?.catalog,
250 nextParent: catalogOf(news.catalog, env.project, location),
251 });
252 }),
253
254 read: Effect.fn(function* ({ id, olds, output }) {
255 const env = yield* GcpEnvironment.current;
256 const location = normalizeLocation(
257 olds?.location ?? output?.location,
258 env.region,
259 );
260 const databaseId = yield* toPhysicalId(
261 id,
262 olds?.databaseId,
263 output?.databaseId,
264 );
265 const catalog =
266 olds?.catalog !== undefined
267 ? catalogOf(olds.catalog, env.project, location)
268 : (output?.catalog ?? "");
269 const name =
270 output?.name ??
271 (catalog.length > 0 ? resourceName(catalog, databaseId) : "");
272 const existing = yield* getByName(name);
273 if (existing === undefined) return undefined;
274 const attrs = toAttrs(existing, env.project);
275 return (yield* hasAlchemyLabels(
276 id,
277 tagRecord(existing.hiveOptions?.parameters),
278 ))
279 ? attrs
280 : Unowned(attrs);
281 }),
282
283 list: () =>
284 Effect.gen(function* () {
285 const env = yield* GcpEnvironment.current;
286 const catalogs = (yield* Effect.forEach(
287 listLocations(env.region),
288 (location) => listCatalogs(locationParent(env.project, location)),
289 )).flat();
290 const databases = yield* listChildResources(
291 namedOf(catalogs),
292 listDatabases,
293 );
294 return databases
295 .filter((item) => hasAlchemyLabelMap(item.hiveOptions?.parameters))
296 .map((item) => toAttrs(item, env.project));
297 }),
298
299 reconcile: Effect.fn(function* ({ id, news, output }) {
300 const env = yield* GcpEnvironment.current;
301 const location = normalizeLocation(
302 news.location ?? output?.location,
303 env.region,
304 );
305 const catalog = catalogOf(news.catalog, env.project, location);
306 const databaseId = yield* toPhysicalId(
307 id,
308 news.databaseId,
309 output?.databaseId,
310 );
311 const name = output?.name ?? resourceName(catalog, databaseId);
312 const type = news.type ?? "HIVE";
313 const parameters = mergeParameters(
314 news.parameters ?? news.hiveOptions?.parameters,
315 yield* createInternalLabels(id),
316 );
317 const hiveOptions = desiredHiveOptions(news, parameters);
318 const body: biglake.Database = { type, hiveOptions };
319
320 let current = yield* getByName(name);
321
322 if (current === undefined) {
323 const created = yield* retryTransient(
324 biglake.createProjectsLocationsCatalogsDatabases({
325 parent: catalog,
326 databaseId,
327 body,
328 }),
329 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
330 current = created ?? undefined;
331 }
332
333 if (current === undefined) {
334 return yield* new BiglakeNotResolved({ name });
335 }
336
337 const currentName = current.name ?? name;
338 const typeChanged = !sameText(current.type, type);
339 const hiveChanged = !sameJson(current.hiveOptions, hiveOptions);
340
341 if (typeChanged || hiveChanged) {
342 current = yield* retryTransient(
343 biglake.patchProjectsLocationsCatalogsDatabases({
344 name: currentName,
345 updateMask: updateMaskOf(
346 typeChanged ? "type" : undefined,
347 hiveChanged ? "hiveOptions" : undefined,
348 ),
349 body: { name: currentName, ...body },
350 }),
351 );
352 }
353
354 return toAttrs(current, env.project);
355 }),
356
357 delete: Effect.fn(function* ({ output }) {
358 if (!output.name) return;
359 yield* emptyAndDeleteDatabase(output.name);
360 yield* waitUntilGone(getByName(output.name));
361 }),
362 });