reylo-effect-tooling 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3662 @@
1
+ // src/esbuild.ts
2
+ import { Effect as Effect8, FileSystem as FileSystem4 } from "effect";
3
+
4
+ // src/emit.ts
5
+ import { dirname as dirname4, join as join6, resolve as resolve3 } from "node:path";
6
+ import { Effect as Effect7, FileSystem as FileSystem3, Option as Option6 } from "effect";
7
+
8
+ // src/analysis.ts
9
+ import { createHash as createHash3, randomUUID } from "node:crypto";
10
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
11
+ import { dirname as dirname3, join as join5, posix, relative as relative2, resolve as resolve2, sep } from "node:path";
12
+ import {
13
+ Effect as Effect3,
14
+ Exit,
15
+ FileSystem as FileSystem2,
16
+ Option as Option4,
17
+ Path as Path2,
18
+ Predicate as Predicate4,
19
+ RcRef,
20
+ Ref,
21
+ Schema as Schema4,
22
+ Scope
23
+ } from "effect";
24
+ import { isMap, isNode, isScalar, parseDocument } from "yaml";
25
+
26
+ // src/reylo-engine.ts
27
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
28
+ import { createRequire } from "node:module";
29
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
30
+ import { fileURLToPath, pathToFileURL } from "node:url";
31
+ import { Predicate } from "effect";
32
+ var ENGINE_VERSION = "0.5.0";
33
+ var ENGINE_PACKAGES = {
34
+ postgres: "@reylosql/postgres",
35
+ sqlite: "@reylosql/sqlite",
36
+ mysql: "@reylosql/mysql"
37
+ };
38
+ var UNREACHABLE = /* @__PURE__ */ Symbol.for("reylo.unreachable");
39
+ var NOT_MYSQL = /* @__PURE__ */ Symbol.for("reylo.not-mysql");
40
+ var Unreachable = class extends Error {
41
+ [UNREACHABLE] = true;
42
+ constructor(cause) {
43
+ super(reason(cause), { cause });
44
+ }
45
+ };
46
+ function isUnreachable(cause) {
47
+ return UNREACHABLE in cause && cause[UNREACHABLE] === true;
48
+ }
49
+ function isNotMysql(cause) {
50
+ return NOT_MYSQL in cause && cause[NOT_MYSQL] === true && "server" in cause && Predicate.isString(cause.server);
51
+ }
52
+ function reason(cause) {
53
+ const own = messageOf(cause);
54
+ if (own !== "") return own;
55
+ if (Predicate.hasProperty(cause, "errors") && Array.isArray(cause.errors)) {
56
+ const joined = cause.errors.map(messageOf).filter((message) => message !== "").join("; ");
57
+ if (joined !== "") return joined;
58
+ }
59
+ if (Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code) && cause.code !== "")
60
+ return cause.code;
61
+ return String(cause);
62
+ }
63
+ function messageOf(cause) {
64
+ return Predicate.hasProperty(cause, "message") && Predicate.isString(cause.message) ? cause.message : "";
65
+ }
66
+ var MissingEngine = class _MissingEngine extends Error {
67
+ constructor(message) {
68
+ super(message);
69
+ this.name = "MissingEngine";
70
+ }
71
+ static of(dialect) {
72
+ const pkg = ENGINE_PACKAGES[dialect];
73
+ return new _MissingEngine(
74
+ `${dialect} statements need ${pkg} ${ENGINE_VERSION}: npm install -D ${pkg}@${ENGINE_VERSION}`
75
+ );
76
+ }
77
+ static none() {
78
+ return new _MissingEngine(
79
+ "reylo reads the project file with a database's engine, and none is installed: npm install -D @reylosql/postgres, @reylosql/mysql or @reylosql/sqlite, the one for your database"
80
+ );
81
+ }
82
+ };
83
+ var AnswerCache = class {
84
+ #entries = /* @__PURE__ */ new Map();
85
+ async get(need, fetch) {
86
+ const ttl = need.kind === "query" && need.scope === "session" ? 6e4 : 5e3;
87
+ const hit = this.#entries.get(need.id);
88
+ if (hit && Date.now() - hit.at < ttl) return hit.answer;
89
+ const answer = await fetch();
90
+ if (!("error" in answer)) this.#entries.set(need.id, { at: Date.now(), answer });
91
+ return answer;
92
+ }
93
+ clear() {
94
+ this.#entries.clear();
95
+ }
96
+ };
97
+ async function fulfil(need, io) {
98
+ try {
99
+ if (need.kind === "describe") {
100
+ const described = await io.describe(need.sql);
101
+ if ("kind" in described)
102
+ return { mysql: { columns: described.columns, params: described.params } };
103
+ return {
104
+ describe: {
105
+ params: described.params.map((param) => param.oid),
106
+ columns: described.columns
107
+ }
108
+ };
109
+ }
110
+ return { rows: await io.query(need.sql, need.params) };
111
+ } catch (cause) {
112
+ if (cause instanceof Error && isUnreachable(cause)) throw cause;
113
+ const error = { message: reason(cause) };
114
+ if (Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code))
115
+ error.code = cause.code;
116
+ if (Predicate.hasProperty(cause, "position") && cause.position !== void 0)
117
+ error.position = Number(cause.position);
118
+ return { error };
119
+ }
120
+ }
121
+ var WASI_WARNING = "WASI is an experimental feature and might change at any time";
122
+ function loadWasi() {
123
+ const original = Object.getOwnPropertyDescriptor(process, "emitWarning");
124
+ const emitWarning = process.emitWarning.bind(process);
125
+ const filtered = (warning, ...rest) => {
126
+ const message = Predicate.isString(warning) ? warning : warning.message;
127
+ const [options] = rest;
128
+ const type = !Predicate.isString(warning) ? warning.name : Predicate.isString(options) ? options : Predicate.hasProperty(options, "type") ? options.type : void 0;
129
+ if (message === WASI_WARNING && type === "ExperimentalWarning") return;
130
+ emitWarning(...[warning, ...rest]);
131
+ };
132
+ process.emitWarning = filtered;
133
+ try {
134
+ return process.getBuiltinModule("node:wasi");
135
+ } finally {
136
+ if (original) Object.defineProperty(process, "emitWarning", original);
137
+ }
138
+ }
139
+ var encoder = new TextEncoder();
140
+ var decoder = new TextDecoder();
141
+ var Engine = class _Engine {
142
+ #exports;
143
+ constructor(exports) {
144
+ this.#exports = exports;
145
+ }
146
+ static async load(path) {
147
+ const module = await WebAssembly.compile(readFileSync(path));
148
+ const { WASI } = loadWasi();
149
+ const wasi = new WASI({ version: "preview1", args: ["reylo-engine"], env: {} });
150
+ const imports = wasi.getImportObject();
151
+ const instance = await WebAssembly.instantiate(module, imports);
152
+ wasi.initialize(instance);
153
+ return new _Engine(instance.exports);
154
+ }
155
+ call(request) {
156
+ const { memory, reylo_alloc, reylo_free, reylo_call } = this.#exports;
157
+ const bytes2 = encoder.encode(JSON.stringify(request));
158
+ const pointer = reylo_alloc(bytes2.length);
159
+ new Uint8Array(memory.buffer, pointer, bytes2.length).set(bytes2);
160
+ const out = reylo_call(pointer, bytes2.length);
161
+ reylo_free(pointer, bytes2.length);
162
+ const length = new DataView(memory.buffer).getUint32(out, true);
163
+ const text = decoder.decode(new Uint8Array(memory.buffer, out + 4, length));
164
+ reylo_free(out, length + 4);
165
+ return JSON.parse(text);
166
+ }
167
+ lex(sql, dialect) {
168
+ const response = this.call({ op: "lex", sql, dialect });
169
+ if (response.status !== "lexed") throw new Error(failureOf(response));
170
+ return response.lexed;
171
+ }
172
+ /** Analyze one statement, fulfilling the engine's needs through `io` until it answers. */
173
+ async analyze(sql, dialect, io, cache = new AnswerCache()) {
174
+ const answers = {};
175
+ for (let round = 0; round < 16; round++) {
176
+ const response = this.call({ op: "analyze", sql, dialect, answers });
177
+ if (response.status === "analysis") return response.analysis;
178
+ if (response.status !== "need") throw new Error(failureOf(response));
179
+ await Promise.all(
180
+ response.needs.map(async (need) => {
181
+ answers[need.id] = await cache.get(need, () => fulfil(need, io));
182
+ })
183
+ );
184
+ }
185
+ throw new Error("the analysis engine did not converge");
186
+ }
187
+ };
188
+ function failureOf(response) {
189
+ return response.status === "error" ? response.message : `unexpected ${response.status}`;
190
+ }
191
+ function enginePath(dialect, projectDir) {
192
+ return resolveFrom(projectDir, `${ENGINE_PACKAGES[dialect]}/engine.wasm`);
193
+ }
194
+ function resolveFrom(projectDir, specifier) {
195
+ for (const base of [fileURLToPath(import.meta.url), join(projectDir, "package.json")]) {
196
+ try {
197
+ return createRequire(base).resolve(specifier);
198
+ } catch {
199
+ continue;
200
+ }
201
+ }
202
+ return void 0;
203
+ }
204
+ async function describer(dialect, projectDir) {
205
+ const path = resolveFrom(projectDir, `${ENGINE_PACKAGES[dialect]}/describe`);
206
+ if (path === void 0) throw MissingEngine.of(dialect);
207
+ return await import(pathToFileURL(path).href);
208
+ }
209
+ function sqlitePackageDir(projectDir) {
210
+ const path = resolveFrom(projectDir, "@reylosql/sqlite/package.json");
211
+ return path === void 0 ? void 0 : dirname(path);
212
+ }
213
+ var engines = /* @__PURE__ */ new Map();
214
+ function engine(dialect, projectDir) {
215
+ const path = enginePath(dialect, projectDir);
216
+ if (path === void 0) return Promise.reject(MissingEngine.of(dialect));
217
+ let loading = engines.get(path);
218
+ if (loading === void 0) {
219
+ loading = loadEngine(dialect, path);
220
+ loading.catch(() => engines.delete(path));
221
+ engines.set(path, loading);
222
+ }
223
+ return loading;
224
+ }
225
+ async function loadEngine(dialect, path) {
226
+ const pkg = ENGINE_PACKAGES[dialect];
227
+ const version = packageVersion(join(dirname(path), "package.json"));
228
+ if (version !== ENGINE_VERSION) {
229
+ throw new MissingEngine(
230
+ `reylo-effect runs with ${pkg} ${ENGINE_VERSION}, and the one installed is ${version ?? "of no version"} (${dirname(path)}): npm install -D ${pkg}@${ENGINE_VERSION}`
231
+ );
232
+ }
233
+ const loaded = await Engine.load(path);
234
+ const ping = loaded.call({ op: "ping" });
235
+ if (ping.status !== "ok" || ping.database !== dialect) {
236
+ const reads = ping.status === "ok" && ping.database ? `${ping.database} statements` : "no one database's statements";
237
+ throw new MissingEngine(
238
+ `${pkg} (${dirname(path)}) carries an engine that reads ${reads}, not ${dialect} ones: npm install -D ${pkg}@${ENGINE_VERSION}`
239
+ );
240
+ }
241
+ return loaded;
242
+ }
243
+ function packageVersion(file) {
244
+ try {
245
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
246
+ return Predicate.hasProperty(parsed, "version") && Predicate.isString(parsed.version) ? parsed.version : void 0;
247
+ } catch {
248
+ return void 0;
249
+ }
250
+ }
251
+ async function anyEngine(projectDir) {
252
+ let failed;
253
+ for (const dialect of ["postgres", "mysql", "sqlite"]) {
254
+ if (enginePath(dialect, projectDir) === void 0) continue;
255
+ try {
256
+ return await engine(dialect, projectDir);
257
+ } catch (cause) {
258
+ failed ??= cause instanceof MissingEngine ? cause : new MissingEngine(reason(cause));
259
+ }
260
+ }
261
+ return failed ?? MissingEngine.none();
262
+ }
263
+ var PROJECT_FILES = [".reylo.yaml", ".reylo.yml"];
264
+ var DEFAULT_ENV_FILES = [".env.local", ".env"];
265
+ function packageDir(from) {
266
+ for (let dir = resolve(from); ; dir = dirname(dir)) {
267
+ if (existsSync(join(dir, "package.json"))) return dir;
268
+ if (dirname(dir) === dir) return resolve(from);
269
+ }
270
+ }
271
+ function findProjectFile(from, env) {
272
+ if (env.REYLO_CONFIG) return resolve(env.REYLO_CONFIG);
273
+ const dir = packageDir(from);
274
+ return PROJECT_FILES.map((name) => join(dir, name)).find((path) => existsSync(path));
275
+ }
276
+ function dialectOf(url) {
277
+ const scheme = /^([a-z][a-z0-9+.-]*):/iu.exec(url)?.[1]?.toLowerCase();
278
+ if (scheme === "postgres" || scheme === "postgresql") return "postgres";
279
+ if (scheme === "mysql" || scheme === "mariadb") return "mysql";
280
+ if (scheme === "file" || scheme === "sqlite") return "sqlite";
281
+ return void 0;
282
+ }
283
+ function readEnvFile(path) {
284
+ const out = /* @__PURE__ */ new Map();
285
+ if (!existsSync(path)) return out;
286
+ for (const line of readFileSync(path, "utf8").split("\n")) {
287
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/u.exec(line);
288
+ if (match) out.set(match[1], match[2].replace(/^(["'])(.*)\1$/u, "$2"));
289
+ }
290
+ return out;
291
+ }
292
+ function place(text, offset) {
293
+ const lines = Buffer.from(text, "utf8").subarray(0, offset).toString("utf8").split("\n");
294
+ return `${lines.length}:${lines.at(-1).length + 1}`;
295
+ }
296
+ var TYPES_KEY = /^types[ \t]*:/mu;
297
+ function resolveNativeProject(host2, projectDir, env = process.env, configText = (text) => text) {
298
+ const file = findProjectFile(projectDir, env);
299
+ const root = file ? dirname(file) : packageDir(projectDir);
300
+ const shown = (path) => isAbsolute(path) ? relative(projectDir, path) || path : path;
301
+ let config = {};
302
+ let error;
303
+ if (file) {
304
+ let text;
305
+ try {
306
+ text = readFileSync(file, "utf8");
307
+ } catch (cause) {
308
+ error = `Cannot read ${shown(file)}: ${reason(cause)}`;
309
+ }
310
+ const types = text === void 0 ? null : TYPES_KEY.exec(text);
311
+ if (text !== void 0 && types) {
312
+ error = `${shown(file)}:${place(text, Buffer.byteLength(text.slice(0, types.index)))}: types: the types section was removed on 2026-10-20. Reylo now gives every value one type on every driver, whatever the driver's settings: a 64-bit integer is a BigInt, a decimal its exact text, a timestamptz its ISO 8601 text in UTC, a date and a timestamp their text, JSON parsed and binary data a Uint8Array. Delete the section.`;
313
+ } else if (text !== void 0) {
314
+ if (host2 instanceof MissingEngine) error = `${shown(file)}: ${host2.message}`;
315
+ else {
316
+ const read = configText(text);
317
+ const response = host2.call({ op: "config", text: read });
318
+ if (response.status === "config") config = response.config;
319
+ else if (response.status === "configError")
320
+ error = `${shown(file)}:${place(read, response.error.start)}: ${response.error.message}`;
321
+ else error = `${shown(file)}: ${failureOf(response)}`;
322
+ }
323
+ }
324
+ }
325
+ const both = PROJECT_FILES.map((name) => join(root, name));
326
+ if (!env.REYLO_CONFIG && both.every((path) => existsSync(path)))
327
+ error = `${both.map(shown).join(" and ")} are both project files; keep one.`;
328
+ const envFiles = (config.env ?? DEFAULT_ENV_FILES).map((name) => resolve(root, name));
329
+ const envValues = /* @__PURE__ */ new Map();
330
+ const lookup = (name) => {
331
+ const set = env[name];
332
+ if (set !== void 0) return { value: set, source: `${name} in the environment` };
333
+ for (const path of envFiles) {
334
+ let values = envValues.get(path);
335
+ if (values === void 0) {
336
+ values = readEnvFile(path);
337
+ envValues.set(path, values);
338
+ }
339
+ const value = values.get(name);
340
+ if (value !== void 0) return { value, source: `${name} in ${shown(path)}` };
341
+ }
342
+ return void 0;
343
+ };
344
+ const fileName = file === void 0 ? ".reylo.yaml" : shown(file);
345
+ const urlAlone = config.url !== void 0 && config.direct_url === void 0;
346
+ const notice = urlAlone ? `${fileName}: Reylo does not read url; set direct_url to the dev database Reylo reads your types from (it uses DATABASE_URL meanwhile)` : void 0;
347
+ const missingUrl = urlAlone ? `No database URL: ${fileName} sets url, which Reylo does not read; set direct_url there, or DATABASE_URL in the environment, .env.local or .env.` : `No database URL: set DATABASE_URL in the environment, .env.local or .env, or "direct_url" in ${fileName}.`;
348
+ let url;
349
+ let urlSource = "DATABASE_URL";
350
+ if (config.direct_url !== void 0) {
351
+ const missing = [];
352
+ const sources = [];
353
+ url = config.direct_url.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/gu, (_, name) => {
354
+ const hit = lookup(name);
355
+ if (hit) sources.push(hit.source);
356
+ else missing.push(name);
357
+ return hit?.value ?? "";
358
+ });
359
+ urlSource = `"direct_url" in ${fileName}${sources.length > 0 ? `, ${sources.join(", ")}` : ""}`;
360
+ if (missing.length > 0) {
361
+ error ??= `${fileName}: "direct_url" names ${missing.map((name) => `\${${name}}`).join(", ")}, which neither the environment nor ${envFiles.map(shown).join(" nor ")} sets.`;
362
+ url = void 0;
363
+ }
364
+ } else {
365
+ const hit = lookup("DATABASE_URL");
366
+ url = hit?.value;
367
+ if (hit) urlSource = hit.source;
368
+ }
369
+ let dialect = url ? dialectOf(url) : "postgres";
370
+ if (url && !dialect)
371
+ error ??= `Cannot tell the database from the URL (${urlSource}): reylo reads postgres://, postgresql://, mysql://, file: and sqlite: URLs.`;
372
+ const watched = [.../* @__PURE__ */ new Set([...file ? [file] : [], ...both]), ...envFiles];
373
+ if (!url) dialect = offlineDialect(root) ?? dialect;
374
+ return {
375
+ file,
376
+ root,
377
+ dialect: dialect ?? "postgres",
378
+ strictParams: config.strict_params ?? true,
379
+ sqlcommenter: config.sqlcommenter ?? true,
380
+ cli: config.cli ?? {},
381
+ url,
382
+ urlSource,
383
+ offline: offlineFrom(env) ?? false,
384
+ watched,
385
+ error,
386
+ notice,
387
+ missingUrl
388
+ };
389
+ }
390
+ function offlineFrom(env) {
391
+ const value = env.REYLO_OFFLINE?.trim().toLowerCase();
392
+ if (value === void 0 || value === "") return void 0;
393
+ return !(value === "0" || value === "false" || value === "no" || value === "off");
394
+ }
395
+ function offlineDialect(root) {
396
+ const declared = declaredEngines(packageDir(root));
397
+ if (declared.length === 1) return declared[0];
398
+ const entries = entryDialects(join(root, ".reylo", "queries"));
399
+ return entries.size === 1 ? [...entries][0] : void 0;
400
+ }
401
+ var DIALECTS = ["postgres", "sqlite", "mysql"];
402
+ function declaredEngines(dir) {
403
+ try {
404
+ const parsed = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
405
+ const listed2 = /* @__PURE__ */ new Set();
406
+ for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
407
+ const group = Predicate.hasProperty(parsed, key) ? parsed[key] : void 0;
408
+ if (Predicate.isObject(group)) for (const name of Object.keys(group)) listed2.add(name);
409
+ }
410
+ return DIALECTS.filter((dialect) => listed2.has(ENGINE_PACKAGES[dialect]));
411
+ } catch {
412
+ return [];
413
+ }
414
+ }
415
+ var entryScans = /* @__PURE__ */ new Map();
416
+ function entryDialects(dir) {
417
+ let mtime;
418
+ try {
419
+ mtime = statSync(dir).mtimeMs;
420
+ } catch {
421
+ return /* @__PURE__ */ new Set();
422
+ }
423
+ const cached = entryScans.get(dir);
424
+ if (cached?.mtime === mtime) return cached.dialects;
425
+ const dialects = /* @__PURE__ */ new Set();
426
+ for (const name of readdirSync(dir)) {
427
+ if (!name.endsWith(".json")) continue;
428
+ try {
429
+ const parsed = JSON.parse(readFileSync(join(dir, name), "utf8"));
430
+ const found = Predicate.hasProperty(parsed, "dialect") ? parsed.dialect : void 0;
431
+ const dialect = DIALECTS.find((candidate) => candidate === found);
432
+ if (dialect !== void 0) dialects.add(dialect);
433
+ } catch {
434
+ continue;
435
+ }
436
+ }
437
+ entryScans.set(dir, { mtime, dialects });
438
+ return dialects;
439
+ }
440
+ function isSqlInputValue(value) {
441
+ return Predicate.isNull(value) || Predicate.isNumber(value) || Predicate.isBigInt(value) || Predicate.isString(value) || ArrayBuffer.isView(value);
442
+ }
443
+ function sqlitePath(url, baseDir) {
444
+ if (url === ":memory:" || url === "file::memory:") return ":memory:";
445
+ const raw = url.startsWith("file://") ? fileURLToPath(url) : url.replace(/^(file|sqlite):(\/\/)?/u, "");
446
+ return isAbsolute(raw) ? raw : resolve(baseDir, raw);
447
+ }
448
+
449
+ // src/reylo-session.ts
450
+ import { createHash } from "node:crypto";
451
+ import {
452
+ existsSync as existsSync3,
453
+ mkdirSync,
454
+ readdirSync as readdirSync2,
455
+ readFileSync as readFileSync3,
456
+ renameSync,
457
+ rmSync,
458
+ writeFileSync
459
+ } from "node:fs";
460
+ import { join as join3 } from "node:path";
461
+ import { Option as Option2, Schema as Schema2 } from "effect";
462
+
463
+ // src/sqlite-drivers.ts
464
+ import { fork } from "node:child_process";
465
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
466
+ import { createRequire as createRequire2 } from "node:module";
467
+ import { dirname as dirname2, join as join2 } from "node:path";
468
+ import { DatabaseSync } from "node:sqlite";
469
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
470
+ import { Option, Predicate as Predicate2, Schema } from "effect";
471
+ function sync(name, database) {
472
+ const version = database.prepare("select sqlite_version() as v").get().v;
473
+ return {
474
+ name,
475
+ says: (refusal) => `${name} runs SQLite ${version}, which refuses this statement: ${refusal}`,
476
+ async refuses(sql) {
477
+ try {
478
+ database.prepare(sql);
479
+ return void 0;
480
+ } catch (cause) {
481
+ return cause instanceof Error ? cause.message : String(cause);
482
+ }
483
+ },
484
+ async close() {
485
+ database.close();
486
+ }
487
+ };
488
+ }
489
+ var DRIVERS = [
490
+ [
491
+ "better-sqlite3",
492
+ async (require2, path) => {
493
+ const Database = require2("better-sqlite3");
494
+ return sync("better-sqlite3", new Database(path, { readonly: true, fileMustExist: true }));
495
+ }
496
+ ],
497
+ [
498
+ // `@libsql/client` opens a file through the `libsql` package, its own dependency.
499
+ "@libsql/client",
500
+ async (require2, path) => {
501
+ const Database = createRequire2(require2.resolve("@libsql/client"))(
502
+ "libsql"
503
+ );
504
+ return sync("@libsql/client", new Database(path, { readonly: true }));
505
+ }
506
+ ],
507
+ [
508
+ "libsql",
509
+ async (require2, path) => {
510
+ const Database = require2("libsql");
511
+ return sync("libsql", new Database(path, { readonly: true }));
512
+ }
513
+ ],
514
+ ["sqlite3", openSqlite3],
515
+ [
516
+ "@tursodatabase/database",
517
+ async (require2, path) => {
518
+ const url = pathToFileURL2(require2.resolve("@tursodatabase/database/compat")).href;
519
+ const { Database } = await import(url);
520
+ return sync("@tursodatabase/database", new Database(path, { readonly: true }));
521
+ }
522
+ ],
523
+ ["wrangler", openD1]
524
+ ];
525
+ async function openSqlite3(require2, path) {
526
+ const sqlite3 = require2("sqlite3");
527
+ const database = await new Promise((done, fail2) => {
528
+ const opened = new sqlite3.Database(
529
+ path,
530
+ sqlite3.OPEN_READONLY,
531
+ (error) => error ? fail2(error) : done(opened)
532
+ );
533
+ });
534
+ const version = await new Promise(
535
+ (done, fail2) => database.get(
536
+ "select sqlite_version() as v",
537
+ (error, row) => error ? fail2(error) : done(row.v)
538
+ )
539
+ );
540
+ return {
541
+ name: "sqlite3",
542
+ says: (refusal) => `sqlite3 runs SQLite ${version}, which refuses this statement: ${refusal}`,
543
+ refuses: (sql) => new Promise((done) => {
544
+ const statement = database.prepare(sql, (error) => {
545
+ if (!error) statement.finalize();
546
+ done(error ? error.message.replace(/^SQLITE_\w+: /u, "") : void 0);
547
+ });
548
+ }),
549
+ close: () => new Promise((done) => database.close(() => done()))
550
+ };
551
+ }
552
+ var D1_START = 3e4;
553
+ var d1s = /* @__PURE__ */ new Map();
554
+ function wranglerConfig(dir) {
555
+ return ["wrangler.json", "wrangler.jsonc", "wrangler.toml"].map((name) => {
556
+ try {
557
+ const { size, mtimeMs } = statSync2(join2(dir, name));
558
+ return `${name} ${size} ${mtimeMs}`;
559
+ } catch {
560
+ return "";
561
+ }
562
+ }).join("\0");
563
+ }
564
+ async function openD1(require2, path) {
565
+ const entry = require2.resolve("wrangler");
566
+ const dir = dirname2(require2.resolve("./package.json"));
567
+ const key = `${entry}\0${dir}`;
568
+ const config = wranglerConfig(dir);
569
+ let held = d1s.get(key);
570
+ if (held?.config !== config) {
571
+ held?.started.then(
572
+ (d12) => d12.close(),
573
+ () => void 0
574
+ );
575
+ const started = startD1(entry, dir, () => {
576
+ if (d1s.get(key)?.started === started) d1s.delete(key);
577
+ });
578
+ started.catch(() => {
579
+ if (d1s.get(key)?.started === started) d1s.delete(key);
580
+ });
581
+ held = { config, started };
582
+ d1s.set(key, held);
583
+ }
584
+ const d1 = await held.started;
585
+ const schema = new DatabaseSync(path, { readOnly: true });
586
+ return {
587
+ name: "wrangler",
588
+ says: (refusal) => `Cloudflare D1, as wrangler ${d1.version} runs it locally, refuses this statement: ${refusal}`,
589
+ async refuses(sql, params) {
590
+ let ddl;
591
+ try {
592
+ ddl = schemaDdl(schema);
593
+ } catch {
594
+ return void 0;
595
+ }
596
+ return d1.explain(sql, params, ddl);
597
+ },
598
+ async close() {
599
+ schema.close();
600
+ }
601
+ };
602
+ }
603
+ function schemaDdl(database) {
604
+ return database.prepare(
605
+ `select s.sql from sqlite_schema s where s.sql is not null and substr(s.name, 1, 7) <> 'sqlite_' and substr(s.name, 1, 4) <> '_cf_'
606
+ and not exists (select 1 from pragma_table_list p where p.name = s.name and p.type = 'shadow') order by s.rowid`
607
+ ).all().flatMap((row) => Predicate2.isString(row.sql) ? [row.sql] : []);
608
+ }
609
+ var ProxyMessage = Schema.Struct({
610
+ kind: Schema.optionalKey(Schema.String),
611
+ id: Schema.optionalKey(Schema.Number),
612
+ d1: Schema.optionalKey(Schema.Boolean),
613
+ error: Schema.optionalKey(Schema.String)
614
+ });
615
+ var decodeProxyMessage = Schema.decodeUnknownOption(ProxyMessage);
616
+ var NO_MESSAGE = {};
617
+ async function startD1(entry, dir, ended) {
618
+ const version = wranglerVersion(entry);
619
+ const engineDir = sqlitePackageDir(dir);
620
+ if (engineDir === void 0) throw new Error("@reylosql/sqlite is not installed");
621
+ const child = fork(join2(engineDir, "d1-proxy.js"), [entry, dir], {
622
+ stdio: ["ignore", "ignore", "ignore", "ipc"],
623
+ env: { ...process.env, WRANGLER_SEND_METRICS: "false" }
624
+ });
625
+ child.unref();
626
+ const pending = /* @__PURE__ */ new Map();
627
+ let next = 0;
628
+ let sent;
629
+ const gone = new Promise(
630
+ (_, fail2) => child.once("exit", () => fail2(new Error("D1's process ended")))
631
+ );
632
+ gone.catch(() => void 0);
633
+ child.on("message", (raw) => {
634
+ const message = Option.getOrElse(decodeProxyMessage(raw), () => NO_MESSAGE);
635
+ if (message.kind !== "explain" || message.id === void 0) return;
636
+ pending.get(message.id)?.(message.error);
637
+ pending.delete(message.id);
638
+ if (pending.size === 0) child.channel?.unref();
639
+ });
640
+ let running = false;
641
+ child.once("exit", () => {
642
+ if (running) ended();
643
+ for (const settle of pending.values()) settle(void 0);
644
+ pending.clear();
645
+ });
646
+ const stop = () => {
647
+ if (child.connected) child.send({ kind: "close" });
648
+ setTimeout(() => child.kill(), 2e3).unref();
649
+ };
650
+ try {
651
+ const timeout = new Promise(
652
+ (_, fail2) => setTimeout(() => fail2(new Error("workerd did not start")), D1_START).unref()
653
+ );
654
+ const ready = await Promise.race([
655
+ gone,
656
+ timeout,
657
+ // SAFETY: the D1 proxy's first message is `ready`.
658
+ new Promise(
659
+ (done) => child.once(
660
+ "message",
661
+ (raw) => done(Option.getOrElse(decodeProxyMessage(raw), () => NO_MESSAGE))
662
+ )
663
+ )
664
+ ]);
665
+ if (!ready.d1) throw new Error("the wrangler config binds no D1 database");
666
+ } catch (cause) {
667
+ stop();
668
+ throw cause;
669
+ }
670
+ running = true;
671
+ child.channel?.unref();
672
+ return {
673
+ version,
674
+ close: stop,
675
+ explain: (sql, params, ddl) => new Promise((done) => {
676
+ if (!child.connected) return done(void 0);
677
+ const id = next++;
678
+ pending.set(id, (error) => {
679
+ const message = error?.replace(/^D1_ERROR: /u, "").replace(/ at offset \d+/u, "").replace(/: SQLITE_\w+$/u, "");
680
+ done(
681
+ message && !message.startsWith("Wrong number of parameter bindings") ? message : void 0
682
+ );
683
+ });
684
+ child.channel?.ref();
685
+ const text = JSON.stringify(ddl);
686
+ if (text !== sent) {
687
+ child.send({ kind: "schema", ddl });
688
+ sent = text;
689
+ }
690
+ child.send({ kind: "explain", id, sql, params });
691
+ })
692
+ };
693
+ }
694
+ function wranglerVersion(entry) {
695
+ for (let dir = dirname2(entry); dirname2(dir) !== dir; dir = dirname2(dir)) {
696
+ const file = join2(dir, "package.json");
697
+ if (!existsSync2(file)) continue;
698
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
699
+ if (Predicate2.hasProperty(parsed, "name") && parsed.name === "wrangler")
700
+ return Predicate2.hasProperty(parsed, "version") && Predicate2.isString(parsed.version) ? parsed.version : "";
701
+ }
702
+ return "";
703
+ }
704
+ function projectDrivers(root) {
705
+ const file = join2(packageDir(root), "package.json");
706
+ if (!existsSync2(file)) return [];
707
+ const listed2 = /* @__PURE__ */ new Set();
708
+ try {
709
+ const parsed = JSON.parse(readFileSync2(file, "utf8"));
710
+ for (const key of ["dependencies", "devDependencies", "optionalDependencies"]) {
711
+ const group = Predicate2.hasProperty(parsed, key) ? parsed[key] : void 0;
712
+ if (Predicate2.isObject(group)) for (const name of Object.keys(group)) listed2.add(name);
713
+ }
714
+ } catch {
715
+ return [];
716
+ }
717
+ return DRIVERS.flatMap(([name]) => listed2.has(name) ? [name] : []);
718
+ }
719
+ async function openProjectDrivers(root, path) {
720
+ if (path === ":memory:") return [];
721
+ const names = projectDrivers(root);
722
+ if (names.length === 0) return [];
723
+ const require2 = createRequire2(join2(packageDir(root), "package.json"));
724
+ const opened = await Promise.all(
725
+ DRIVERS.flatMap(
726
+ ([name, open]) => names.includes(name) ? [open(require2, path).catch(() => void 0)] : []
727
+ )
728
+ );
729
+ return opened.filter((driver) => driver !== void 0);
730
+ }
731
+ var utf8 = new TextEncoder();
732
+ function placeRefusal(sql, message) {
733
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
734
+ const fn = /(?:no such function|not authorized to use function): ([\w$]+)/iu.exec(message)?.[1] ?? /arguments to function ([\w$]+)\(\)/iu.exec(message)?.[1];
735
+ const near = /near "([^"]+)"/u.exec(message)?.[1];
736
+ const found = fn ? new RegExp(`\\b${escape(fn)}\\s*\\(`, "iu").exec(sql) : near ? new RegExp(escape(near), "iu").exec(sql) : null;
737
+ const at = found ? { index: found.index, text: fn ?? near ?? "" } : { index: sql.search(/\S/u), text: /\S+/u.exec(sql)?.[0] ?? "" };
738
+ return {
739
+ start: utf8.encode(sql.slice(0, Math.max(at.index, 0))).length,
740
+ length: Math.max(utf8.encode(at.text).length, 1)
741
+ };
742
+ }
743
+
744
+ // src/reylo-session.ts
745
+ var ENTRY_VERSION = 1;
746
+ var OFFLINE = "Cache-only analysis (REYLO_OFFLINE or offline in .reylo.yaml) opens no database connection";
747
+ var OFFLINE_MISS = `${OFFLINE}, and .reylo has no analysis of this statement: analyze it where the analysis database can be reached (reylo generate, a build, or a test run in CI) and deploy .reylo with the application.`;
748
+ var SCHEMA_TTL = 5e3;
749
+ var OFFLINE_TTL = 6e4;
750
+ var NativeSession = class {
751
+ project;
752
+ #engine;
753
+ #engineFingerprint;
754
+ #answers = new AnswerCache();
755
+ #idle;
756
+ #io;
757
+ #busy = 0;
758
+ #timer;
759
+ #schema;
760
+ constructor(engine2, project, idle) {
761
+ this.#engine = engine2;
762
+ this.project = project;
763
+ this.#idle = idle;
764
+ const ping = engine2 instanceof MissingEngine ? void 0 : engine2.call({ op: "ping" });
765
+ this.#engineFingerprint = ping?.status === "ok" ? ping.fingerprint : "unknown";
766
+ }
767
+ lex(sql) {
768
+ if (this.#engine instanceof MissingEngine) throw this.#engine;
769
+ return this.#engine.lex(sql, this.project.dialect);
770
+ }
771
+ /** Named by the statement, then by the server, so a dev and a test database keep one each. */
772
+ entryFile(sql) {
773
+ return join3(
774
+ this.project.root,
775
+ ".reylo",
776
+ "queries",
777
+ `${this.#statementKey(sql)}-${sha256(this.#server()).slice(0, 16)}.json`
778
+ );
779
+ }
780
+ #statementKey(sql) {
781
+ return sha256(
782
+ JSON.stringify([ENTRY_VERSION, this.#engineFingerprint, this.project.dialect, sql])
783
+ ).slice(0, 32);
784
+ }
785
+ /** This server's entry for the statement, else the newest any other server wrote. */
786
+ #offlineEntry(sql) {
787
+ const own = this.entryFile(sql);
788
+ const entry = readEntry(own);
789
+ if (entry) return { file: own, entry };
790
+ const dir = join3(this.project.root, ".reylo", "queries");
791
+ const prefix = `${this.#statementKey(sql)}-`;
792
+ let names;
793
+ try {
794
+ names = readdirSync2(dir).filter((name) => name.startsWith(prefix) && name.endsWith(".json"));
795
+ } catch {
796
+ return void 0;
797
+ }
798
+ let newest;
799
+ for (const file of names.map((name) => join3(dir, name))) {
800
+ const other = readEntry(file);
801
+ if (other && (!newest || (other.at ?? -Infinity) > (newest.entry.at ?? -Infinity)))
802
+ newest = { file, entry: other };
803
+ }
804
+ return newest;
805
+ }
806
+ async analyze(sql) {
807
+ clearTimeout(this.#timer);
808
+ this.#busy++;
809
+ try {
810
+ const outcome = await this.#analyze(sql);
811
+ return outcome.ok && this.project.dialect === "sqlite" ? await this.#onDrivers(sql, outcome) : outcome;
812
+ } finally {
813
+ if (--this.#busy === 0 && this.#io && Number.isFinite(this.#idle))
814
+ this.#timer = setTimeout(() => void this.#release(), this.#idle).unref();
815
+ }
816
+ }
817
+ /** A SQLite statement prepared on the app's own drivers too, whose SQLite may be older. */
818
+ async #onDrivers(sql, outcome) {
819
+ const { analysis } = outcome;
820
+ if (analysis.diagnostics.length > 0 || this.#schema?.value == null) return outcome;
821
+ const io = await this.#connection().catch(() => void 0);
822
+ const drivers = await io?.drivers?.() ?? [];
823
+ for (const driver of drivers) {
824
+ const refusal = await driver.refuses(analysis.rewritten.sql, analysis.rewritten.order.length);
825
+ if (refusal === void 0) continue;
826
+ const { start, length } = placeRefusal(sql, refusal);
827
+ return {
828
+ ...outcome,
829
+ analysis: {
830
+ ...analysis,
831
+ diagnostics: [{ message: driver.says(refusal), start, length, code: 6 }]
832
+ }
833
+ };
834
+ }
835
+ return outcome;
836
+ }
837
+ async #analyze(sql) {
838
+ if (this.project.error) return { ok: false, message: this.project.error, code: 11 };
839
+ if (this.project.offline) {
840
+ const engine3 = this.#engine;
841
+ if (engine3 instanceof MissingEngine) return { ok: false, message: engine3.message, code: 11 };
842
+ const hit = this.#offlineEntry(sql);
843
+ return hit ? { ok: true, analysis: hit.entry.analysis, source: "cache", entry: hit.file } : { ok: false, message: OFFLINE_MISS, code: 4 };
844
+ }
845
+ if (!this.project.url) {
846
+ const hit = this.#engine instanceof MissingEngine ? void 0 : this.#offlineEntry(sql);
847
+ return hit ? { ok: true, analysis: hit.entry.analysis, source: "cache", entry: hit.file } : { ok: false, message: this.project.missingUrl, code: 4 };
848
+ }
849
+ const engine2 = this.#engine;
850
+ if (engine2 instanceof MissingEngine) return { ok: false, message: engine2.message, code: 11 };
851
+ const file = this.entryFile(sql);
852
+ const schema = await this.schema();
853
+ if (schema.fatal) return { ok: false, message: schema.error ?? "Cannot load schema", code: 5 };
854
+ if (schema.value === null) {
855
+ const hit = this.#offlineEntry(sql);
856
+ if (hit) return { ok: true, analysis: hit.entry.analysis, source: "cache", entry: hit.file };
857
+ return {
858
+ ok: false,
859
+ message: `${this.cannotConnect(schema.error)}; .reylo has no analysis of this statement, so run reylo generate where the database can be reached.`,
860
+ code: 5
861
+ };
862
+ }
863
+ const entry = readEntry(file);
864
+ if (entry && entry.schema === schema.value)
865
+ return { ok: true, analysis: entry.analysis, source: "cache", entry: file };
866
+ let analysis;
867
+ try {
868
+ analysis = await engine2.analyze(
869
+ sql,
870
+ this.project.dialect,
871
+ await this.#connection(),
872
+ this.#answers
873
+ );
874
+ } catch (cause) {
875
+ if (cause instanceof Error && isUnreachable(cause)) {
876
+ this.#io = void 0;
877
+ this.#schema = { value: null, at: Date.now(), error: reason(cause) };
878
+ return { ok: false, message: this.cannotConnect(reason(cause)), code: 5 };
879
+ }
880
+ if (cause instanceof Error && isNotMysql(cause)) {
881
+ const error = this.notMysql(cause);
882
+ this.#io = void 0;
883
+ this.#schema = { value: null, at: Date.now(), error, fatal: true };
884
+ return { ok: false, message: error, code: 5 };
885
+ }
886
+ return { ok: false, message: reason(cause), code: 5 };
887
+ }
888
+ writeEntry(this.project.root, file, {
889
+ version: ENTRY_VERSION,
890
+ dialect: this.project.dialect,
891
+ schema: schema.value,
892
+ analysis,
893
+ at: Date.now()
894
+ });
895
+ return { ok: true, analysis, source: "database", entry: file };
896
+ }
897
+ /** A server that refuses the fingerprint query gets a value no entry matches: analysis goes live. */
898
+ async schema() {
899
+ const now = Date.now();
900
+ if (this.project.offline) return this.#schema ??= { value: null, at: now, error: OFFLINE };
901
+ if (this.#schema && now - this.#schema.at < (this.#schema.value === null ? OFFLINE_TTL : SCHEMA_TTL))
902
+ return this.#schema;
903
+ let io;
904
+ try {
905
+ io = await this.#connection();
906
+ } catch (cause) {
907
+ this.#io = void 0;
908
+ this.#schema = cause instanceof Error && isUnreachable(cause) ? { value: null, at: now, error: reason(cause) } : {
909
+ value: null,
910
+ at: now,
911
+ error: cause instanceof Error && isNotMysql(cause) ? this.notMysql(cause) : reason(cause),
912
+ fatal: true
913
+ };
914
+ return this.#schema;
915
+ }
916
+ try {
917
+ const rows = await io.query(FINGERPRINT[this.project.dialect], []);
918
+ const value = sha256(JSON.stringify(rows)).slice(0, 16);
919
+ if (this.#schema?.value && this.#schema.value !== value) this.#answers.clear();
920
+ this.#schema = { value, at: now };
921
+ } catch (cause) {
922
+ if (cause instanceof Error && isUnreachable(cause)) {
923
+ this.#io = void 0;
924
+ this.#schema = { value: null, at: now, error: reason(cause) };
925
+ } else {
926
+ this.#schema = { value: `unverified ${process.pid} ${now}`, at: now };
927
+ }
928
+ }
929
+ return this.#schema;
930
+ }
931
+ /** Reads the fingerprint again now, however recent the last read; whether it changed. */
932
+ async recheck() {
933
+ const before = this.#schema?.value;
934
+ if (this.#schema) this.#schema = { ...this.#schema, at: -Infinity };
935
+ return (await this.schema()).value !== before;
936
+ }
937
+ /** Where the database was looked for, and where that came from; never the credentials. */
938
+ cannotConnect(why) {
939
+ const { dialect, url, root, urlSource } = this.project;
940
+ const detail = why ? `: ${why}` : "";
941
+ if (dialect === "sqlite")
942
+ return `Cannot open the database ${sqlitePath(url ?? "", root)} (${urlSource})${detail}`;
943
+ return `Cannot connect to the database at ${this.where()} (${urlSource})${detail}`;
944
+ }
945
+ notMysql(cause) {
946
+ return `Cannot type-check against the database at ${this.where()} (${this.project.urlSource}): it is ${cause.server}, and Reylo type-checks MySQL statements against MySQL. Point the type check's URL at a MySQL server, 8.4 or later, with the same schema.`;
947
+ }
948
+ /** The server, without credentials, that names this session's entries. */
949
+ #server() {
950
+ const { dialect, url, root } = this.project;
951
+ if (!url) return "";
952
+ if (dialect === "sqlite") return sqlitePath(url, root);
953
+ try {
954
+ const parsed = new URL(url);
955
+ return `${parsed.protocol}//${parsed.username}@${parsed.host}${parsed.pathname}`;
956
+ } catch {
957
+ return url;
958
+ }
959
+ }
960
+ /** The database as a person names it: host, port and database, or the SQLite file. */
961
+ where() {
962
+ const { dialect, url, root } = this.project;
963
+ if (dialect === "sqlite") return sqlitePath(url ?? "", root);
964
+ try {
965
+ const parsed = new URL(url ?? "");
966
+ const host2 = parsed.hostname || parsed.searchParams.get("host") || "localhost";
967
+ return `${host2}${parsed.port ? `:${parsed.port}` : ""}${parsed.pathname}`;
968
+ } catch {
969
+ return "the database";
970
+ }
971
+ }
972
+ async close() {
973
+ await this.release();
974
+ }
975
+ /** Closes the connection now; the next analysis opens another. */
976
+ async release() {
977
+ clearTimeout(this.#timer);
978
+ await this.#release();
979
+ }
980
+ async #release() {
981
+ const io = this.#io;
982
+ this.#io = void 0;
983
+ if (io) await io.then((open) => open.close()).catch(() => void 0);
984
+ }
985
+ #connection() {
986
+ this.#io ??= openConnection(this.project);
987
+ const io = this.#io;
988
+ io.catch(() => {
989
+ if (this.#io === io) this.#io = void 0;
990
+ });
991
+ return io;
992
+ }
993
+ };
994
+ async function openConnection(project) {
995
+ const { dialect, url, root } = project;
996
+ if (dialect === "sqlite") {
997
+ const path = sqlitePath(url ?? "", root);
998
+ const database = await openSqlite(path).catch((cause) => Promise.reject(new Unreachable(cause)));
999
+ let drivers;
1000
+ return {
1001
+ describe: () => Promise.reject(new Error("SQLite is described inside the engine")),
1002
+ query: (sql, params) => database.values(sql, params),
1003
+ async close() {
1004
+ const opened = await drivers ?? [];
1005
+ await Promise.all(opened.map((driver) => driver.close().catch(() => void 0)));
1006
+ database.close();
1007
+ },
1008
+ drivers: () => drivers ??= openProjectDrivers(root, path)
1009
+ };
1010
+ }
1011
+ const loaded = await describer(dialect, root);
1012
+ const open = dialect === "mysql" ? loaded.openMysqlDescriber : loaded.openPostgresDescriber;
1013
+ if (open === void 0) throw MissingEngine.of(dialect);
1014
+ const described = await open(url);
1015
+ return {
1016
+ describe: (sql) => described.describe(sql),
1017
+ query: (sql, params) => described.values(sql, params),
1018
+ close: () => described.close()
1019
+ };
1020
+ }
1021
+ async function openSqlite(path) {
1022
+ const { DatabaseSync: DatabaseSync3 } = await import("node:sqlite");
1023
+ const database = new DatabaseSync3(path, { readOnly: path !== ":memory:" });
1024
+ return {
1025
+ async values(sql, params) {
1026
+ if (!params.every(isSqlInputValue)) {
1027
+ throw new Error(`SQLite cannot bind a catalog query parameter: ${sql}`);
1028
+ }
1029
+ const statement = database.prepare(sql);
1030
+ statement.setReturnArrays(true);
1031
+ return statement.all(...params).map((row) => Object.values(row));
1032
+ },
1033
+ close: () => database.close()
1034
+ };
1035
+ }
1036
+ var sha256 = (text) => createHash("sha256").update(text).digest("hex");
1037
+ var StoredEntry = Schema2.fromJsonString(
1038
+ Schema2.Struct({
1039
+ version: Schema2.Literal(ENTRY_VERSION),
1040
+ dialect: Schema2.Literals(["postgres", "sqlite", "mysql"]),
1041
+ schema: Schema2.String,
1042
+ analysis: Schema2.Record(Schema2.String, Schema2.Unknown),
1043
+ at: Schema2.optionalKey(Schema2.Number)
1044
+ })
1045
+ );
1046
+ var decodeEntry = Schema2.decodeUnknownOption(StoredEntry);
1047
+ function readEntry(file) {
1048
+ if (!existsSync3(file)) return void 0;
1049
+ let text;
1050
+ try {
1051
+ text = readFileSync3(file, "utf8");
1052
+ } catch {
1053
+ return void 0;
1054
+ }
1055
+ const entry = decodeEntry(text);
1056
+ if (Option2.isNone(entry)) return void 0;
1057
+ return { ...entry.value, analysis: entry.value.analysis };
1058
+ }
1059
+ function writeEntry(root, file, entry) {
1060
+ const dir = join3(root, ".reylo");
1061
+ const temporary = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
1062
+ try {
1063
+ mkdirSync(join3(dir, "queries"), { recursive: true });
1064
+ const ignore = join3(dir, ".gitignore");
1065
+ if (!existsSync3(ignore))
1066
+ writeFileSync(
1067
+ ignore,
1068
+ "# reylo's cache: written by the type check, the build steps and reylo generate; never committed.\n*\n"
1069
+ );
1070
+ writeFileSync(temporary, `${JSON.stringify(entry)}
1071
+ `);
1072
+ renameSync(temporary, file);
1073
+ } catch {
1074
+ rmSync(temporary, { force: true });
1075
+ }
1076
+ }
1077
+ var NS = `not in ('pg_catalog', 'information_schema') and n.nspname !~ '^pg_(toast|temp)'`;
1078
+ var FINGERPRINT = {
1079
+ postgres: `select md5(coalesce(string_agg(x, E'\\n' order by x), '')) from (
1080
+ select 'v ' || current_setting('server_version_num') || ' ' || current_setting('search_path') as x
1081
+ union all select 'r ' || c.oid || ' ' || n.nspname || '.' || c.relname || ' ' || c.relkind::text from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname ${NS}
1082
+ union all select 'a ' || a.attrelid || ' ' || a.attnum || ' ' || a.attname || ' ' || a.atttypid || ' ' || a.attnotnull || ' ' || a.attcollation from pg_attribute a join pg_class c on c.oid = a.attrelid join pg_namespace n on n.oid = c.relnamespace where a.attnum > 0 and not a.attisdropped and n.nspname ${NS}
1083
+ union all select 'h ' || h.inhrelid || ' ' || h.inhparent || ' ' || h.inhseqno from pg_inherits h join pg_class c on c.oid = h.inhrelid join pg_namespace n on n.oid = c.relnamespace where n.nspname ${NS}
1084
+ union all select 'c ' || o.conrelid || ' ' || o.contypid || ' ' || o.conname || ' ' || pg_get_constraintdef(o.oid) from pg_constraint o join pg_namespace n on n.oid = o.connamespace where n.nspname ${NS}
1085
+ union all select 'i ' || i.indexrelid || ' ' || i.indisvalid || ' ' || pg_get_indexdef(i.indexrelid) from pg_index i join pg_class c on c.oid = i.indrelid join pg_namespace n on n.oid = c.relnamespace where n.nspname ${NS}
1086
+ union all select 'g ' || g.tgrelid || ' ' || g.tgname || ' ' || g.tgtype || ' ' || g.tgenabled::text from pg_trigger g join pg_class c on c.oid = g.tgrelid join pg_namespace n on n.oid = c.relnamespace where not g.tgisinternal and n.nspname ${NS}
1087
+ union all select 'u ' || w.ev_class || ' ' || w.rulename || ' ' || w.ev_type::text || ' ' || w.ev_enabled::text || ' ' || md5(pg_get_ruledef(w.oid)) from pg_rewrite w join pg_class c on c.oid = w.ev_class join pg_namespace n on n.oid = c.relnamespace where w.rulename <> '_RETURN' and n.nspname ${NS}
1088
+ union all select 'o ' || o.oid || ' ' || o.oprname || ' ' || o.oprleft || ' ' || o.oprright || ' ' || o.oprcode::oid from pg_operator o where o.oid >= 16384
1089
+ union all select 'e ' || e.enumtypid || ' ' || e.enumsortorder || ' ' || e.enumlabel from pg_enum e
1090
+ union all select 't ' || t.oid || ' ' || t.typname || ' ' || t.typtype::text || ' ' || t.typbasetype || ' ' || t.typnotnull from pg_type t join pg_namespace n on n.oid = t.typnamespace where n.nspname ${NS}
1091
+ union all select 'f ' || p.oid || ' ' || p.proname || ' ' || p.prorettype || ' ' || p.proretset || ' ' || p.proargtypes::text || ' ' || md5(coalesce(p.prosrc, '')) from pg_proc p join pg_namespace n on n.oid = p.pronamespace where n.nspname ${NS}
1092
+ union all select 'w ' || c.oid || ' ' || md5(pg_get_viewdef(c.oid)) from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind in ('v', 'm') and n.nspname ${NS}
1093
+ union all select 'd ' || d.objoid || ' ' || d.objsubid || ' ' || md5(d.description) from pg_description d join pg_class c on c.oid = d.objoid join pg_namespace n on n.oid = c.relnamespace where d.classoid = 'pg_class'::regclass and d.objsubid > 0 and n.nspname ${NS}
1094
+ ) t`,
1095
+ mysql: `select concat_ws(' ', @@version, @@sql_mode, count(*), sum(crc32(x)), bit_xor(crc32(x))) from (
1096
+ select concat_ws(' ', 'c', table_name, column_name, ordinal_position, column_type, is_nullable, column_key, extra, coalesce(generation_expression, ''), md5(column_comment)) as x from information_schema.columns where table_schema = database()
1097
+ union all select concat_ws(' ', 's', table_name, index_name, non_unique, seq_in_index, column_name) from information_schema.statistics where table_schema = database()
1098
+ union all select concat_ws(' ', 'v', table_name, md5(view_definition)) from information_schema.views where table_schema = database()
1099
+ union all select concat_ws(' ', 'r', routine_name, routine_type, coalesce(dtd_identifier, ''), md5(coalesce(routine_definition, ''))) from information_schema.routines where routine_schema = database()
1100
+ ) t`,
1101
+ sqlite: `select type, name, tbl_name, coalesce(sql, '') from sqlite_schema
1102
+ union all select 'function', name, type, narg from pragma_function_list order by 1, 2, 3, 4`
1103
+ };
1104
+
1105
+ // src/schema-files.ts
1106
+ import { createHash as createHash2 } from "node:crypto";
1107
+ import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
1108
+ import { Effect as Effect2, FileSystem, Path, Semaphore } from "effect";
1109
+
1110
+ // src/schema-postgres.ts
1111
+ import { createRequire as createRequire3 } from "node:module";
1112
+ import { join as join4 } from "node:path";
1113
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
1114
+ import {
1115
+ messages as pgMessages,
1116
+ PGlite
1117
+ } from "@electric-sql/pglite";
1118
+ import { Effect, Option as Option3, Predicate as Predicate3, Schema as Schema3 } from "effect";
1119
+
1120
+ // src/sql-tokens.ts
1121
+ function* sqlTokens(sql, dialect = "sqlite", comments = false) {
1122
+ for (let index = 0; index < sql.length; ) {
1123
+ const start = index;
1124
+ const ch = sql[index];
1125
+ if (/\s/u.test(ch)) {
1126
+ index++;
1127
+ continue;
1128
+ }
1129
+ if (sql.startsWith("--", index) && (dialect !== "mysql" || /\s/u.test(sql[index + 2] ?? "\n")) || dialect === "mysql" && ch === "#") {
1130
+ const end = sql.indexOf("\n", index);
1131
+ index = end < 0 ? sql.length : end + 1;
1132
+ if (comments) yield { kind: "comment", text: sql.slice(start, index), start, end: index };
1133
+ continue;
1134
+ }
1135
+ if (sql.startsWith("/*", index)) {
1136
+ const executable = dialect === "mysql" && sql[index + 2] === "!";
1137
+ let depth = 1;
1138
+ index += 2;
1139
+ while (index < sql.length && depth > 0) {
1140
+ if (dialect === "postgres" && sql.startsWith("/*", index)) {
1141
+ depth++;
1142
+ index += 2;
1143
+ } else if (sql.startsWith("*/", index)) {
1144
+ depth--;
1145
+ index += 2;
1146
+ } else index++;
1147
+ }
1148
+ if (comments) yield { kind: "comment", text: sql.slice(start, index), start, end: index };
1149
+ else if (executable) {
1150
+ const prefix = /^\/\*!\d*\s*/u.exec(sql.slice(start))[0].length;
1151
+ const offset = start + prefix;
1152
+ const body = sql.slice(offset, depth === 0 ? index - 2 : index);
1153
+ for (const token of sqlTokens(body, dialect)) {
1154
+ yield { ...token, start: token.start + offset, end: token.end + offset };
1155
+ }
1156
+ }
1157
+ continue;
1158
+ }
1159
+ const escaped = dialect === "postgres" && /^[eE]'/.test(sql.slice(index, index + 2));
1160
+ const quote = escaped ? "'" : ch;
1161
+ if (quote === "'" || quote === '"' || quote === "`" || dialect === "sqlite" && quote === "[") {
1162
+ const close = quote === "[" ? "]" : quote;
1163
+ const backslash = escaped || dialect === "mysql" && quote !== "`";
1164
+ index += escaped ? 2 : 1;
1165
+ while (index < sql.length) {
1166
+ if (backslash && sql[index] === "\\") index += 2;
1167
+ else if (sql[index++] === close) {
1168
+ if (quote !== "[" && sql[index] === close) index++;
1169
+ else break;
1170
+ }
1171
+ }
1172
+ yield { kind: "quoted", text: sql.slice(start, index), start, end: index };
1173
+ continue;
1174
+ }
1175
+ if (ch === "$" && dialect !== "mysql") {
1176
+ const tag = /^\$(?:[A-Za-z_][\w]*)?\$/u.exec(sql.slice(index))?.[0];
1177
+ if (tag !== void 0) {
1178
+ const end = sql.indexOf(tag, index + tag.length);
1179
+ index = end < 0 ? sql.length : end + tag.length;
1180
+ yield { kind: "quoted", text: sql.slice(start, index), start, end: index };
1181
+ continue;
1182
+ }
1183
+ }
1184
+ if (/[A-Za-z_]/u.test(ch)) {
1185
+ while (/[\w$]/u.test(sql[++index] ?? "")) {
1186
+ }
1187
+ yield { kind: "word", text: sql.slice(start, index), start, end: index };
1188
+ } else {
1189
+ index++;
1190
+ yield { kind: "symbol", text: ch, start, end: index };
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ // src/split-sql.ts
1196
+ var STATEMENT_START = /^(?:select|insert|update|delete|replace|with|end)\b/iu;
1197
+ var COMPOUND_END = /* @__PURE__ */ new Set(["IF", "LOOP", "WHILE", "REPEAT", "FOREACH", "CASE"]);
1198
+ function splitSql(sql, dialect = "sqlite") {
1199
+ const statements = [];
1200
+ const tokens2 = [...sqlTokens(sql, dialect)];
1201
+ let start = 0;
1202
+ let hasSql = false;
1203
+ let caseDepth = 0;
1204
+ let mode = "sql";
1205
+ let body = 0;
1206
+ let routineDepth = 0;
1207
+ let statementStart = true;
1208
+ for (const [index, token] of tokens2.entries()) {
1209
+ if (token.kind === "symbol" && token.text === ";") {
1210
+ if (body === 0 && routineDepth === 0 && caseDepth === 0) {
1211
+ if (hasSql) statements.push(sql.slice(start, token.start).trim());
1212
+ start = token.end;
1213
+ hasSql = false;
1214
+ mode = "sql";
1215
+ }
1216
+ statementStart = true;
1217
+ continue;
1218
+ }
1219
+ hasSql = true;
1220
+ const initial = statementStart;
1221
+ statementStart = false;
1222
+ if (token.kind !== "word") continue;
1223
+ const upper = token.text.toUpperCase();
1224
+ const next = tokens2[index + 1];
1225
+ const nextWord = next?.kind === "word" ? next.text.toUpperCase() : "";
1226
+ if (mode === "routine") {
1227
+ if (upper === "CASE" && (!initial || caseDepth > 0) && tokens2[index - 1]?.text.toUpperCase() !== "END") {
1228
+ caseDepth++;
1229
+ } else if (upper === "END" && caseDepth > 0) {
1230
+ caseDepth--;
1231
+ continue;
1232
+ }
1233
+ if (caseDepth === 0 && ["THEN", "ELSE", "DO", "LOOP", "REPEAT"].includes(upper)) {
1234
+ statementStart = true;
1235
+ }
1236
+ if (upper === "BEGIN" && (routineDepth === 0 || initial)) {
1237
+ routineDepth++;
1238
+ statementStart = true;
1239
+ } else if (upper === "END" && routineDepth > 0 && initial && !COMPOUND_END.has(nextWord)) {
1240
+ routineDepth--;
1241
+ }
1242
+ } else if (upper === "CASE") caseDepth++;
1243
+ else if (upper === "END") {
1244
+ if (caseDepth > 0) caseDepth--;
1245
+ else if (body > 0 && initial) body = 0;
1246
+ } else if (upper === "BEGIN" && mode === "trigger" && STATEMENT_START.test(nextWord)) {
1247
+ body = 1;
1248
+ statementStart = true;
1249
+ } else if (initial && upper === "CREATE") mode = "create";
1250
+ else if (mode === "create") {
1251
+ if (upper === "TRIGGER") mode = dialect === "mysql" ? "routine" : "trigger";
1252
+ else if (upper === "PROCEDURE" || upper === "FUNCTION") mode = "routine";
1253
+ else if (upper !== "TEMP" && upper !== "TEMPORARY" && upper !== "OR" && upper !== "REPLACE")
1254
+ mode = "sql";
1255
+ }
1256
+ }
1257
+ if (hasSql) statements.push(sql.slice(start).trim());
1258
+ return statements;
1259
+ }
1260
+
1261
+ // src/schema-postgres.ts
1262
+ var SchemaLoadError = class _SchemaLoadError extends Schema3.TaggedError("reylo-effect/SchemaLoadError")("SchemaLoadError", {
1263
+ message: Schema3.String,
1264
+ cause: Schema3.optionalKey(Schema3.Defect())
1265
+ }) {
1266
+ static forFile(file, cause) {
1267
+ return new _SchemaLoadError({
1268
+ message: `Cannot load the schema: ${file}: ${describeCause(cause)}`,
1269
+ cause
1270
+ });
1271
+ }
1272
+ };
1273
+ function describeCause(cause) {
1274
+ return cause instanceof Error ? cause.message : String(cause);
1275
+ }
1276
+ var MissingRole = class extends Schema3.TaggedError(
1277
+ "reylo-effect/MissingRole"
1278
+ )("MissingRole", {
1279
+ role: Schema3.String,
1280
+ file: Schema3.String,
1281
+ cause: Schema3.Defect()
1282
+ }) {
1283
+ };
1284
+ var MAX_ROLES = 64;
1285
+ var describePostgres = Effect.fn("SchemaPostgres.describe")(function* (database, sql, parameterTypes = []) {
1286
+ const text = Buffer.from(sql);
1287
+ const parse2 = Buffer.alloc(text.length + 9 + parameterTypes.length * 4);
1288
+ parse2[0] = 80;
1289
+ parse2.writeInt32BE(parse2.length - 1, 1);
1290
+ text.copy(parse2, 6);
1291
+ parse2.writeUInt16BE(parameterTypes.length, text.length + 7);
1292
+ parameterTypes.forEach((oid, index) => parse2.writeUInt32BE(oid, text.length + 9 + index * 4));
1293
+ const describe = Buffer.from([68, 0, 0, 0, 6, 83, 0]);
1294
+ const sync2 = Buffer.from([83, 0, 0, 0, 4]);
1295
+ const { messages } = yield* Effect.promise(
1296
+ () => database.execProtocol(Buffer.concat([parse2, describe, sync2]), { throwOnError: false })
1297
+ );
1298
+ const result = { params: [], columns: [] };
1299
+ for (const message of messages) {
1300
+ if (message.name === "error") return yield* Effect.fail(message);
1301
+ if (message instanceof pgMessages.ParameterDescriptionMessage) {
1302
+ result.params = message.dataTypeIDs.map((oid) => ({ oid }));
1303
+ }
1304
+ if (message instanceof pgMessages.RowDescriptionMessage) {
1305
+ result.columns = message.fields.map((field) => ({
1306
+ name: field.name,
1307
+ oid: field.dataTypeID,
1308
+ table: field.tableID,
1309
+ attnum: field.columnID
1310
+ }));
1311
+ }
1312
+ }
1313
+ return result;
1314
+ });
1315
+ var loadPostgresSchema = (texts, root) => Effect.flatMap(
1316
+ extensionsOf(texts, root),
1317
+ (extensions) => openWithRoles(texts, extensions, /* @__PURE__ */ new Set())
1318
+ );
1319
+ var openWithRoles = (texts, extensions, roles) => Effect.gen(function* () {
1320
+ const database = yield* Effect.promise(() => PGlite.create({ extensions }));
1321
+ const close = Effect.promise(() => database.close());
1322
+ yield* Effect.onError(populate(database, texts, roles), () => close);
1323
+ yield* Effect.addFinalizer(() => close);
1324
+ return database;
1325
+ }).pipe(
1326
+ Effect.catchTag(
1327
+ "MissingRole",
1328
+ (missing) => roles.has(missing.role) || roles.size >= MAX_ROLES ? SchemaLoadError.forFile(missing.file, missing.cause) : openWithRoles(texts, extensions, /* @__PURE__ */ new Set([...roles, missing.role]))
1329
+ )
1330
+ );
1331
+ var populate = Effect.fn("SchemaPostgres.populate")(function* (database, texts, roles) {
1332
+ for (const role of roles) {
1333
+ yield* run(database, `create role "${role.replaceAll('"', '""')}" superuser`, "schema");
1334
+ }
1335
+ for (const source of texts) {
1336
+ yield* run(database, source.text, source.file);
1337
+ yield* run(database, "reset all; set session authorization default", source.file);
1338
+ }
1339
+ });
1340
+ var run = (database, sql, file) => Effect.tryPromise({
1341
+ try: () => database.exec(sql),
1342
+ catch: (cause) => {
1343
+ const missing = /^role "(.+)" does not exist$/u.exec(describeCause(cause))?.[1];
1344
+ return missing === void 0 ? SchemaLoadError.forFile(file, cause) : new MissingRole({ role: missing, file, cause });
1345
+ }
1346
+ });
1347
+ var extensionsOf = Effect.fn("SchemaPostgres.extensions")(function* (texts, root) {
1348
+ const extensions = {};
1349
+ const require2 = createRequire3(join4(root, "package.json"));
1350
+ for (const { text } of texts) {
1351
+ for (const statement of splitSql(text, "postgres")) {
1352
+ const key = extensionKey(statement);
1353
+ if (key === void 0) continue;
1354
+ const specifier = key === "vector" ? "@electric-sql/pglite-pgvector" : `@electric-sql/pglite/contrib/${key}`;
1355
+ const path = Option3.orElse(
1356
+ resolveFrom2(require2, specifier),
1357
+ () => resolveFrom2(createRequire3(import.meta.url), specifier)
1358
+ );
1359
+ if (Option3.isNone(path)) continue;
1360
+ const loaded = yield* Effect.promise(() => import(pathToFileURL3(path.value).href));
1361
+ const extension = Predicate3.isReadonlyObject(loaded) ? loaded[key] : void 0;
1362
+ if (isExtension(extension)) extensions[key] = extension;
1363
+ }
1364
+ }
1365
+ return extensions;
1366
+ });
1367
+ function extensionKey(statement) {
1368
+ const tokens2 = [...sqlTokens(statement, "postgres")];
1369
+ if (tokens2[0]?.text.toLowerCase() !== "create" || tokens2[1]?.text.toLowerCase() !== "extension")
1370
+ return void 0;
1371
+ const raw = tokens2[tokens2[2]?.text.toLowerCase() === "if" ? 5 : 2]?.text;
1372
+ if (raw === void 0) return void 0;
1373
+ const name = raw.startsWith('"') ? raw.slice(1, -1).replaceAll('""', '"') : raw.toLowerCase();
1374
+ return name.replaceAll("-", "_");
1375
+ }
1376
+ function resolveFrom2(require2, specifier) {
1377
+ return Option3.liftThrowable(() => require2.resolve(specifier))();
1378
+ }
1379
+ function isExtension(value) {
1380
+ return Predicate3.isReadonlyObject(value) && Predicate3.isString(value.name) && Predicate3.isFunction(value.setup);
1381
+ }
1382
+
1383
+ // src/schema-files.ts
1384
+ var GLOB = /[*?[\]{}()\\]|^!/u;
1385
+ var schemaFiles = Effect2.fn("SchemaFiles.find")(function* (root, patterns) {
1386
+ const fs = yield* FileSystem.FileSystem;
1387
+ const path = yield* Path.Path;
1388
+ const files = /* @__PURE__ */ new Set();
1389
+ for (const pattern of patterns) {
1390
+ const glob = GLOB.test(pattern);
1391
+ const paths = glob ? (yield* fs.glob(pattern, { root, exclude: ["**/node_modules/**"] })).map(
1392
+ (name) => path.resolve(root, name)
1393
+ ) : [path.resolve(root, pattern)];
1394
+ const found = (yield* Effect2.filter(paths, (candidate) => isSchemaFile(fs, candidate))).sort(
1395
+ (a, b) => natural(path.relative(root, a), path.relative(root, b))
1396
+ );
1397
+ if (found.length === 0) {
1398
+ return {
1399
+ files: [...files],
1400
+ error: `"schema" in .reylo.yaml names ${pattern}, which ${glob ? "matches no file" : "does not exist"}`
1401
+ };
1402
+ }
1403
+ for (const file of found) files.add(file);
1404
+ }
1405
+ return { files: [...files] };
1406
+ });
1407
+ function isSchemaFile(fs, candidate) {
1408
+ if (/\.down\.sql$/iu.test(candidate)) return Effect2.succeed(false);
1409
+ return fs.stat(candidate).pipe(
1410
+ Effect2.map((info) => info.type === "File"),
1411
+ Effect2.orElseSucceed(() => false)
1412
+ );
1413
+ }
1414
+ function natural(left, right) {
1415
+ const a = left.match(/\d+|\D+/gu) ?? [];
1416
+ const b = right.match(/\d+|\D+/gu) ?? [];
1417
+ for (let index = 0; index < Math.min(a.length, b.length); index++) {
1418
+ const x = a[index];
1419
+ const y = b[index];
1420
+ if (x === y) continue;
1421
+ if (/^\d/u.test(x) && /^\d/u.test(y)) {
1422
+ const delta = BigInt(x) - BigInt(y);
1423
+ if (delta !== 0n) return delta < 0n ? -1 : 1;
1424
+ } else return x < y ? -1 : 1;
1425
+ }
1426
+ return a.length - b.length;
1427
+ }
1428
+ var schemaFingerprint = Effect2.fn("SchemaFiles.fingerprint")(function* (root, files, dialect) {
1429
+ const fs = yield* FileSystem.FileSystem;
1430
+ const path = yield* Path.Path;
1431
+ const hash = createHash2("sha256").update(
1432
+ JSON.stringify(["schema-text/2", dialect, process.versions.sqlite, "pglite/0.5.11"])
1433
+ );
1434
+ for (const file of files) {
1435
+ hash.update(path.relative(root, file)).update("\0").update(yield* fs.readFile(file)).update("\0");
1436
+ }
1437
+ return hash.digest("hex");
1438
+ });
1439
+ var openSchemaDatabase = (dialect, files, root) => Effect2.gen(function* () {
1440
+ if (dialect === "mysql") {
1441
+ return yield* new SchemaLoadError({
1442
+ message: '"schema" loads a Postgres or SQLite schema; MySQL needs a MySQL 8.4 server. Remove schema and set direct_url or DATABASE_URL.'
1443
+ });
1444
+ }
1445
+ const fs = yield* FileSystem.FileSystem;
1446
+ const texts = yield* Effect2.forEach(
1447
+ files,
1448
+ (file) => Effect2.map(fs.readFileString(file), (text) => ({
1449
+ file,
1450
+ text: schemaText(text, dialect)
1451
+ }))
1452
+ );
1453
+ if (dialect === "sqlite") return yield* openSqlite2(texts);
1454
+ const database = yield* loadPostgresSchema(texts, root);
1455
+ const session = yield* Semaphore.make(1);
1456
+ const serial = session.withPermits(1);
1457
+ const runPromise = Effect2.runPromiseWith(yield* Effect2.context());
1458
+ return {
1459
+ describe: (sql) => runPromise(serial(describePostgres(database, sql))),
1460
+ describeParameters: (sql, parameterTypes) => runPromise(serial(describePostgres(database, sql, parameterTypes))),
1461
+ query: (sql, params) => runPromise(
1462
+ serial(
1463
+ Effect2.promise(async () => {
1464
+ const result = await database.query(sql, params, {
1465
+ rowMode: "array",
1466
+ parsers: { 20: (text) => text }
1467
+ });
1468
+ return result.rows;
1469
+ })
1470
+ )
1471
+ )
1472
+ };
1473
+ });
1474
+ var openSqlite2 = Effect2.fn("SchemaFiles.openSqlite")(function* (texts) {
1475
+ const database = yield* Effect2.acquireRelease(
1476
+ Effect2.sync(() => new DatabaseSync2(":memory:")),
1477
+ (open) => Effect2.sync(() => open.close())
1478
+ );
1479
+ for (const { file, text } of texts) {
1480
+ yield* Effect2.try({
1481
+ try: () => database.exec(text),
1482
+ catch: (cause) => SchemaLoadError.forFile(file, cause)
1483
+ });
1484
+ }
1485
+ return {
1486
+ async describe() {
1487
+ throw new Error("The engine describes SQLite internally");
1488
+ },
1489
+ async query(sql, params) {
1490
+ if (!params.every(isSqlInputValue)) {
1491
+ throw new Error(`SQLite cannot bind a catalog query parameter: ${sql}`);
1492
+ }
1493
+ const statement = database.prepare(sql);
1494
+ statement.setReturnArrays(true);
1495
+ return statement.all(...params).map((row) => Object.values(row));
1496
+ }
1497
+ };
1498
+ });
1499
+ function schemaText(text, dialect) {
1500
+ const tokens2 = [...sqlTokens(text, dialect, true)];
1501
+ const down = tokens2.find(
1502
+ (token) => token.kind === "comment" && /^[ \t]*$/u.test(text.slice(text.lastIndexOf("\n", token.start - 1) + 1, token.start)) && /^(?:--[ \t]*(?:migrate:down|\+goose[ \t]+down|\+migrate[ \t]+down)\b|----[ \t]*create above[ \t]*\/[ \t]*drop below[ \t]*----)/iu.test(
1503
+ token.text
1504
+ )
1505
+ );
1506
+ const up = down === void 0 ? text : text.slice(0, down.start);
1507
+ const protectedTokens = tokens2.filter(
1508
+ (token) => token.kind === "quoted" || token.kind === "comment"
1509
+ );
1510
+ const strip = dialect === "postgres" ? /^\\(?:un)?restrict\b.*$/gmu : /^[ \t]*create[ \t]+table[ \t]+(?:if[ \t]+not[ \t]+exists[ \t]+)?["'`[]?sqlite_sequence["'`\]]?[ \t]*\([^;]*\);?/gimu;
1511
+ return up.replace(
1512
+ strip,
1513
+ (match, offset) => protectedTokens.some((token) => token.start <= offset && offset < token.end) ? match : ""
1514
+ );
1515
+ }
1516
+
1517
+ // src/analysis.ts
1518
+ var ReyloError = class _ReyloError extends Schema4.TaggedError(
1519
+ "reylo-effect/ReyloError"
1520
+ )("ReyloError", {
1521
+ message: Schema4.String,
1522
+ cause: Schema4.Defect()
1523
+ }) {
1524
+ static from = (cause) => new _ReyloError({ message: describeCause(cause), cause });
1525
+ };
1526
+ var LexError = class extends Schema4.TaggedError(
1527
+ "reylo-effect/LexError"
1528
+ )("LexError", {
1529
+ message: Schema4.String,
1530
+ cause: Schema4.Defect()
1531
+ }) {
1532
+ };
1533
+ var SchemaSetting = Schema4.Union([Schema4.String, Schema4.NonEmptyArray(Schema4.String)]);
1534
+ var IntegersSetting = Schema4.Literals(["bigint", "number"]);
1535
+ var OWN_KEYS = [
1536
+ "schema",
1537
+ "integers",
1538
+ "postgres_numbers",
1539
+ "retry_reads",
1540
+ "result_names",
1541
+ "offline"
1542
+ ];
1543
+ var StoredAnalysis = Schema4.fromJsonString(
1544
+ Schema4.Struct({ identity: Schema4.String, analysis: Schema4.Unknown })
1545
+ );
1546
+ function findProject(from) {
1547
+ const configured = process.env.REYLO_CONFIG;
1548
+ if (configured) return { dir: packageDir(from), file: resolve2(configured) };
1549
+ let owner;
1550
+ for (let dir = resolve2(from); ; dir = dirname3(dir)) {
1551
+ const file = PROJECT_FILES.map((name) => join5(dir, name)).find((path) => existsSync4(path));
1552
+ if (file !== void 0) return { dir, file };
1553
+ if (owner === void 0 && existsSync4(join5(dir, "package.json")))
1554
+ owner = { dir, stop: workspaceRoot(dir) ?? dir };
1555
+ if (dir === owner?.stop) return { dir: owner.dir };
1556
+ if (dirname3(dir) === dir) return { dir: resolve2(from) };
1557
+ }
1558
+ }
1559
+ var WorkspacePatterns = Schema4.Array(Schema4.String);
1560
+ var PackageWorkspaces = Schema4.Struct({
1561
+ workspaces: Schema4.Union([WorkspacePatterns, Schema4.Struct({ packages: WorkspacePatterns })])
1562
+ });
1563
+ var PnpmWorkspace = Schema4.Struct({ packages: WorkspacePatterns });
1564
+ function workspaceRoot(member) {
1565
+ for (let dir = member; dirname3(dir) !== dir; ) {
1566
+ dir = dirname3(dir);
1567
+ const patterns = workspacePatterns(dir);
1568
+ if (patterns !== void 0) return listed(relative2(dir, member), patterns) ? dir : void 0;
1569
+ }
1570
+ return void 0;
1571
+ }
1572
+ function listed(path, patterns) {
1573
+ const posixPath = path.split(sep).join("/");
1574
+ const matches = (pattern) => posix.matchesGlob(posixPath, pattern.replace(/^\.\//u, "").replace(/\/+$/u, ""));
1575
+ return patterns.some((pattern) => !pattern.startsWith("!") && matches(pattern)) && !patterns.some((pattern) => pattern.startsWith("!") && matches(pattern.slice(1)));
1576
+ }
1577
+ function workspacePatterns(dir) {
1578
+ const pnpm = join5(dir, "pnpm-workspace.yaml");
1579
+ if (existsSync4(pnpm)) {
1580
+ let parsed2;
1581
+ try {
1582
+ parsed2 = parseDocument(readFileSync4(pnpm, "utf8")).toJS();
1583
+ } catch {
1584
+ return [];
1585
+ }
1586
+ return Option4.match(Schema4.decodeUnknownOption(PnpmWorkspace)(parsed2), {
1587
+ onNone: () => [],
1588
+ onSome: (workspace) => workspace.packages
1589
+ });
1590
+ }
1591
+ const manifest = join5(dir, "package.json");
1592
+ if (!existsSync4(manifest)) return void 0;
1593
+ let parsed;
1594
+ try {
1595
+ parsed = JSON.parse(readFileSync4(manifest, "utf8"));
1596
+ } catch {
1597
+ return void 0;
1598
+ }
1599
+ return Option4.match(Schema4.decodeUnknownOption(PackageWorkspaces)(parsed), {
1600
+ onNone: () => void 0,
1601
+ onSome: ({ workspaces }) => "packages" in workspaces ? workspaces.packages : workspaces
1602
+ });
1603
+ }
1604
+ function projectDirectory(from) {
1605
+ return findProject(from).dir;
1606
+ }
1607
+ function resolveProject(host2, dir) {
1608
+ const found = findProject(dir);
1609
+ const both = PROJECT_FILES.map((name) => join5(found.dir, name));
1610
+ const duplicate = !process.env.REYLO_CONFIG && both.every((path) => existsSync4(path)) ? `${both.map((path) => relative2(dir, path) || path).join(" and ")} are both project files; keep one.` : void 0;
1611
+ const env = found.file === void 0 || process.env.REYLO_CONFIG ? process.env : { ...process.env, REYLO_CONFIG: found.file };
1612
+ if (host2 instanceof MissingEngine) {
1613
+ const project2 = resolveNativeProject(host2, dir, env);
1614
+ return duplicate === void 0 ? project2 : { ...project2, error: duplicate };
1615
+ }
1616
+ let schema;
1617
+ let integers;
1618
+ let postgresNumbers;
1619
+ let retryReads;
1620
+ let resultNames;
1621
+ let offline;
1622
+ let error;
1623
+ const configText = (text) => {
1624
+ const document = parseDocument(text);
1625
+ if (document.errors.length > 0 || !OWN_KEYS.some((name) => document.has(name))) return text;
1626
+ const settings = document.toJS();
1627
+ const read = (name) => Predicate4.isObject(settings) ? settings[name] : void 0;
1628
+ if (document.has("schema")) {
1629
+ const configured = Schema4.decodeUnknownOption(SchemaSetting)(read("schema"));
1630
+ if (Option4.isNone(configured))
1631
+ error = '"schema" in .reylo.yaml must be a nonempty list of file paths or glob patterns';
1632
+ else if (Predicate4.isString(configured.value)) schema = [configured.value];
1633
+ else schema = configured.value;
1634
+ }
1635
+ if (document.has("integers")) {
1636
+ const configured = Schema4.decodeUnknownOption(IntegersSetting)(read("integers"));
1637
+ if (Option4.isNone(configured))
1638
+ error ??= '"integers" in .reylo.yaml must be "bigint" or "number"';
1639
+ else integers = configured.value;
1640
+ }
1641
+ if (document.has("postgres_numbers")) {
1642
+ if (read("postgres_numbers") !== "number")
1643
+ error ??= '"postgres_numbers" in .reylo.yaml must be "number"';
1644
+ else postgresNumbers = "number";
1645
+ }
1646
+ if (document.has("retry_reads")) {
1647
+ const configured = read("retry_reads");
1648
+ if (!Predicate4.isBoolean(configured))
1649
+ error ??= '"retry_reads" in .reylo.yaml must be a boolean';
1650
+ else retryReads = configured;
1651
+ }
1652
+ if (document.has("result_names")) {
1653
+ if (read("result_names") !== "camelCase")
1654
+ error ??= '"result_names" in .reylo.yaml must be "camelCase"';
1655
+ else resultNames = "camelCase";
1656
+ }
1657
+ if (document.has("offline")) {
1658
+ const configured = read("offline");
1659
+ if (!Predicate4.isBoolean(configured)) error ??= '"offline" in .reylo.yaml must be a boolean';
1660
+ else offline = configured;
1661
+ }
1662
+ return withoutOwnKeys(text, document);
1663
+ };
1664
+ const project = resolveNativeProject(host2, dir, env, configText);
1665
+ const dialect = integers === "number" && project.dialect !== "sqlite" ? `"integers: number" in .reylo.yaml applies to SQLite; this project is ${project.dialect}` : void 0;
1666
+ const numbers = postgresNumbers !== void 0 && project.dialect !== "postgres" ? '"postgres_numbers" in .reylo.yaml applies only to PostgreSQL' : void 0;
1667
+ return {
1668
+ ...project,
1669
+ schema,
1670
+ integers,
1671
+ postgresNumbers,
1672
+ retryReads,
1673
+ resultNames,
1674
+ // The environment decides over the file, so one checkout serves CI and production.
1675
+ offline: offlineFrom(env) ?? offline ?? false,
1676
+ error: duplicate ?? error ?? project.error ?? dialect ?? numbers
1677
+ };
1678
+ }
1679
+ var utf82 = new TextEncoder();
1680
+ function withoutOwnKeys(text, document) {
1681
+ const contents = document.contents;
1682
+ if (!isMap(contents)) return text;
1683
+ let blanked = text;
1684
+ for (const pair of contents.items.toReversed()) {
1685
+ const key = pair.key;
1686
+ if (!isScalar(key) || !OWN_KEYS.some((name) => name === key.value)) continue;
1687
+ let start = key.range?.[0];
1688
+ let end = (isNode(pair.value) ? pair.value.range?.[1] : void 0) ?? key.range?.[1];
1689
+ if (start === void 0 || end === void 0) continue;
1690
+ if (contents.flow === true) {
1691
+ const after = /^\s*,/u.exec(blanked.slice(end));
1692
+ const before = /,\s*$/u.exec(blanked.slice(0, start));
1693
+ if (after) end += after[0].length;
1694
+ else if (before) start = before.index;
1695
+ }
1696
+ const spaced = blanked.slice(start, end).replace(/[^\n]/gu, (char) => " ".repeat(utf82.encode(char).length));
1697
+ blanked = blanked.slice(0, start) + spaced + blanked.slice(end);
1698
+ }
1699
+ return blanked;
1700
+ }
1701
+ var anyEngineFor = (from) => Effect3.tryPromise({ try: () => anyEngine(from), catch: ReyloError.from });
1702
+ var engineFor = (project) => Effect3.tryPromise({ try: () => engine(project.dialect, project.root), catch: ReyloError.from });
1703
+ var nativeSession = Effect3.fn("Analysis.nativeSession")(function* (project, idle) {
1704
+ const own = yield* engineFor(project).pipe(
1705
+ Effect3.catch(
1706
+ (failure2) => Effect3.succeed(
1707
+ failure2.cause instanceof MissingEngine ? failure2.cause : new MissingEngine(failure2.message)
1708
+ )
1709
+ )
1710
+ );
1711
+ const native = new NativeSession(own, project, idle);
1712
+ const session = {
1713
+ project,
1714
+ lex: (sql) => Effect3.try({
1715
+ try: () => native.lex(sql),
1716
+ catch: (cause) => new LexError({ message: describeCause(cause), cause })
1717
+ }),
1718
+ schema: Effect3.promise(() => native.schema()),
1719
+ recheck: Effect3.promise(() => native.recheck()),
1720
+ analyze: (sql) => Effect3.promise(() => native.analyze(sql)),
1721
+ release: Effect3.promise(() => native.release()),
1722
+ close: Effect3.promise(() => native.close())
1723
+ };
1724
+ return session;
1725
+ });
1726
+ var BUILD_IDLE = 250;
1727
+ var sessions = /* @__PURE__ */ new Map();
1728
+ var sessionFor = Effect3.fn("Analysis.session")(function* (project, idle) {
1729
+ if (project.schema === void 0 && project.error === void 0) {
1730
+ return yield* nativeSession(project, idle);
1731
+ }
1732
+ return yield* fileSession(project, yield* engineFor(project));
1733
+ });
1734
+ var Session = {
1735
+ open: Effect3.fn("Analysis.Session.open")(function* (from) {
1736
+ return yield* sessionFor(resolveProject(yield* anyEngineFor(from), from), Infinity);
1737
+ }),
1738
+ /** Not cached process-wide, nor released by `releaseConnections`. */
1739
+ owned: Effect3.fn("Analysis.Session.owned")(function* (from) {
1740
+ const root = projectDirectory(from);
1741
+ return yield* sessionFor(resolveProject(yield* anyEngineFor(root), root), 2e3);
1742
+ }),
1743
+ for: Effect3.fn("Analysis.Session.for")(function* (from) {
1744
+ const root = projectDirectory(from);
1745
+ const project = resolveProject(yield* anyEngineFor(root), root);
1746
+ const key = JSON.stringify(project);
1747
+ const existing = sessions.get(key);
1748
+ if (existing !== void 0) return existing;
1749
+ const session = yield* sessionFor(project, BUILD_IDLE);
1750
+ sessions.set(key, session);
1751
+ return session;
1752
+ })
1753
+ };
1754
+ var releaseConnections = Effect3.suspend(
1755
+ () => Effect3.forEach([...sessions.values()], (session) => session.release, { discard: true })
1756
+ );
1757
+ var fileSession = Effect3.fn("Analysis.fileSession")(function* (project, engine2) {
1758
+ const fs = yield* FileSystem2.FileSystem;
1759
+ const path = yield* Path2.Path;
1760
+ const scope = yield* Scope.make();
1761
+ const fingerprint = yield* Ref.make(void 0);
1762
+ const answers = new AnswerCache();
1763
+ const database = yield* RcRef.make({
1764
+ acquire: Effect3.flatMap(
1765
+ schemaFiles(project.root, project.schema ?? []),
1766
+ (found) => openSchemaDatabase(project.dialect, found.files, project.root)
1767
+ ),
1768
+ idleTimeToLive: "2 seconds"
1769
+ }).pipe(Scope.provide(scope));
1770
+ const release = RcRef.invalidate(database);
1771
+ const platform = (effect) => effect.pipe(
1772
+ Effect3.provideService(FileSystem2.FileSystem, fs),
1773
+ Effect3.provideService(Path2.Path, path)
1774
+ );
1775
+ const schema = Effect3.gen(function* () {
1776
+ if (project.error !== void 0) return yield* Effect3.fail(project.error);
1777
+ if (project.dialect === "mysql") {
1778
+ return yield* Effect3.fail(
1779
+ '"schema" requires SQLite or Postgres; MySQL needs its server: use a MySQL 8.4 server'
1780
+ );
1781
+ }
1782
+ const found = yield* schemaFiles(project.root, project.schema ?? []);
1783
+ if (found.error !== void 0) return yield* Effect3.fail(found.error);
1784
+ const value = yield* schemaFingerprint(project.root, found.files, project.dialect);
1785
+ if (value !== (yield* Ref.get(fingerprint))) {
1786
+ yield* release;
1787
+ answers.clear();
1788
+ yield* Ref.set(fingerprint, value);
1789
+ }
1790
+ return { value };
1791
+ }).pipe(
1792
+ Effect3.catch(
1793
+ (failure2) => Effect3.gen(function* () {
1794
+ yield* release;
1795
+ yield* Ref.set(fingerprint, void 0);
1796
+ return { value: null, fatal: true, error: messageOf2(failure2) };
1797
+ })
1798
+ ),
1799
+ platform
1800
+ );
1801
+ const recheck = Effect3.gen(function* () {
1802
+ const previous = yield* Ref.get(fingerprint);
1803
+ return (yield* schema).value !== previous;
1804
+ });
1805
+ const cached = (entry, identity) => fs.readFileString(entry).pipe(
1806
+ Effect3.flatMap(Schema4.decodeUnknownEffect(StoredAnalysis)),
1807
+ Effect3.flatMap(
1808
+ (stored) => stored.identity === identity ? (
1809
+ // SAFETY: only `store` writes this file, keyed by engine, dialect, schema, and SQL.
1810
+ Effect3.succeed(stored.analysis)
1811
+ ) : Effect3.fail("stale")
1812
+ ),
1813
+ Effect3.option
1814
+ );
1815
+ const store = (entry, value) => {
1816
+ const temporary = `${entry}.${randomUUID()}.tmp`;
1817
+ return Effect3.gen(function* () {
1818
+ yield* fs.makeDirectory(path.dirname(entry), { recursive: true });
1819
+ yield* fs.writeFileString(temporary, JSON.stringify(value));
1820
+ yield* fs.rename(temporary, entry);
1821
+ }).pipe(Effect3.ensuring(Effect3.ignore(fs.remove(temporary, { force: true }))), Effect3.ignore);
1822
+ };
1823
+ const analyze = (sql) => Effect3.gen(function* () {
1824
+ const status = yield* schema;
1825
+ if (status.value === null) {
1826
+ return { ok: false, code: 5, message: status.error ?? "Cannot load schema" };
1827
+ }
1828
+ const ping = engine2.call({ op: "ping" });
1829
+ const identity = createHash3("sha256").update(JSON.stringify(["schema/1", ping, project.dialect, status.value, sql])).digest("hex");
1830
+ const entry = path.join(project.root, ".reylo", "queries", `${identity}.json`);
1831
+ const stored = yield* cached(entry, identity);
1832
+ if (Option4.isSome(stored)) {
1833
+ return { ok: true, analysis: stored.value, source: "cache", entry };
1834
+ }
1835
+ if (project.offline) return { ok: false, code: 4, message: OFFLINE_MISS };
1836
+ const io = yield* RcRef.get(database);
1837
+ const analysis = yield* Effect3.tryPromise({
1838
+ try: () => engine2.analyze(sql, project.dialect, io, answers),
1839
+ catch: ReyloError.from
1840
+ });
1841
+ yield* store(entry, { identity, analysis });
1842
+ return { ok: true, analysis, source: "database", entry };
1843
+ }).pipe(
1844
+ Effect3.scoped,
1845
+ Effect3.catch(
1846
+ (failure2) => Effect3.succeed({ ok: false, code: 5, message: messageOf2(failure2) })
1847
+ )
1848
+ );
1849
+ const session = {
1850
+ project,
1851
+ lex: (sql) => Effect3.try({
1852
+ try: () => engine2.lex(sql, project.dialect),
1853
+ catch: (cause) => new LexError({ message: describeCause(cause), cause })
1854
+ }),
1855
+ schema,
1856
+ recheck,
1857
+ analyze,
1858
+ describeParameters: project.dialect === "postgres" ? (sql, parameterTypes) => Effect3.gen(function* () {
1859
+ if (project.offline)
1860
+ return yield* new ReyloError({
1861
+ message: `${OFFLINE_MISS} Native JSON parameter descriptions are not cached.`,
1862
+ cause: void 0
1863
+ });
1864
+ const io = yield* RcRef.get(database).pipe(Effect3.mapError(ReyloError.from));
1865
+ const describe = io.describeParameters;
1866
+ if (!describe)
1867
+ return yield* new ReyloError({
1868
+ message: "Explicit PostgreSQL parameter types are unavailable",
1869
+ cause: void 0
1870
+ });
1871
+ return yield* Effect3.tryPromise({
1872
+ try: () => describe(sql, parameterTypes),
1873
+ catch: ReyloError.from
1874
+ });
1875
+ }).pipe(Effect3.scoped, platform) : void 0,
1876
+ release,
1877
+ close: Effect3.suspend(() => {
1878
+ for (const [key, entry] of sessions) if (entry === session) sessions.delete(key);
1879
+ return Scope.close(scope, Exit.void);
1880
+ })
1881
+ };
1882
+ return session;
1883
+ });
1884
+ function messageOf2(failure2) {
1885
+ return Predicate4.isString(failure2) ? failure2 : failure2.message;
1886
+ }
1887
+
1888
+ // src/emit.ts
1889
+ import { exclusive } from "./host.js";
1890
+
1891
+ // src/emit-contract.ts
1892
+ import { createHash as createHash4 } from "node:crypto";
1893
+ import { basename } from "node:path";
1894
+ import { Effect as Effect4, Result, String as String2 } from "effect";
1895
+
1896
+ // src/map.ts
1897
+ import { Predicate as Predicate5 } from "effect";
1898
+
1899
+ // src/schema-ir.ts
1900
+ import { Data } from "effect";
1901
+ var SchemaIR = Data.taggedEnum();
1902
+ var codec = (name) => SchemaIR.Codec({ name });
1903
+ var effectSchema = (name) => SchemaIR.Schema({ name });
1904
+ var nullOr = (item, nullable = true) => nullable ? SchemaIR.NullOr({ item }) : item;
1905
+ var arrayOf = (item) => SchemaIR.Array({ item });
1906
+ var json = (item) => SchemaIR.Json({ item });
1907
+ var union = (members) => SchemaIR.Union({ members });
1908
+ var struct = (fields) => SchemaIR.Struct({ fields });
1909
+ function nonNull(schema) {
1910
+ return SchemaIR.$is("NullOr")(schema) ? schema.item : void 0;
1911
+ }
1912
+ function sameSchema(left, right) {
1913
+ return JSON.stringify(left) === JSON.stringify(right);
1914
+ }
1915
+ function admits(wider2, narrower) {
1916
+ const inner = nonNull(wider2);
1917
+ return sameSchema(wider2, narrower) || inner !== void 0 && sameSchema(inner, narrower);
1918
+ }
1919
+ var printSchema = SchemaIR.$match({
1920
+ Codec: ({ name }) => name,
1921
+ Schema: ({ name }) => `Schema.${name}`,
1922
+ Ref: ({ identifier: identifier2 }) => identifier2,
1923
+ Literal: ({ value }) => `Schema.Literal(${JSON.stringify(value)})`,
1924
+ Literals: ({ values }) => `Schema.Literals([${values.map((value) => JSON.stringify(value)).join(", ")}])`,
1925
+ NullOr: ({ item }) => `Schema.NullOr(${printSchema(item)})`,
1926
+ Array: ({ item }) => `Schema.Array(${printSchema(item)})`,
1927
+ Tuple: ({ items }) => `Schema.Tuple([${items.map(printSchema).join(", ")}])`,
1928
+ Struct: ({ fields }) => fields.length === 0 ? "Schema.Struct({})" : `Schema.Struct({ ${printFields(fields)} })`,
1929
+ Union: ({ members }) => `Schema.Union([${members.map(printSchema).join(", ")}])`,
1930
+ Json: ({ item }) => `Json(${printSchema(item)})`
1931
+ });
1932
+ function printFields(fields) {
1933
+ return fields.map((field) => `${schemaIdent(field.name)}: ${printField(field)}`).join(", ");
1934
+ }
1935
+ function printField(field) {
1936
+ const printed = printSchema(field.schema);
1937
+ return field.optional === true ? `Schema.optionalKey(${printed})` : printed;
1938
+ }
1939
+ var none = () => [];
1940
+ var schemaBindings = SchemaIR.$match({
1941
+ Codec: ({ name }) => [name],
1942
+ Schema: none,
1943
+ Ref: none,
1944
+ Literal: none,
1945
+ Literals: none,
1946
+ NullOr: ({ item }) => schemaBindings(item),
1947
+ Array: ({ item }) => schemaBindings(item),
1948
+ Json: ({ item }) => ["Json", ...schemaBindings(item)],
1949
+ Tuple: ({ items }) => items.flatMap(schemaBindings),
1950
+ Union: ({ members }) => members.flatMap(schemaBindings),
1951
+ Struct: ({ fields }) => fields.flatMap((field) => schemaBindings(field.schema))
1952
+ });
1953
+ function schemaIdent(name) {
1954
+ if (name === "__proto__") return '["__proto__"]';
1955
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
1956
+ }
1957
+
1958
+ // src/map.ts
1959
+ var SQLITE_COLUMN = {
1960
+ dialect: "sqlite",
1961
+ role: "column",
1962
+ insideJson: false,
1963
+ jsonText: false,
1964
+ integers: "bigint"
1965
+ };
1966
+ function emitContext(over = {}) {
1967
+ return { ...SQLITE_COLUMN, ...over };
1968
+ }
1969
+ var INT = effectSchema("Int");
1970
+ var INTEGER = codec("Integer");
1971
+ var REAL = codec("Real");
1972
+ var TEXT = codec("Text");
1973
+ var BLOB = codec("Blob");
1974
+ var BOOLEAN = effectSchema("Boolean");
1975
+ var DATE = effectSchema("Date");
1976
+ var UNKNOWN = effectSchema("Unknown");
1977
+ var INSTANT = codec("Instant");
1978
+ var BOOL = codec("Bool");
1979
+ var JSON_BOOL = codec("JsonBool");
1980
+ var JSON_INTEGER = codec("JsonInteger");
1981
+ var MYSQL_BIGINT = codec("MysqlBigInt");
1982
+ var POSTGRES_TIME = codec("PostgresTime");
1983
+ var DYNAMIC = codec("Dynamic");
1984
+ var SAFE_INTEGER = codec("SafeInteger");
1985
+ var SAFE_DYNAMIC = codec("SafeDynamic");
1986
+ var POSTGRES = {
1987
+ int2: INT,
1988
+ int4: INT,
1989
+ int8: INTEGER,
1990
+ bigint: INTEGER,
1991
+ bool: BOOLEAN,
1992
+ boolean: BOOLEAN,
1993
+ numeric: TEXT,
1994
+ decimal: TEXT,
1995
+ date: TEXT,
1996
+ timestamp: DATE,
1997
+ timestamptz: DATE,
1998
+ time: INTEGER,
1999
+ timetz: TEXT,
2000
+ float4: REAL,
2001
+ float8: REAL,
2002
+ text: TEXT,
2003
+ varchar: TEXT,
2004
+ name: TEXT,
2005
+ uuid: TEXT,
2006
+ bytea: BLOB,
2007
+ json: UNKNOWN,
2008
+ jsonb: UNKNOWN
2009
+ };
2010
+ var POSTGRES_JSON = {
2011
+ ...POSTGRES,
2012
+ int8: INT,
2013
+ bigint: INT,
2014
+ numeric: REAL,
2015
+ decimal: REAL,
2016
+ timestamp: TEXT,
2017
+ timestamptz: TEXT,
2018
+ time: TEXT,
2019
+ bytea: TEXT
2020
+ };
2021
+ var MYSQL = {
2022
+ tinyint: INT,
2023
+ smallint: INT,
2024
+ mediumint: INT,
2025
+ int: INT,
2026
+ integer: INT,
2027
+ int4: INT,
2028
+ year: INT,
2029
+ int8: MYSQL_BIGINT,
2030
+ bigint: MYSQL_BIGINT,
2031
+ bool: JSON_BOOL,
2032
+ boolean: JSON_BOOL,
2033
+ numeric: TEXT,
2034
+ decimal: TEXT,
2035
+ float: REAL,
2036
+ double: REAL,
2037
+ real: REAL,
2038
+ float4: REAL,
2039
+ float8: REAL,
2040
+ text: TEXT,
2041
+ char: TEXT,
2042
+ varchar: TEXT,
2043
+ longtext: TEXT,
2044
+ date: DATE,
2045
+ datetime: DATE,
2046
+ timestamp: DATE,
2047
+ time: TEXT,
2048
+ json: UNKNOWN,
2049
+ blob: BLOB,
2050
+ binary: BLOB,
2051
+ varbinary: BLOB
2052
+ };
2053
+ var MYSQL_JSON = {
2054
+ ...MYSQL,
2055
+ int8: INT,
2056
+ bigint: INT,
2057
+ numeric: REAL,
2058
+ decimal: REAL,
2059
+ date: TEXT,
2060
+ datetime: TEXT,
2061
+ timestamp: TEXT
2062
+ };
2063
+ var PARAMETER_CODEC = {
2064
+ postgres: { time: POSTGRES_TIME, tid: TEXT }
2065
+ };
2066
+ var CAST = {
2067
+ sqlite: {
2068
+ date: INSTANT,
2069
+ datetime: INSTANT,
2070
+ timestamp: INSTANT,
2071
+ timestamptz: INSTANT,
2072
+ bool: BOOL,
2073
+ boolean: BOOL
2074
+ },
2075
+ postgres: {
2076
+ date: TEXT,
2077
+ datetime: INSTANT,
2078
+ timestamp: DATE,
2079
+ timestamptz: DATE,
2080
+ time: POSTGRES_TIME,
2081
+ timetz: TEXT,
2082
+ bool: BOOLEAN,
2083
+ boolean: BOOLEAN
2084
+ },
2085
+ mysql: {
2086
+ date: DATE,
2087
+ datetime: DATE,
2088
+ timestamp: DATE,
2089
+ timestamptz: DATE,
2090
+ bool: JSON_BOOL,
2091
+ boolean: JSON_BOOL
2092
+ }
2093
+ };
2094
+ var SCALAR = {
2095
+ integer: "integer",
2096
+ int: "integer",
2097
+ int2: "integer",
2098
+ int4: "integer",
2099
+ int8: "integer",
2100
+ bigint: "integer",
2101
+ real: "real",
2102
+ float: "real",
2103
+ float4: "real",
2104
+ float8: "real",
2105
+ double: "real",
2106
+ text: "text",
2107
+ string: "text",
2108
+ blob: "blob",
2109
+ bytea: "blob",
2110
+ numeric: "numeric",
2111
+ bool: "bool",
2112
+ boolean: "bool",
2113
+ date: "instant",
2114
+ datetime: "instant",
2115
+ timestamp: "instant",
2116
+ timestamptz: "instant"
2117
+ };
2118
+ var BINDING = {
2119
+ integer: "Integer",
2120
+ real: "Real",
2121
+ text: "Text",
2122
+ blob: "Blob",
2123
+ integerOrReal: "IntegerOrReal",
2124
+ numeric: "Numeric",
2125
+ dynamic: "Dynamic",
2126
+ bool: "Bool",
2127
+ instant: "Instant"
2128
+ };
2129
+ var JSON_BINDING = {
2130
+ integer: "JsonInteger",
2131
+ bool: "JsonBool"
2132
+ };
2133
+ var NUMBER_BINDING = {
2134
+ integer: "SafeInteger",
2135
+ blob: "SafeBlob",
2136
+ integerOrReal: "SafeReal",
2137
+ numeric: "SafeNumeric",
2138
+ dynamic: "SafeDynamic",
2139
+ bool: "JsonBool"
2140
+ };
2141
+ function storageOf(type) {
2142
+ if (type.kind === "unknown") return { ok: true, storage: "dynamic" };
2143
+ if (type.kind !== "scalar") {
2144
+ return { ok: false, reason: `${type.kind} results are not a single storage class` };
2145
+ }
2146
+ const parts = type.name.split("|").map((part) => part.trim()).filter((part) => part.length > 0);
2147
+ if (parts.length === 0) {
2148
+ return { ok: false, reason: "the analyzer reported a scalar with no type name" };
2149
+ }
2150
+ const mapped = [];
2151
+ for (const part of parts) {
2152
+ const kind = isKey(SCALAR, part) ? SCALAR[part] : void 0;
2153
+ if (kind === void 0) {
2154
+ return {
2155
+ ok: false,
2156
+ reason: `SQLite type \`${part}\` has no established storage representation`
2157
+ };
2158
+ }
2159
+ mapped.push(kind);
2160
+ }
2161
+ const storage = combine(mapped);
2162
+ if (storage === void 0) {
2163
+ return {
2164
+ ok: false,
2165
+ reason: `SQLite type \`${type.name}\` has no established storage representation`
2166
+ };
2167
+ }
2168
+ return { ok: true, storage };
2169
+ }
2170
+ function combine(parts) {
2171
+ const set = new Set(parts);
2172
+ if (set.size === 1) return parts[0];
2173
+ const kinds = [...set];
2174
+ const only = (...allowed) => kinds.every((kind) => allowed.includes(kind));
2175
+ if (only("integer", "real", "integerOrReal")) return "integerOrReal";
2176
+ if (only("integer", "real", "integerOrReal", "text", "numeric")) return "numeric";
2177
+ if (only("integer", "real", "integerOrReal", "text", "numeric", "blob", "dynamic"))
2178
+ return "dynamic";
2179
+ return void 0;
2180
+ }
2181
+ function schemaExpr(storage, nullable, emit) {
2182
+ const binding = (emit.integers === "number" ? NUMBER_BINDING[storage] : void 0) ?? (emit.insideJson ? JSON_BINDING[storage] : void 0) ?? BINDING[storage];
2183
+ return withNull(codec(binding), nullable);
2184
+ }
2185
+ function schemaOf(type, nullable, emit = SQLITE_COLUMN) {
2186
+ switch (type.kind) {
2187
+ case "array":
2188
+ return arrayExpr(type, nullable, emit);
2189
+ case "scalar":
2190
+ return scalarExpr(type, nullable, emit);
2191
+ case "unknown":
2192
+ return withNull(dynamicOf(emit), nullable);
2193
+ case "object":
2194
+ return objectExpr(type.fields, nullable, emit);
2195
+ case "json":
2196
+ return jsonKindExpr(type.schema, nullable, emit);
2197
+ case "rows":
2198
+ return rowsExpr(type, nullable, emit);
2199
+ case "enum":
2200
+ case "composite":
2201
+ case "list":
2202
+ return { ok: false, reason: `${type.kind} results have no storage representation` };
2203
+ }
2204
+ }
2205
+ function applyCast(mapped, cast, nullable, dialect, integers = "bigint") {
2206
+ if (!mapped.ok || cast === void 0) return mapped;
2207
+ const codec2 = isKey(CAST[dialect], cast) ? CAST[dialect][cast] : void 0;
2208
+ if (codec2 === void 0) return mapped;
2209
+ return withNull(codec2 === BOOL && integers === "number" ? JSON_BOOL : codec2, nullable);
2210
+ }
2211
+ function scalarExpr(type, nullable, emit) {
2212
+ if (emit.dialect !== "sqlite") return namedScalar(emit.dialect, type.name, nullable, emit);
2213
+ const storage = storageOf(type);
2214
+ if (!storage.ok) return storage;
2215
+ return schemaExpr(storage.storage, nullable, emit);
2216
+ }
2217
+ function namedScalar(dialect, name, nullable, emit) {
2218
+ const parts = name.split("|").map((part) => part.trim()).filter((part) => part.length > 0);
2219
+ if (parts.length === 0)
2220
+ return { ok: false, reason: `the analyzer reported a ${dialect} scalar with no type name` };
2221
+ const mapped = [];
2222
+ for (const part of parts) {
2223
+ const expr = codecFor(dialect, part, emit);
2224
+ if (expr === void 0)
2225
+ return {
2226
+ ok: false,
2227
+ reason: `${dialect} type \`${part}\` has no established storage representation`
2228
+ };
2229
+ mapped.push(expr);
2230
+ }
2231
+ return withNull(mapped.length === 1 ? mapped[0] : union(mapped), nullable);
2232
+ }
2233
+ function codecFor(dialect, name, emit) {
2234
+ if (dialect === "postgres" && emit.insideJson && emit.role === "column")
2235
+ return isKey(POSTGRES_JSON, name) ? POSTGRES_JSON[name] : void 0;
2236
+ if (dialect === "postgres" && emit.postgresNumbers === "number" && emit.role === "column") {
2237
+ if (name === "int8" || name === "bigint") return SAFE_INTEGER;
2238
+ if (name === "numeric" || name === "decimal") return codec("PostgresNumeric");
2239
+ }
2240
+ if (dialect === "mysql" && emit.insideJson && emit.role === "column")
2241
+ return isKey(MYSQL_JSON, name) ? MYSQL_JSON[name] : void 0;
2242
+ if (emit.role === "parameter") {
2243
+ if (dialect === "postgres" && emit.insideJson && name === "bytea") return codec("PostgresBytea");
2244
+ const overrides = PARAMETER_CODEC[dialect];
2245
+ const override = overrides !== void 0 && isKey(overrides, name) ? overrides[name] : void 0;
2246
+ if (override !== void 0) return override;
2247
+ }
2248
+ if (dialect === "postgres") return isKey(POSTGRES, name) ? POSTGRES[name] : void 0;
2249
+ return isKey(MYSQL, name) ? MYSQL[name] : void 0;
2250
+ }
2251
+ function isKey(table, name) {
2252
+ return Object.hasOwn(table, name);
2253
+ }
2254
+ function forChild(emit, kind) {
2255
+ const cleared = { ...emit, jsonText: false };
2256
+ if (kind === "object") return { ...cleared, insideJson: true };
2257
+ if (kind === "rows")
2258
+ return { ...cleared, insideJson: emit.insideJson || emit.dialect === "sqlite" || emit.jsonText };
2259
+ return cleared;
2260
+ }
2261
+ function wrapsJson(emit, kind) {
2262
+ if (kind === "object") return emit.dialect === "sqlite" && !emit.insideJson;
2263
+ return emit.dialect === "sqlite" || emit.jsonText;
2264
+ }
2265
+ function arrayExpr(type, nullable, emit) {
2266
+ const nullableItems = emit.role === "column" ? type.nullable_items !== false : type.nullable_items === true;
2267
+ const item = schemaOf(type.of, nullableItems, forChild(emit, "keep"));
2268
+ if (!item.ok) return item;
2269
+ return { ok: true, schema: nullOr(arrayOf(item.schema), nullable) };
2270
+ }
2271
+ function objectExpr(fields, nullable, emit) {
2272
+ const built = structExpr(fields, forChild(emit, "object"));
2273
+ if (!built.ok) return built;
2274
+ return jsonWrapped(built, nullable, wrapsJson(emit, "object"));
2275
+ }
2276
+ function jsonKindExpr(schema, nullable, emit) {
2277
+ const built = schema === void 0 ? withNull(UNKNOWN, false) : jsonSchemaExpr(schema, emit);
2278
+ if (!built.ok) return built;
2279
+ return jsonWrapped(built, nullable, wrapsJson(emit, "json"));
2280
+ }
2281
+ function rowsExpr(type, nullable, emit) {
2282
+ if (type.fields.length === 0) return { ok: false, reason: "row parameters have no fields" };
2283
+ const built = structExpr(type.fields, forChild(emit, "rows"));
2284
+ if (!built.ok) return built;
2285
+ const body = type.one ? built : { ok: true, schema: arrayOf(built.schema) };
2286
+ return jsonWrapped(body, nullable, wrapsJson(emit, "rows"));
2287
+ }
2288
+ function structExpr(fields, emit) {
2289
+ const parts = [];
2290
+ const child = forChild(emit, "keep");
2291
+ for (const field of fields) {
2292
+ const mapped = schemaOf(field.type, field.nullable, child);
2293
+ if (!mapped.ok) return mapped;
2294
+ parts.push({ name: field.name, schema: mapped.schema, optional: field.optional === true });
2295
+ }
2296
+ return { ok: true, schema: struct(parts) };
2297
+ }
2298
+ function jsonSchemaExpr(schema, emit) {
2299
+ if (schema === true) return withNull(UNKNOWN, false);
2300
+ if (schema === false) return { ok: false, reason: "the JSON schema admits no value" };
2301
+ if (!Predicate5.isObjectOrArray(schema))
2302
+ return { ok: false, reason: "the JSON schema is not an object" };
2303
+ if (Array.isArray(schema.anyOf)) {
2304
+ return unionExpr(schema.anyOf.filter(isJsonSchema), emit);
2305
+ }
2306
+ if (Array.isArray(schema.enum)) {
2307
+ const literals = schema.enum.filter(
2308
+ (entry) => entry === null || Predicate5.isString(entry) || Predicate5.isNumber(entry) || Predicate5.isBoolean(entry)
2309
+ );
2310
+ if (literals.length !== schema.enum.length || literals.length === 0) {
2311
+ return { ok: false, reason: "the JSON enum has no representable members" };
2312
+ }
2313
+ return { ok: true, schema: SchemaIR.Literals({ values: literals }) };
2314
+ }
2315
+ const type = schema.type;
2316
+ if (Array.isArray(type)) {
2317
+ return unionExpr(
2318
+ type.map((name) => ({ type: name })),
2319
+ emit
2320
+ );
2321
+ }
2322
+ if (type === "integer") return withNull(jsonIntegerOf(emit), false);
2323
+ if (type === "number") return withNull(REAL, false);
2324
+ if (type === "string") return withNull(TEXT, false);
2325
+ if (type === "boolean") return withNull(BOOLEAN, false);
2326
+ if (type === "null") return { ok: true, schema: effectSchema("Null") };
2327
+ if (type === "array") return arraySchemaExpr(schema, emit);
2328
+ if (type === "object" || schema.properties !== void 0) return objectSchemaExpr(schema, emit);
2329
+ if (type === void 0) return withNull(UNKNOWN, false);
2330
+ if (!Predicate5.isString(type))
2331
+ return { ok: false, reason: "the JSON schema type is not a string" };
2332
+ return { ok: false, reason: `JSON schema type \`${type}\` has no representation` };
2333
+ }
2334
+ function arraySchemaExpr(schema, emit) {
2335
+ const prefix = Array.isArray(schema.prefixItems) ? schema.prefixItems : void 0;
2336
+ const items = schema.items;
2337
+ if (prefix !== void 0) {
2338
+ return together(
2339
+ prefix.map((entry) => isJsonSchema(entry) ? entry : false),
2340
+ emit,
2341
+ (ok) => {
2342
+ if (items !== false && items !== void 0) {
2343
+ return {
2344
+ ok: false,
2345
+ reason: "a JSON array with both a prefix and additional items has no representation"
2346
+ };
2347
+ }
2348
+ return { ok: true, schema: SchemaIR.Tuple({ items: ok.map((entry) => entry.schema) }) };
2349
+ }
2350
+ );
2351
+ }
2352
+ if (isJsonSchema(items) && !Predicate5.isBoolean(items)) {
2353
+ const item = jsonSchemaExpr(items, emit);
2354
+ if (!item.ok) return item;
2355
+ return { ok: true, schema: arrayOf(item.schema) };
2356
+ }
2357
+ return { ok: false, reason: "the JSON array schema has no item schema" };
2358
+ }
2359
+ function objectSchemaExpr(schema, emit) {
2360
+ const properties = schema.properties;
2361
+ if (!Predicate5.isObject(properties)) {
2362
+ return { ok: false, reason: "the JSON object schema has no properties" };
2363
+ }
2364
+ if (schema.additionalProperties !== void 0 && schema.additionalProperties !== false) {
2365
+ return { ok: false, reason: "open JSON objects have no representation" };
2366
+ }
2367
+ const required = new Set(
2368
+ Array.isArray(schema.required) ? schema.required.filter(Predicate5.isString) : []
2369
+ );
2370
+ const parts = [];
2371
+ for (const [name, property] of Object.entries(properties)) {
2372
+ const mapped = jsonSchemaExpr(isJsonSchema(property) ? property : false, emit);
2373
+ if (!mapped.ok) return mapped;
2374
+ parts.push({ name, schema: mapped.schema, optional: !required.has(name) });
2375
+ }
2376
+ return { ok: true, schema: struct(parts) };
2377
+ }
2378
+ function unionExpr(members, emit) {
2379
+ if (members.length === 0) return { ok: false, reason: "the JSON union has no members" };
2380
+ return together(members, emit, (ok) => {
2381
+ if (ok.length === 1) return ok[0];
2382
+ return { ok: true, schema: union(ok.map((entry) => entry.schema)) };
2383
+ });
2384
+ }
2385
+ function together(members, emit, join7) {
2386
+ const built = members.map((member) => jsonSchemaExpr(member, emit));
2387
+ const failed = built.find((entry) => !entry.ok);
2388
+ if (failed !== void 0 && !failed.ok) return failed;
2389
+ return join7(built.filter((entry) => entry.ok));
2390
+ }
2391
+ function isJsonSchema(value) {
2392
+ return Predicate5.isBoolean(value) || Predicate5.isObjectOrArray(value);
2393
+ }
2394
+ function dynamicOf(emit) {
2395
+ return emit.dialect === "sqlite" && emit.integers === "number" ? SAFE_DYNAMIC : DYNAMIC;
2396
+ }
2397
+ function jsonIntegerOf(emit) {
2398
+ if (emit.dialect !== "sqlite") return INT;
2399
+ return emit.integers === "number" ? SAFE_INTEGER : JSON_INTEGER;
2400
+ }
2401
+ function withNull(schema, nullable) {
2402
+ return { ok: true, schema: nullOr(schema, nullable) };
2403
+ }
2404
+ function jsonWrapped(inner, nullable, wrapJson) {
2405
+ return { ok: true, schema: nullOr(wrapJson ? json(inner.schema) : inner.schema, nullable) };
2406
+ }
2407
+
2408
+ // src/emit-contract.ts
2409
+ var TARGET = "effect-sql/5";
2410
+ var UNSUPPORTED = 30;
2411
+ function hashedField(field, bindings = schemaBindings(field.schema)) {
2412
+ const expr = printField(field);
2413
+ if (field.storageName === void 0) return { name: field.name, expr, bindings };
2414
+ return { name: field.name, storageName: field.storageName, expr, bindings };
2415
+ }
2416
+ function printDeclaration(declaration) {
2417
+ return `const ${declaration.identifier} = ${printSchema(declaration.schema)};`;
2418
+ }
2419
+ var withEntry = (check) => Effect4.suspend(() => {
2420
+ const seen = {};
2421
+ return Effect4.map(check(seen), (checked) => ({ ...checked, entry: seen.entry }));
2422
+ });
2423
+ var analyzeContract = Effect4.fn("Emit.analyzeContract")(function* (sql, owned) {
2424
+ const refused = (message, code) => ({
2425
+ ok: false,
2426
+ diagnostics: [{ message, start: 0, length: 1, code }],
2427
+ entry: void 0
2428
+ });
2429
+ const lexed = yield* Effect4.result(owned.lex(sql));
2430
+ if (Result.isFailure(lexed)) return refused(lexed.failure.message, 1);
2431
+ let outcome = yield* owned.analyze(sql);
2432
+ if (outcome.ok && outcome.analysis.diagnostics.some((diagnostic) => diagnostic.code === 6) && (yield* owned.recheck)) {
2433
+ outcome = yield* owned.analyze(sql);
2434
+ }
2435
+ if (!outcome.ok) return refused(outcome.message, outcome.code);
2436
+ const diagnostics = outcome.analysis.diagnostics.map((diagnostic) => ({
2437
+ message: diagnostic.message,
2438
+ start: diagnostic.start,
2439
+ length: diagnostic.length,
2440
+ code: diagnostic.code
2441
+ }));
2442
+ if (diagnostics.length > 0) return { ok: false, diagnostics, entry: outcome.entry };
2443
+ const contract = contractOf(
2444
+ sql,
2445
+ outcome.analysis,
2446
+ lexed.success,
2447
+ owned.project.dialect,
2448
+ owned.project
2449
+ );
2450
+ return { ...contract, entry: outcome.entry };
2451
+ });
2452
+ function contractOf(source, analysis, lexed, dialect, settings) {
2453
+ const { postgresNumbers, retryReads, resultNames } = settings;
2454
+ const integers = settings.integers ?? "bigint";
2455
+ const diagnostics = [];
2456
+ const reject3 = (message, name) => {
2457
+ const span = name === void 0 ? { start: 0, length: 1 } : spanOf(lexed, name, source);
2458
+ diagnostics.push({ message, start: span.start, length: span.length, code: UNSUPPORTED });
2459
+ };
2460
+ const names = /* @__PURE__ */ new Map();
2461
+ const row = [];
2462
+ for (const structured of analysis.structured ?? []) {
2463
+ const param = analysis.params.find((entry) => entry.name === structured.name);
2464
+ if (param?.type.kind === "rows" && param.type.fields.some((field) => field.optional)) {
2465
+ reject3(
2466
+ `Parameter :${structured.name} is unsupported: MySQL structured inserts with optional fields require runtime column omission to preserve database defaults. Use an explicit INSERT column list instead.`,
2467
+ structured.name
2468
+ );
2469
+ }
2470
+ }
2471
+ for (const column of analysis.columns) {
2472
+ const name = resultNames === "camelCase" ? String2.snakeToCamel(column.name) : column.name;
2473
+ const earlier = names.get(name);
2474
+ if (name === "") {
2475
+ reject3(`Column \`${column.name}\` has no camelCase name under result_names: camelCase.`);
2476
+ continue;
2477
+ }
2478
+ if (earlier !== void 0) {
2479
+ reject3(
2480
+ earlier === column.name ? `Column \`${name}\` is duplicated; a row is an object keyed by column name.` : `Columns \`${earlier}\` and \`${column.name}\` both become \`${name}\` under result_names: camelCase.`
2481
+ );
2482
+ continue;
2483
+ }
2484
+ names.set(name, column.name);
2485
+ const mapped = schemaOf(
2486
+ column.type,
2487
+ column.nullable,
2488
+ emitContext({ dialect, integers, postgresNumbers })
2489
+ );
2490
+ if (!mapped.ok) {
2491
+ reject3(`Column \`${column.name}\` is unsupported: ${mapped.reason}.`);
2492
+ continue;
2493
+ }
2494
+ row.push({
2495
+ name,
2496
+ storageName: name === column.name ? void 0 : column.name,
2497
+ schema: mapped.schema
2498
+ });
2499
+ }
2500
+ const byName = new Map(analysis.params.map((param) => [param.name, param]));
2501
+ const input = [];
2502
+ for (const name of analysis.rewritten.order) {
2503
+ const param = byName.get(name);
2504
+ if (param === void 0) {
2505
+ reject3(`Parameter :${name} is in the binding order but has no analyzed type.`, name);
2506
+ continue;
2507
+ }
2508
+ if (param.type.kind === "unknown") {
2509
+ reject3(
2510
+ `Parameter :${name} is unsupported: nothing in the statement gives it a storage class. Cast it, as in :${name}::text.`,
2511
+ name
2512
+ );
2513
+ continue;
2514
+ }
2515
+ const parameter = emitContext({
2516
+ dialect,
2517
+ integers,
2518
+ postgresNumbers,
2519
+ role: "parameter",
2520
+ jsonText: dialect !== "sqlite" && (param.type.kind === "json" || param.type.kind === "rows")
2521
+ });
2522
+ const mapped = applyCast(
2523
+ schemaOf(param.type, param.nullable, parameter),
2524
+ param.cast,
2525
+ param.nullable,
2526
+ dialect,
2527
+ integers
2528
+ );
2529
+ if (!mapped.ok) {
2530
+ reject3(`Parameter :${name} is unsupported: ${mapped.reason}.`, name);
2531
+ continue;
2532
+ }
2533
+ input.push({ name, schema: mapped.schema });
2534
+ }
2535
+ for (const param of analysis.params) {
2536
+ if (!analysis.rewritten.order.includes(param.name)) {
2537
+ reject3(
2538
+ `Parameter :${param.name} was analyzed but is missing from the binding order.`,
2539
+ param.name
2540
+ );
2541
+ }
2542
+ }
2543
+ if (diagnostics.length > 0) return { ok: false, diagnostics };
2544
+ const cardinality = analysis.cardinality;
2545
+ const sql = analysis.rewritten.sql;
2546
+ const order = analysis.rewritten.order;
2547
+ const hashed = {
2548
+ target: TARGET,
2549
+ dialect,
2550
+ cardinality,
2551
+ sql,
2552
+ order,
2553
+ input: input.map((field) => [field.name, printSchema(field.schema)]),
2554
+ row: row.map(
2555
+ (field) => field.storageName === void 0 ? [field.name, printSchema(field.schema)] : [field.name, printSchema(field.schema), field.storageName]
2556
+ ),
2557
+ // JSON drops an undefined setting, so contracts without it keep their hash.
2558
+ postgresNumbers,
2559
+ retryReads
2560
+ };
2561
+ const contractHash = sha2562(
2562
+ JSON.stringify(integers === "bigint" ? hashed : { ...hashed, integers })
2563
+ );
2564
+ const fresh = analysis.fresh === void 0 ? void 0 : { query: analysis.fresh.query, rows: analysis.fresh.rows };
2565
+ return {
2566
+ ok: true,
2567
+ value: {
2568
+ identity: sha2562(source),
2569
+ sql,
2570
+ sqlHash: sha2562(sql),
2571
+ contractHash,
2572
+ cardinality,
2573
+ order,
2574
+ input,
2575
+ row,
2576
+ fresh,
2577
+ dialect,
2578
+ integers: integers === "number" ? integers : void 0,
2579
+ retryReads
2580
+ }
2581
+ };
2582
+ }
2583
+ function render(path, contract) {
2584
+ const name = basename(path).replace(/\.sql$/i, "") || "query";
2585
+ const bindings = [
2586
+ ...new Set(
2587
+ [...contract.declarations ?? [], ...contract.input, ...contract.row].flatMap(
2588
+ (entry) => schemaBindings(entry.schema)
2589
+ )
2590
+ )
2591
+ ].sort();
2592
+ const imported = ["makeQuery", ...bindings].join(", ");
2593
+ const renamed = contract.row.flatMap(
2594
+ (field) => field.storageName === void 0 ? [] : [[field.storageName, field.name]]
2595
+ );
2596
+ const lines = [
2597
+ `import { Schema } from "effect";`,
2598
+ `import { ${imported} } from "reylo-effect/runtime";`,
2599
+ ``,
2600
+ ...(contract.declarations ?? []).map(printDeclaration),
2601
+ `export const Input = Schema.Struct({ ${printFields(contract.input)} });`,
2602
+ `export const Row = Schema.Struct({ ${printFields(contract.row)} });`,
2603
+ ``,
2604
+ `export default makeQuery({`,
2605
+ ` name: ${JSON.stringify(name)},`,
2606
+ ` identity: ${JSON.stringify(contract.identity)},`,
2607
+ ` sql: ${JSON.stringify(contract.sql)},`,
2608
+ ` sqlHash: ${JSON.stringify(contract.sqlHash)},`,
2609
+ ` contractHash: ${JSON.stringify(contract.contractHash)},`,
2610
+ ` cardinality: ${JSON.stringify(contract.cardinality)},`,
2611
+ ` order: ${JSON.stringify(contract.order)},`,
2612
+ ` dialect: ${JSON.stringify(contract.dialect)},`,
2613
+ ` input: Input,`,
2614
+ ` row: Row,`
2615
+ ];
2616
+ if (contract.program !== void 0) lines.push(` program: ${JSON.stringify(contract.program)},`);
2617
+ if (contract.fresh !== void 0) lines.push(` fresh: ${JSON.stringify(contract.fresh)},`);
2618
+ if (contract.integers !== void 0)
2619
+ lines.push(` integers: ${JSON.stringify(contract.integers)},`);
2620
+ if (renamed.length > 0) lines.push(` rowNames: ${JSON.stringify(renamed)},`);
2621
+ if (contract.retryReads !== void 0)
2622
+ lines.push(` retryReads: ${JSON.stringify(contract.retryReads)},`);
2623
+ lines.push(`});`, ``);
2624
+ return lines.join("\n");
2625
+ }
2626
+ function spanOf(lexed, name, source) {
2627
+ const placeholder = lexed?.placeholders.find((item) => item.name === name);
2628
+ if (placeholder === void 0) return { start: 0, length: 1 };
2629
+ const written = Buffer.from(source, "utf8").subarray(placeholder.start, placeholder.end).toString("utf8").replace(/\s*::[\s\S]*$/, "");
2630
+ return { start: placeholder.start, length: Math.max(1, Buffer.byteLength(written)) };
2631
+ }
2632
+ function sha2562(text) {
2633
+ return createHash4("sha256").update(text).digest("hex");
2634
+ }
2635
+
2636
+ // src/emit-repeated.ts
2637
+ import { Effect as Effect5 } from "effect";
2638
+
2639
+ // src/repeated-sql.ts
2640
+ var repeatPattern = /^\/\*\s*@repeat\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([A-Za-z_][A-Za-z0-9_]*)\s+(AND|OR)\s*\*\/$/u;
2641
+ var casePattern = /^\/\*\s*@case\s+([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)\s*\*\/$/u;
2642
+ var endPattern = /^\/\*\s*@endcase\s*\*\/$/u;
2643
+ var directive = /^\/\*\s*@(repeat|case|endcase)\b/u;
2644
+ var identifier = /^[A-Za-z_][A-Za-z0-9_]*$/u;
2645
+ var reserved = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2646
+ var bytes = (source, index) => Buffer.byteLength(source.slice(0, index));
2647
+ var problem = (source, message, start, end = start + 1) => ({
2648
+ message,
2649
+ start: bytes(source, start),
2650
+ length: Math.max(1, Buffer.byteLength(source.slice(start, end))),
2651
+ code: 30
2652
+ });
2653
+ var bindingScope = (group, tag) => group === void 0 ? "root" : JSON.stringify([group, tag]);
2654
+ function parseRepeatedSql(source, dialect = "postgres") {
2655
+ const tokens2 = [...sqlTokens(source, dialect, true)];
2656
+ const marked = tokens2.filter((token) => token.kind === "comment" && directive.test(token.text));
2657
+ if (!marked.length) return { ok: true, value: void 0 };
2658
+ const diagnostics = [];
2659
+ const definitions = [];
2660
+ let open;
2661
+ let footer = source.length;
2662
+ for (const token of marked) {
2663
+ const branch = casePattern.exec(token.text);
2664
+ if (branch) {
2665
+ if (open)
2666
+ diagnostics.push(
2667
+ problem(source, "Expression cases cannot be nested", token.start, token.end)
2668
+ );
2669
+ else {
2670
+ footer = Math.min(footer, token.start);
2671
+ open = { group: branch[1], tag: branch[2], start: token.start, body: token.end };
2672
+ }
2673
+ } else if (endPattern.test(token.text)) {
2674
+ if (!open) diagnostics.push(problem(source, "Unexpected @endcase", token.start, token.end));
2675
+ else {
2676
+ definitions.push({ ...open, end: token.start, close: token.end });
2677
+ open = void 0;
2678
+ }
2679
+ } else if (!repeatPattern.test(token.text))
2680
+ diagnostics.push(
2681
+ problem(source, "Malformed repeated-expression directive", token.start, token.end)
2682
+ );
2683
+ }
2684
+ if (open) diagnostics.push(problem(source, "Expression case is missing @endcase", open.start));
2685
+ for (const token of tokens2) {
2686
+ if (token.start < footer || token.kind === "comment") continue;
2687
+ if (!definitions.some(
2688
+ (definition) => token.start >= definition.body && token.end <= definition.end
2689
+ ))
2690
+ diagnostics.push(
2691
+ problem(source, "Only expression definitions may follow the query", token.start, token.end)
2692
+ );
2693
+ }
2694
+ const pieces = (start, end, tagged) => {
2695
+ const parts = [];
2696
+ const local = tokens2.filter((token) => token.start >= start && token.end <= end);
2697
+ let cursor = start;
2698
+ const flush = (until) => {
2699
+ if (until > cursor) parts.push({ text: source.slice(cursor, until), start: cursor });
2700
+ };
2701
+ for (const [index, token] of local.entries()) {
2702
+ const repeat = token.kind === "comment" ? repeatPattern.exec(token.text) : null;
2703
+ if (repeat) {
2704
+ const name2 = repeat[1];
2705
+ if (reserved.has(name2) || tagged && name2 === "_tag")
2706
+ diagnostics.push(
2707
+ problem(source, "Reserved expression-list parameter name", token.start, token.end)
2708
+ );
2709
+ flush(token.start);
2710
+ parts.push({
2711
+ repeat: name2,
2712
+ group: repeat[2],
2713
+ join: repeat[3] === "AND" ? "AND" : "OR",
2714
+ start: token.start,
2715
+ end: token.end
2716
+ });
2717
+ cursor = token.end;
2718
+ continue;
2719
+ }
2720
+ const next = local[index + 1];
2721
+ const previous = local[index - 1];
2722
+ if (token.kind !== "symbol" || token.text !== ":" || !next || next.kind !== "word" || next.start !== token.end || previous?.text === ":" && previous.end === token.start)
2723
+ continue;
2724
+ const name = next.text;
2725
+ if (!identifier.test(name) || reserved.has(name) || tagged && name === "_tag")
2726
+ diagnostics.push(
2727
+ problem(source, "Unsupported repeated-expression parameter name", token.start, next.end)
2728
+ );
2729
+ flush(token.start);
2730
+ parts.push({ parameter: name, start: token.start, end: next.end });
2731
+ cursor = next.end;
2732
+ }
2733
+ flush(end);
2734
+ return parts;
2735
+ };
2736
+ if (splitSql(source.slice(0, footer), dialect).length !== 1)
2737
+ diagnostics.push(problem(source, "Repeated expressions require exactly one statement", 0));
2738
+ const root = pieces(0, footer, false);
2739
+ const groups = /* @__PURE__ */ new Map();
2740
+ for (const definition of definitions) {
2741
+ const bodyTokens = tokens2.filter(
2742
+ (token) => token.start >= definition.body && token.end <= definition.end && (token.kind !== "comment" || repeatPattern.test(token.text))
2743
+ );
2744
+ const stack = [];
2745
+ for (const token of bodyTokens) {
2746
+ if (token.kind !== "symbol") continue;
2747
+ if (token.text === ";")
2748
+ diagnostics.push(
2749
+ problem(source, "Expression cases cannot contain statements", token.start, token.end)
2750
+ );
2751
+ if (token.text === "(" || token.text === "[") stack.push(token);
2752
+ else if (token.text === ")" || token.text === "]") {
2753
+ const first = stack.pop();
2754
+ if (!first || (first.text === "(" ? token.text !== ")" : token.text !== "]"))
2755
+ diagnostics.push(problem(source, "Unbalanced expression case", token.start, token.end));
2756
+ }
2757
+ }
2758
+ if (stack.length || !bodyTokens.length)
2759
+ diagnostics.push(
2760
+ problem(
2761
+ source,
2762
+ "Expression case must contain a balanced expression",
2763
+ definition.body,
2764
+ definition.end
2765
+ )
2766
+ );
2767
+ const cases = groups.get(definition.group) ?? [];
2768
+ if (cases.some((branch) => branch.tag === definition.tag))
2769
+ diagnostics.push(problem(source, "Duplicate expression case", definition.start));
2770
+ cases.push({
2771
+ tag: definition.tag,
2772
+ parts: pieces(definition.body, definition.end, true),
2773
+ start: definition.start
2774
+ });
2775
+ groups.set(definition.group, cases);
2776
+ }
2777
+ const active = /* @__PURE__ */ new Set();
2778
+ const reached = /* @__PURE__ */ new Set();
2779
+ const visit = (parts) => {
2780
+ const names = /* @__PURE__ */ new Map();
2781
+ for (const part of parts) {
2782
+ if ("text" in part) continue;
2783
+ const name = "parameter" in part ? part.parameter : part.repeat;
2784
+ const parameterKind = "parameter" in part ? "scalar" : part.group;
2785
+ const found = names.get(name);
2786
+ if (found !== void 0 && found !== parameterKind)
2787
+ diagnostics.push(
2788
+ problem(
2789
+ source,
2790
+ "A parameter cannot have conflicting expression shapes",
2791
+ part.start,
2792
+ part.end
2793
+ )
2794
+ );
2795
+ names.set(name, parameterKind);
2796
+ if ("parameter" in part) continue;
2797
+ const cases = groups.get(part.group);
2798
+ if (!cases) {
2799
+ diagnostics.push(
2800
+ problem(source, `Unknown expression group: ${part.group}`, part.start, part.end)
2801
+ );
2802
+ continue;
2803
+ }
2804
+ if (active.has(part.group)) {
2805
+ diagnostics.push(
2806
+ problem(source, "Expression groups cannot be recursive", part.start, part.end)
2807
+ );
2808
+ continue;
2809
+ }
2810
+ if (reached.has(part.group)) continue;
2811
+ active.add(part.group);
2812
+ for (const branch of cases) visit(branch.parts);
2813
+ active.delete(part.group);
2814
+ reached.add(part.group);
2815
+ }
2816
+ };
2817
+ visit(root);
2818
+ for (const [name, cases] of groups)
2819
+ if (!reached.has(name))
2820
+ diagnostics.push(problem(source, `Unused expression group: ${name}`, cases[0].start));
2821
+ if (diagnostics.length) return { ok: false, diagnostics };
2822
+ const runtimeParts = (parts) => parts.map(
2823
+ (part) => "text" in part ? part.text : "parameter" in part ? { parameter: part.parameter } : { repeat: part.repeat, group: part.group, join: part.join }
2824
+ );
2825
+ const sourceGroups = [...groups].map(([name, cases]) => ({ name, cases }));
2826
+ return {
2827
+ ok: true,
2828
+ value: {
2829
+ source,
2830
+ root,
2831
+ groups: sourceGroups,
2832
+ program: {
2833
+ root: runtimeParts(root),
2834
+ groups: sourceGroups.map((group) => ({
2835
+ name: group.name,
2836
+ cases: group.cases.map((branch) => ({
2837
+ tag: branch.tag,
2838
+ parts: runtimeParts(branch.parts)
2839
+ }))
2840
+ }))
2841
+ }
2842
+ }
2843
+ };
2844
+ }
2845
+ function repeatedSamples(query, dialect = "postgres") {
2846
+ const groups = new Map(query.groups.map((group) => [group.name, group]));
2847
+ const build = (empty, selected) => {
2848
+ const sql = [];
2849
+ const positional = [];
2850
+ const origins = [];
2851
+ const bindings = [];
2852
+ let nodes = 0;
2853
+ const append = (text, start, end) => {
2854
+ sql.push(text);
2855
+ const origin = bytes(query.source, start);
2856
+ const length = Buffer.byteLength(text);
2857
+ const width = end === void 0 ? length : Math.max(1, Buffer.byteLength(query.source.slice(start, end)));
2858
+ for (let index = 0; index < length; index++) origins.push(origin + Math.min(index, width - 1));
2859
+ };
2860
+ const render2 = (parts, scope) => {
2861
+ for (const part of parts) {
2862
+ if (++nodes > 4096) return false;
2863
+ if ("text" in part) {
2864
+ append(part.text, part.start);
2865
+ positional.push(part.text);
2866
+ } else if ("parameter" in part) {
2867
+ const alias = `__repeated_${bindings.length}`;
2868
+ bindings.push({
2869
+ alias,
2870
+ scope,
2871
+ name: part.parameter,
2872
+ start: bytes(query.source, part.start),
2873
+ end: bytes(query.source, part.end)
2874
+ });
2875
+ append(`:${alias}`, part.start, part.end);
2876
+ positional.push(dialect === "sqlite" ? "?" : `$${bindings.length}`);
2877
+ } else {
2878
+ append("(", part.start, part.end);
2879
+ positional.push("(");
2880
+ const group = groups.get(part.group);
2881
+ const cases = empty ? [] : group.cases.filter((branch) => selected?.group !== part.group || selected.tag === branch.tag).flatMap((branch) => [branch, branch]);
2882
+ if (!cases.length) {
2883
+ const value = part.join === "AND" ? "TRUE" : "FALSE";
2884
+ append(value, part.start, part.end);
2885
+ positional.push(value);
2886
+ }
2887
+ for (const [index, branch] of cases.entries()) {
2888
+ if (index) {
2889
+ const separator = `
2890
+ ${part.join}
2891
+ `;
2892
+ append(separator, part.start, part.end);
2893
+ positional.push(separator);
2894
+ }
2895
+ append("(", branch.start);
2896
+ positional.push("(");
2897
+ if (!render2(branch.parts, bindingScope(part.group, branch.tag))) return false;
2898
+ append("\n)", branch.start);
2899
+ positional.push("\n)");
2900
+ }
2901
+ append("\n)", part.start);
2902
+ positional.push("\n)");
2903
+ }
2904
+ }
2905
+ return true;
2906
+ };
2907
+ if (!render2(query.root, bindingScope())) return void 0;
2908
+ return {
2909
+ sql: sql.join(""),
2910
+ positional: positional.join(""),
2911
+ bindings,
2912
+ origins
2913
+ };
2914
+ };
2915
+ const samples = [
2916
+ build(true),
2917
+ build(false),
2918
+ ...query.groups.flatMap(
2919
+ (group) => group.cases.map((branch) => build(false, { group: group.name, tag: branch.tag }))
2920
+ )
2921
+ ];
2922
+ if (samples.some((sample) => sample === void 0))
2923
+ return {
2924
+ ok: false,
2925
+ diagnostics: [
2926
+ problem(
2927
+ query.source,
2928
+ "Expression definitions expand beyond the analysis limit; split the groups",
2929
+ 0
2930
+ )
2931
+ ]
2932
+ };
2933
+ return { ok: true, samples: samples.filter((sample) => sample !== void 0) };
2934
+ }
2935
+ function repeatedLocation(sample, diagnostic) {
2936
+ const start = sample.origins[diagnostic.start] ?? 0;
2937
+ const end = sample.origins[diagnostic.start + Math.max(1, diagnostic.length) - 1] ?? start;
2938
+ return { ...diagnostic, start, length: Math.max(1, end - start + 1) };
2939
+ }
2940
+
2941
+ // src/emit-repeated.ts
2942
+ var reject = (message, start = 0, length = 1) => ({
2943
+ ok: false,
2944
+ diagnostics: [{ message, start, length, code: 30 }]
2945
+ });
2946
+ var tokens = (sql, dialect) => JSON.stringify([...sqlTokens(sql, dialect)].map((token) => token.text));
2947
+ var compatible = (left, right) => admits(left, right) || admits(right, left);
2948
+ var wider = (left, right) => admits(left, right) ? left : right;
2949
+ var repeatedContract = (query, owned) => withEntry((seen) => checkRepeated(query, owned, seen));
2950
+ var checkRepeated = Effect5.fn("Emit.repeatedContract")(function* (query, owned, seen) {
2951
+ const dialect = owned.project.dialect;
2952
+ if (dialect !== "postgres" && dialect !== "sqlite")
2953
+ return reject("Repeated expressions require Postgres or SQLite");
2954
+ const expanded = repeatedSamples(query, dialect);
2955
+ if (!expanded.ok) return expanded;
2956
+ const checked = [];
2957
+ const fields = /* @__PURE__ */ new Map();
2958
+ for (const sample of expanded.samples) {
2959
+ const mapped = yield* analyzeContract(sample.sql, owned);
2960
+ seen.entry = mapped.entry ?? seen.entry;
2961
+ if (!mapped.ok)
2962
+ return {
2963
+ ok: false,
2964
+ diagnostics: mapped.diagnostics.map((diagnostic) => repeatedLocation(sample, diagnostic))
2965
+ };
2966
+ if (tokens(mapped.value.sql, dialect) !== tokens(sample.positional, dialect))
2967
+ return reject(
2968
+ "Repeated expressions do not support SQL requiring structural parameter rewrites"
2969
+ );
2970
+ checked.push(mapped.value);
2971
+ const byAlias = new Map(mapped.value.input.map((field) => [field.name, field]));
2972
+ if (byAlias.size !== sample.bindings.length || [...byAlias.keys()].some(
2973
+ (alias) => !sample.bindings.some((binding) => binding.alias === alias)
2974
+ ))
2975
+ return reject("Repeated expressions require authored :name bindings for every parameter");
2976
+ for (const binding of sample.bindings) {
2977
+ const field = byAlias.get(binding.alias);
2978
+ if (!field)
2979
+ return reject(
2980
+ `Missing analyzed expression parameter: ${binding.name}`,
2981
+ binding.start,
2982
+ binding.end - binding.start
2983
+ );
2984
+ const scope = fields.get(binding.scope) ?? /* @__PURE__ */ new Map();
2985
+ const previous = scope.get(binding.name);
2986
+ if (previous && !compatible(previous.schema, field.schema))
2987
+ return reject(
2988
+ `Expression parameter :${binding.name} has incompatible storage types in its query contexts`,
2989
+ binding.start,
2990
+ binding.end - binding.start
2991
+ );
2992
+ scope.set(binding.name, {
2993
+ ...field,
2994
+ name: binding.name,
2995
+ schema: previous ? wider(field.schema, previous.schema) : field.schema
2996
+ });
2997
+ fields.set(binding.scope, scope);
2998
+ }
2999
+ }
3000
+ const full = checked[1];
3001
+ const row = full.row.map((field) => ({ ...field }));
3002
+ for (const contract of checked) {
3003
+ if (contract.row.length !== row.length)
3004
+ return reject("Repeated expressions must retain a consistent result shape");
3005
+ for (const [index, field] of contract.row.entries()) {
3006
+ const previous = row[index];
3007
+ if (field.name !== previous.name || field.storageName !== previous.storageName)
3008
+ return reject("Repeated expressions must retain consistent result columns");
3009
+ if (!compatible(field.schema, previous.schema))
3010
+ return reject(
3011
+ `Result column ${field.name} has incompatible expression-dependent storage types`
3012
+ );
3013
+ if (!sameSchema(field.schema, previous.schema) && admits(field.schema, previous.schema))
3014
+ row[index] = field;
3015
+ }
3016
+ }
3017
+ const cardinalities = new Set(checked.map((contract) => contract.cardinality));
3018
+ if (cardinalities.has("none") && cardinalities.size > 1)
3019
+ return reject("Repeated expressions cannot change whether a statement returns rows");
3020
+ const cardinality = cardinalities.has("many") ? "many" : cardinalities.has("maybeOne") ? "maybeOne" : full.cardinality;
3021
+ const groups = new Map(
3022
+ query.groups.map((group, index) => [
3023
+ group.name,
3024
+ { ...group, identifier: `__ReyloGroup${index}` }
3025
+ ])
3026
+ );
3027
+ const declarations = [];
3028
+ const declared = /* @__PURE__ */ new Set();
3029
+ const allBindings = /* @__PURE__ */ new Set();
3030
+ const inputFieldsFor = (parts, scope) => {
3031
+ const inputFields = new Map(fields.get(scope));
3032
+ for (const part of parts) {
3033
+ if (!("repeat" in part)) continue;
3034
+ const group = groups.get(part.group);
3035
+ declare(group.name);
3036
+ inputFields.set(part.repeat, {
3037
+ name: part.repeat,
3038
+ schema: arrayOf(SchemaIR.Ref({ identifier: group.identifier }))
3039
+ });
3040
+ }
3041
+ for (const field of inputFields.values())
3042
+ for (const binding of schemaBindings(field.schema)) allBindings.add(binding);
3043
+ return [...inputFields.values()];
3044
+ };
3045
+ const declare = (name) => {
3046
+ if (declared.has(name)) return;
3047
+ const group = groups.get(name);
3048
+ const cases = group.cases.map((branch) => {
3049
+ const branchFields = inputFieldsFor(branch.parts, bindingScope(name, branch.tag));
3050
+ return struct([
3051
+ { name: "_tag", schema: SchemaIR.Literal({ value: branch.tag }) },
3052
+ ...branchFields.map((field) => ({ name: field.name, schema: field.schema }))
3053
+ ]);
3054
+ });
3055
+ declarations.push({
3056
+ identifier: group.identifier,
3057
+ schema: cases.length === 1 ? cases[0] : union(cases)
3058
+ });
3059
+ declared.add(name);
3060
+ };
3061
+ const input = inputFieldsFor(query.root, bindingScope());
3062
+ const identity = sha2562(query.source);
3063
+ const contractHash = sha2562(
3064
+ JSON.stringify({
3065
+ target: TARGET,
3066
+ identity,
3067
+ program: query.program,
3068
+ // Every input field once carried every binding the module imports.
3069
+ input: input.map((field) => hashedField(field, [...allBindings])),
3070
+ row: row.map((field) => hashedField(field)),
3071
+ declarations: declarations.map(printDeclaration),
3072
+ cardinality,
3073
+ integers: full.integers,
3074
+ postgresNumbers: owned.project.postgresNumbers,
3075
+ retryReads: full.retryReads
3076
+ })
3077
+ );
3078
+ return {
3079
+ ok: true,
3080
+ value: {
3081
+ ...full,
3082
+ identity,
3083
+ contractHash,
3084
+ cardinality,
3085
+ input,
3086
+ row,
3087
+ order: input.map((field) => field.name),
3088
+ program: query.program,
3089
+ declarations
3090
+ }
3091
+ };
3092
+ });
3093
+
3094
+ // src/emit-native-json.ts
3095
+ import { Cache, Duration, Effect as Effect6, Equal, Exit as Exit2, Hash, Option as Option5, Result as Result2, Schema as Schema5 } from "effect";
3096
+
3097
+ // src/statement-program.ts
3098
+ import { Predicate as Predicate6 } from "effect";
3099
+ var isList = (value) => Array.isArray(value);
3100
+ var isRecord = (value) => Predicate6.isReadonlyObject(value) && !isList(value) && !(value instanceof Date) && !(value instanceof Uint8Array) && !(value instanceof ArrayBuffer);
3101
+ var statementCompiler = (program, dialect = "postgres") => {
3102
+ const groups = new Map(program.groups.map((group) => [group.name, group]));
3103
+ const active = /* @__PURE__ */ new Set();
3104
+ const visited = /* @__PURE__ */ new Set();
3105
+ const verify = (parts) => {
3106
+ for (const part of parts) {
3107
+ if (Predicate6.isString(part) || "parameter" in part) continue;
3108
+ if ("in" in part) {
3109
+ if (!/^[A-Za-z_]\w*$/u.test(part.in.column)) throw new Error("Invalid checked IN column");
3110
+ continue;
3111
+ }
3112
+ const group = groups.get(part.group);
3113
+ if (!group) throw new Error(`Missing checked expression group: ${part.group}`);
3114
+ if (active.has(group.name)) throw new Error(`Cyclic checked expression group: ${group.name}`);
3115
+ if (new Set(group.cases.map((branch) => branch.tag)).size !== group.cases.length)
3116
+ throw new Error(`Duplicate checked expression case: ${group.name}`);
3117
+ if (visited.has(group.name)) continue;
3118
+ active.add(group.name);
3119
+ for (const branch of group.cases) verify(branch.parts);
3120
+ active.delete(group.name);
3121
+ visited.add(group.name);
3122
+ }
3123
+ };
3124
+ if (groups.size !== program.groups.length) throw new Error("Duplicate checked expression group");
3125
+ verify(program.root);
3126
+ return (input) => {
3127
+ const text = [];
3128
+ const parameters = [];
3129
+ const append = (parts, scope) => {
3130
+ for (const part of parts) {
3131
+ if (Predicate6.isString(part)) text.push(part);
3132
+ else if ("parameter" in part) {
3133
+ if (!Object.hasOwn(scope, part.parameter))
3134
+ throw new Error(`Missing encoded SQL parameter: ${part.parameter}`);
3135
+ parameters.push(scope[part.parameter]);
3136
+ text.push(dialect === "sqlite" ? "?" : `$${parameters.length}`);
3137
+ } else if ("in" in part) {
3138
+ if (!Object.hasOwn(scope, part.in.parameter))
3139
+ throw new Error(`Missing encoded SQL parameter: ${part.in.parameter}`);
3140
+ const values = scope[part.in.parameter];
3141
+ if (!isList(values))
3142
+ throw new Error(`Expected checked IN argument list: ${part.in.parameter}`);
3143
+ if (!values.length) text.push(" 1=0 ");
3144
+ else {
3145
+ text.push(` ${part.in.column} IN (`);
3146
+ for (const [index, value] of values.entries()) {
3147
+ if (index) text.push(",");
3148
+ parameters.push(value);
3149
+ text.push(dialect === "sqlite" ? "?" : `$${parameters.length}`);
3150
+ }
3151
+ text.push(") ");
3152
+ }
3153
+ } else {
3154
+ const values = scope[part.repeat];
3155
+ if (!isList(values)) throw new Error(`Expected encoded expression list: ${part.repeat}`);
3156
+ const group = groups.get(part.group);
3157
+ if (!group) throw new Error(`Missing checked expression group: ${part.group}`);
3158
+ text.push("(");
3159
+ if (!values.length) text.push(part.join === "AND" ? "TRUE" : "FALSE");
3160
+ for (const [index, value] of values.entries()) {
3161
+ if (!isRecord(value))
3162
+ throw new Error(`Expected encoded expression record: ${part.repeat}`);
3163
+ const tag = value._tag;
3164
+ const branch = group.cases.find((candidate) => candidate.tag === tag);
3165
+ if (!branch) throw new Error("Unknown checked expression case");
3166
+ if (index) text.push(`
3167
+ ${part.join}
3168
+ `);
3169
+ text.push("(");
3170
+ append(branch.parts, value);
3171
+ text.push("\n)");
3172
+ }
3173
+ text.push("\n)");
3174
+ }
3175
+ }
3176
+ };
3177
+ append(program.root, input);
3178
+ return { text: text.join(""), parameters };
3179
+ };
3180
+ };
3181
+
3182
+ // src/emit-native-json.ts
3183
+ var TEXT2 = codec("Text");
3184
+ var NATIVE_ARGUMENT = codec("NativePgJsonArgument");
3185
+ var reject2 = (message, start = 0, length = 1) => ({
3186
+ ok: false,
3187
+ diagnostics: [{ message, start, length, code: 30 }]
3188
+ });
3189
+ var NativeError = class extends Error {
3190
+ start;
3191
+ length;
3192
+ constructor(message, start = 0, length = 1) {
3193
+ super(message);
3194
+ this.start = start;
3195
+ this.length = length;
3196
+ }
3197
+ };
3198
+ var family = [0, 16, 23, 20, 701, 1009, 1e3, 1007, 1016, 1022];
3199
+ var failure = Schema5.Struct({ code: Schema5.String, message: Schema5.String });
3200
+ var rowKey = (contract) => JSON.stringify(contract.row);
3201
+ var tokenText = (sql) => JSON.stringify([...sqlTokens(sql, "postgres")].map((token) => token.text));
3202
+ var DIRECTIVE = /^(?:\/\*|--)\s*@pg-(?:native|in)\b/u;
3203
+ var hasNativeJson = (source, dialect = "postgres") => [...sqlTokens(source, dialect, true)].some(
3204
+ (token) => token.kind === "comment" && DIRECTIVE.test(token.text)
3205
+ );
3206
+ var parse = (source) => {
3207
+ const comments = [...sqlTokens(source, "postgres", true)].filter((t) => t.kind === "comment");
3208
+ const line = comments.find((t) => t.text.startsWith("--") && DIRECTIVE.test(t.text));
3209
+ if (line)
3210
+ throw new NativeError(
3211
+ "Native directives must be block comments, such as /* @pg-native-json name */",
3212
+ line.start,
3213
+ line.end - line.start
3214
+ );
3215
+ const declarations = comments.filter((t) => /^\/\*\s*@pg-native-json\b/u.test(t.text));
3216
+ if (declarations.length !== 1)
3217
+ throw new NativeError(
3218
+ "Exactly one @pg-native-json declaration is required",
3219
+ declarations[1]?.start,
3220
+ declarations[1] && declarations[1].end - declarations[1].start
3221
+ );
3222
+ const declaration = declarations[0];
3223
+ const at = [declaration.start, declaration.end - declaration.start];
3224
+ const match = /^\/\*\s*@pg-native-json\s+([A-Za-z_]\w*(?:\[\])?(?:\s+[A-Za-z_]\w*(?:\[\])?)*)\s*\*\/$/u.exec(
3225
+ declaration.text
3226
+ );
3227
+ if (!match) throw new NativeError("Invalid @pg-native-json declaration", ...at);
3228
+ const names = match[1].trim().split(/\s+/u);
3229
+ if (new Set(names).size !== names.length)
3230
+ throw new NativeError("Duplicate native argument declaration", ...at);
3231
+ const lists = comments.filter((t) => /^\/\*\s*@pg-in\b/u.test(t.text));
3232
+ if (lists.length > 1)
3233
+ throw new NativeError(
3234
+ "Only one independent @pg-in predicate is supported",
3235
+ lists[1].start,
3236
+ lists[1].end - lists[1].start
3237
+ );
3238
+ const marker = lists[0];
3239
+ const listMatch = marker && /^\/\*\s*@pg-in\s+([A-Za-z_]\w*)\s+:([A-Za-z_]\w*)\s*\*\/$/u.exec(marker.text);
3240
+ if (marker && !listMatch)
3241
+ throw new NativeError("Invalid @pg-in predicate", marker.start, marker.end - marker.start);
3242
+ const list = marker && listMatch ? { ...marker, column: listMatch[1], name: listMatch[2] } : void 0;
3243
+ const scalar = names.filter((name) => !name.endsWith("[]"));
3244
+ const listNames = names.filter((name) => name.endsWith("[]"));
3245
+ if (listNames.length !== (list ? 1 : 0) || list && listNames[0] !== `${list.name}[]`)
3246
+ throw new NativeError("Each @pg-in requires one matching name[] admission declaration", ...at);
3247
+ if (list && scalar.length)
3248
+ throw new NativeError("Native list and scalar admissions cannot be combined", ...at);
3249
+ if (names.some((name) => /^(?:__proto__|constructor|prototype)(?:\[\])?$/u.test(name)))
3250
+ throw new NativeError("Reserved native admission name", ...at);
3251
+ if (comments.some((t) => /^\/\*\s*@(?:repeat|case|endcase)\b/u.test(t.text)))
3252
+ throw new NativeError("Native admission cannot be mixed with boolean repeated expressions");
3253
+ const program = list ? [
3254
+ source.slice(0, list.start),
3255
+ { in: { column: list.column, parameter: list.name } },
3256
+ source.slice(list.end)
3257
+ ] : [];
3258
+ const alias = (index) => `__pg_native_${list.name}_${index}`;
3259
+ const replacement = (count) => count ? ` ${list.column} IN (${Array.from({ length: count }, (_, i) => `:${alias(i)}`).join(",")}) ` : " 1=0 ";
3260
+ const sample = (count) => list ? source.slice(0, list.start) + replacement(count) + source.slice(list.end) : source;
3261
+ const locate = (count, diagnostic) => {
3262
+ const message = diagnostic.message.replace(/__pg_native_([A-Za-z_]\w*?)_\d+\b/gu, "$1");
3263
+ if (!list) return { ...diagnostic, message };
3264
+ const replaced = replacement(count).length;
3265
+ const shift = replaced - (list.end - list.start);
3266
+ const origin = (offset, last) => offset < list.start ? offset : offset < list.start + replaced ? last ? list.end - 1 : list.start : offset - shift;
3267
+ const start = origin(diagnostic.start, false);
3268
+ const end = origin(diagnostic.start + Math.max(1, diagnostic.length) - 1, true);
3269
+ return { ...diagnostic, message, start, length: Math.max(1, end - start + 1) };
3270
+ };
3271
+ return { source, scalar, list, alias, sample, locate, program };
3272
+ };
3273
+ var independent = (source, names) => {
3274
+ const tokens2 = [...sqlTokens(source, "postgres")].map((t) => t.text);
3275
+ while (tokens2.at(-1) === ";") tokens2.pop();
3276
+ const upper = tokens2.map((t) => t.toUpperCase());
3277
+ const identifier2 = (text) => /^[A-Za-z_]\w*$/u.test(text ?? "");
3278
+ const parameters = (parts) => parts.flatMap((t, i) => t === ":" && parts[i + 1] ? [parts[i + 1]] : []);
3279
+ const split = (parts) => {
3280
+ const out = [[]];
3281
+ let depth = 0;
3282
+ for (const token of parts) {
3283
+ if (token === "(") depth++;
3284
+ if (token === ")") depth--;
3285
+ if (token === "," && depth === 0) out.push([]);
3286
+ else out.at(-1).push(token);
3287
+ }
3288
+ return out;
3289
+ };
3290
+ const close = (start) => {
3291
+ let depth = 1;
3292
+ for (let i = start + 1; i < tokens2.length; i++) {
3293
+ if (tokens2[i] === "(") depth++;
3294
+ if (tokens2[i] === ")" && --depth === 0) return i;
3295
+ }
3296
+ throw new Error("Unclosed native argument context");
3297
+ };
3298
+ const uses = /* @__PURE__ */ new Map();
3299
+ const register = (name, context) => {
3300
+ if (!names.includes(name)) return;
3301
+ if (uses.has(name)) throw new Error("Each native argument requires one direct use");
3302
+ uses.set(name, context);
3303
+ };
3304
+ const scalar = (term) => {
3305
+ if (term.length === 1 && (/^(?:NULL|TRUE|FALSE|DEFAULT)$/iu.test(term[0]) || term[0].startsWith("'")))
3306
+ return true;
3307
+ if (term.every((t) => t.length === 1) && /^-?(?:\d+(?:\.\d*)?|\.\d+)$/u.test(term.join("")))
3308
+ return true;
3309
+ return /^: [A-Za-z_]\w*(?: : : (?:text|bigint|double precision))*$/iu.test(term.join(" "));
3310
+ };
3311
+ const setters = (parts, native) => {
3312
+ for (const term of split(parts)) {
3313
+ if (!identifier2(term[0]) || term[1] !== "=" || !scalar(term.slice(2)))
3314
+ throw new Error("Native setters require a simple column and scalar");
3315
+ for (const name of parameters(term))
3316
+ if (names.includes(name)) {
3317
+ if (!native || term.length !== 4 || term[2] !== ":")
3318
+ throw new Error("Native UPDATE requires one direct setter");
3319
+ register(name, `UPDATE ${term[0]}`);
3320
+ }
3321
+ }
3322
+ };
3323
+ if (upper[0] === "INSERT" && upper[1] === "INTO") {
3324
+ const open = tokens2.indexOf("("), end = close(open), values = upper.indexOf("VALUES", end);
3325
+ if (open !== 3 || !identifier2(tokens2[2]) || values !== end + 1 || tokens2[values + 1] !== "(")
3326
+ throw new Error("Native INSERT requires one table and direct VALUES");
3327
+ const valueEnd = close(values + 1);
3328
+ const columns = split(tokens2.slice(open + 1, end)), terms = split(tokens2.slice(values + 2, valueEnd));
3329
+ if (columns.length !== terms.length) throw new Error("Native INSERT arity differs");
3330
+ columns.forEach((column, i) => {
3331
+ if (column.length !== 1 || !identifier2(column[0]))
3332
+ throw new Error("Native INSERT target must be a simple column");
3333
+ const term = terms[i];
3334
+ if (!scalar(term)) throw new Error("Native INSERT requires scalar terms");
3335
+ for (const name of parameters(term))
3336
+ if (names.includes(name)) {
3337
+ if (term.length !== 2 || term[0] !== ":")
3338
+ throw new Error("Native arguments cannot occur in expressions");
3339
+ register(name, `INSERT ${column[0]}`);
3340
+ }
3341
+ });
3342
+ if (valueEnd + 1 < tokens2.length) {
3343
+ const conflict = valueEnd + 1;
3344
+ if (upper[conflict] !== "ON" || upper[conflict + 1] !== "CONFLICT" || tokens2[conflict + 2] !== "(")
3345
+ throw new Error("Additional VALUES rows or INSERT suffixes are unsupported");
3346
+ const conflictEnd = close(conflict + 2);
3347
+ if (!split(tokens2.slice(conflict + 3, conflictEnd)).every(
3348
+ (column) => column.length === 1 && identifier2(column[0])
3349
+ ) || upper[conflictEnd + 1] !== "DO" || upper[conflictEnd + 2] !== "UPDATE" || upper[conflictEnd + 3] !== "SET")
3350
+ throw new Error("Native conflict requires a simple column target and direct setters");
3351
+ setters(tokens2.slice(conflictEnd + 4), false);
3352
+ }
3353
+ } else if (upper[0] === "UPDATE") {
3354
+ const set = upper.indexOf("SET"), where = upper.indexOf("WHERE", set);
3355
+ if (set !== 2 || !identifier2(tokens2[1]) || where < 0)
3356
+ throw new Error("Native UPDATE requires one table, SET and WHERE");
3357
+ setters(tokens2.slice(set + 1, where), true);
3358
+ const predicate = tokens2.slice(where + 1);
3359
+ if (predicate.length !== 4 || !identifier2(predicate[0]) || predicate[1] !== "=" || predicate[2] !== ":" || !identifier2(predicate[3]))
3360
+ throw new Error("Native UPDATE requires a direct key predicate");
3361
+ } else if (upper[0] === "SELECT") {
3362
+ const from = upper.indexOf("FROM"), where = upper.indexOf("WHERE");
3363
+ if (from < 0 || where !== from + 2 || !identifier2(tokens2[from + 1]) || !split(tokens2.slice(1, from)).every((column) => column.length === 1 && identifier2(column[0])))
3364
+ throw new Error(
3365
+ "Native IN requires a plain column SELECT from one table; aggregates, windows and expressions are unsupported"
3366
+ );
3367
+ const body = tokens2.slice(where + 1);
3368
+ const members = names.flatMap((name, i) => i ? [",", ":", name] : [":", name]);
3369
+ const expected = [body[0], "IN", "(", ...members, ")"];
3370
+ const suffix = body.slice(expected.length);
3371
+ if (!/^[A-Za-z_]\w*$/u.test(body[0] ?? "") || suffix.length && (suffix.length !== 4 || suffix[0]?.toUpperCase() !== "AND" || !/^[A-Za-z_]\w*$/u.test(suffix[1] ?? "") || suffix[2]?.toUpperCase() !== "IS" || suffix[3]?.toUpperCase() !== "NULL"))
3372
+ throw new Error(
3373
+ "Native list context must be an isolated IN predicate with an optional IS NULL filter"
3374
+ );
3375
+ expected.push(...suffix);
3376
+ if (JSON.stringify(body.map((t) => t.toUpperCase())) !== JSON.stringify(expected.map((t) => t.toUpperCase())))
3377
+ throw new Error("Native list context must be an isolated IN predicate");
3378
+ for (const name of names) register(name, `${body[0]} IN member`);
3379
+ } else throw new Error("Unsupported native admission statement context");
3380
+ for (const name of names)
3381
+ if (!uses.has(name) || parameters(tokens2).filter((n) => n === name).length !== 1)
3382
+ throw new Error(`Unsupported or repeated native argument :${name}`);
3383
+ return Object.fromEntries(uses);
3384
+ };
3385
+ var DescribeKey = class _DescribeKey {
3386
+ id;
3387
+ run;
3388
+ constructor(id, run2) {
3389
+ this.id = id;
3390
+ this.run = run2;
3391
+ }
3392
+ [Equal.symbol](that) {
3393
+ return that instanceof _DescribeKey && that.id === this.id;
3394
+ }
3395
+ [Hash.symbol]() {
3396
+ return Hash.string(this.id);
3397
+ }
3398
+ };
3399
+ var repeats = (exit) => Exit2.isSuccess(exit) && (exit.value.ok || Option5.isSome(exit.value.error));
3400
+ var descriptions = Effect6.runSync(
3401
+ Cache.makeWith((key) => key.run, {
3402
+ capacity: 4096,
3403
+ timeToLive: (exit) => repeats(exit) ? Duration.infinity : Duration.zero
3404
+ })
3405
+ );
3406
+ var describeOnce = (describe, entry, sql, oids) => {
3407
+ const run2 = Effect6.map(
3408
+ Effect6.result(describe(sql, oids)),
3409
+ (described) => Result2.isSuccess(described) ? { ok: true, columns: described.success.columns } : {
3410
+ ok: false,
3411
+ message: described.failure.message,
3412
+ error: Schema5.decodeUnknownOption(failure)(described.failure.cause)
3413
+ }
3414
+ );
3415
+ if (entry === void 0) return run2;
3416
+ return Cache.get(descriptions, new DescribeKey(`${entry}\0${oids.join(",")}`, run2));
3417
+ };
3418
+ var nativeJsonContract = (source, owned) => withEntry((seen) => checkNative(source, owned, seen));
3419
+ var checkNative = Effect6.fn("Emit.nativeJsonContract")(function* (source, owned, seen) {
3420
+ if (owned.project.dialect !== "postgres")
3421
+ return reject2("Native JSON arguments require PostgreSQL");
3422
+ const describe = owned.describeParameters;
3423
+ if (!describe)
3424
+ return reject2(
3425
+ "Native JSON arguments require schema-file PostgreSQL analysis with explicit OID support"
3426
+ );
3427
+ const parsed = yield* Effect6.result(
3428
+ Effect6.try({
3429
+ try: () => parse(source),
3430
+ catch: (e) => e instanceof NativeError ? e : new NativeError("Invalid native declaration")
3431
+ })
3432
+ );
3433
+ if (Result2.isFailure(parsed))
3434
+ return reject2(parsed.failure.message, parsed.failure.start, parsed.failure.length);
3435
+ const query = parsed.success;
3436
+ const contracts = [];
3437
+ const admissions = [];
3438
+ for (const count of query.list ? [0, 1, 2] : [1]) {
3439
+ const sql = query.sample(count);
3440
+ const names = query.list ? Array.from({ length: count }, (_, i) => query.alias(i)) : query.scalar;
3441
+ if (count) {
3442
+ const proof = yield* Effect6.result(
3443
+ Effect6.try({
3444
+ try: () => independent(sql, names),
3445
+ catch: (e) => e instanceof Error ? e.message : "Invalid native use"
3446
+ })
3447
+ );
3448
+ if (Result2.isFailure(proof)) return reject2(proof.failure);
3449
+ }
3450
+ const mapped = yield* analyzeContract(sql, owned);
3451
+ seen.entry = mapped.entry ?? seen.entry;
3452
+ if (!mapped.ok)
3453
+ return {
3454
+ ok: false,
3455
+ diagnostics: mapped.diagnostics.map((diagnostic) => query.locate(count, diagnostic))
3456
+ };
3457
+ const contract = mapped.value;
3458
+ if (query.list) {
3459
+ const sent = statementCompiler({ root: query.program, groups: [] })({
3460
+ [query.list.name]: Array.from({ length: count }, () => null)
3461
+ });
3462
+ if (tokenText(sent.text) !== tokenText(contract.sql))
3463
+ return reject2("Native IN does not support SQL requiring structural parameter rewrites");
3464
+ }
3465
+ contracts.push(contract);
3466
+ for (const name of names) {
3467
+ const field = contract.input.find((f) => f.name === name);
3468
+ if (!field || !admits(nullOr(TEXT2), field.schema))
3469
+ return reject2(
3470
+ `Native argument :${query.list ? query.list.name : name} must have an analyzed TEXT context`
3471
+ );
3472
+ }
3473
+ const baseline = yield* describeOnce(
3474
+ describe,
3475
+ mapped.entry,
3476
+ contract.sql,
3477
+ contract.order.map(() => 0)
3478
+ );
3479
+ if (!baseline.ok) return reject2(baseline.message);
3480
+ const columnKey = JSON.stringify(baseline.columns);
3481
+ const vectors = query.list && count === 2 ? family.flatMap((first) => family.map((second) => [first, second])) : names.flatMap((name) => family.map((oid) => ({ name, oid })));
3482
+ for (const variant of vectors) {
3483
+ const oids = Array.isArray(variant) ? contract.order.map((name) => variant[names.indexOf(name)] ?? 0) : contract.order.map((name) => name === variant.name ? variant.oid : 0);
3484
+ if (oids.every((oid) => oid === 0)) continue;
3485
+ const described = yield* describeOnce(describe, mapped.entry, contract.sql, oids);
3486
+ if (!described.ok) {
3487
+ if (!query.list || Option5.isNone(described.error) || described.error.value.code !== "42883")
3488
+ return reject2(`Native OID variant is unsupported: ${described.message}`);
3489
+ admissions.push({ oids, code: described.error.value.code });
3490
+ } else {
3491
+ if (query.list) return reject2("Native IN gained an unexpected non-TEXT success variant");
3492
+ if (JSON.stringify(described.columns) !== columnKey)
3493
+ return reject2("Native OID alternatives change the result contract");
3494
+ admissions.push({ oids, code: null });
3495
+ }
3496
+ }
3497
+ }
3498
+ const base = contracts.at(-1);
3499
+ const returning = query.list ? contracts.slice(1) : contracts;
3500
+ if (returning.some((contract) => rowKey(contract) !== rowKey(base)))
3501
+ return reject2("Native list changes the analyzed Row contract");
3502
+ const aliases = query.list ? [0, 1].map(query.alias) : [];
3503
+ const input = base.input.filter((field) => !aliases.includes(field.name)).map(
3504
+ (field) => query.scalar.includes(field.name) ? { ...field, schema: NATIVE_ARGUMENT, optional: true } : field
3505
+ );
3506
+ if (query.list) input.push({ name: query.list.name, schema: arrayOf(NATIVE_ARGUMENT) });
3507
+ const value = {
3508
+ ...base,
3509
+ identity: sha2562(source),
3510
+ input,
3511
+ order: query.list ? input.map((field) => field.name) : base.order,
3512
+ cardinality: query.list ? "many" : base.cardinality,
3513
+ program: query.list ? { root: query.program, groups: [] } : void 0,
3514
+ // The checked OID outcomes, by SQLSTATE: server wording does not change the contract.
3515
+ contractHash: sha2562(
3516
+ JSON.stringify([
3517
+ base.contractHash,
3518
+ source,
3519
+ input.map((field) => hashedField(field)),
3520
+ query.program,
3521
+ admissions,
3522
+ "pg-native-json/2"
3523
+ ])
3524
+ )
3525
+ };
3526
+ return { ok: true, value };
3527
+ });
3528
+
3529
+ // src/emit.ts
3530
+ var releaseAnalysis = exclusive(releaseConnections);
3531
+ var emitModule = (path, sql, session) => exclusive(
3532
+ Effect7.gen(function* () {
3533
+ const owned = session ?? (yield* Session.for(dirname4(path)));
3534
+ yield* owned.recheck;
3535
+ const emitted = yield* emitModuleExclusive(path, sql, owned);
3536
+ return { ...emitted, watchDirs: yield* watchDirectories(owned.project) };
3537
+ })
3538
+ );
3539
+ var emitModuleExclusive = Effect7.fn("Emit.module")(function* (path, sql, owned) {
3540
+ const watch = yield* watchFiles(owned.project);
3541
+ const dialect = owned.project.dialect;
3542
+ const contract = hasNativeJson(sql, dialect) ? yield* nativeJsonContract(sql, owned) : yield* ordinaryContract(sql, owned);
3543
+ if (!contract.ok)
3544
+ return fail(
3545
+ contract.diagnostics.map((diagnostic) => diagnostic.message).join("\n"),
3546
+ contract.diagnostics,
3547
+ watch,
3548
+ contract.entry
3549
+ );
3550
+ return {
3551
+ ok: true,
3552
+ code: render(path, contract.value),
3553
+ diagnostics: [],
3554
+ entry: contract.entry,
3555
+ watch
3556
+ };
3557
+ });
3558
+ var ordinaryContract = Effect7.fn("Emit.ordinaryContract")(function* (sql, owned) {
3559
+ const repeated = parseRepeatedSql(sql, owned.project.dialect);
3560
+ if (!repeated.ok) return { ...repeated, entry: void 0 };
3561
+ if (repeated.value !== void 0) return yield* repeatedContract(repeated.value, owned);
3562
+ return yield* analyzeContract(sql, owned);
3563
+ });
3564
+ function fail(message, diagnostics, watch, entry) {
3565
+ return {
3566
+ ok: false,
3567
+ code: unsupportedModule(message),
3568
+ diagnostics,
3569
+ entry,
3570
+ watch
3571
+ };
3572
+ }
3573
+ function unsupportedModule(message) {
3574
+ return [
3575
+ `import { Effect, Schema } from "effect";`,
3576
+ ``,
3577
+ `export const Input = Schema.Never;`,
3578
+ `export const Row = Schema.Never;`,
3579
+ ``,
3580
+ `export default function unsupported(_input: never): Effect.Effect<never> {`,
3581
+ ` return Effect.die(new Error(${JSON.stringify(message)}));`,
3582
+ `}`,
3583
+ ``
3584
+ ].join("\n");
3585
+ }
3586
+ var watchFiles = Effect7.fn("Emit.watchFiles")(function* (project) {
3587
+ const watch = [...project.watched];
3588
+ if (project.schema !== void 0) {
3589
+ watch.push(...(yield* schemaFiles(project.root, project.schema)).files);
3590
+ }
3591
+ if (project.dialect === "sqlite" && project.url !== void 0 && project.schema === void 0) {
3592
+ const database = sqlitePath(project.url, project.root);
3593
+ if (database !== ":memory:" && !watch.includes(database)) watch.push(database);
3594
+ }
3595
+ return [...new Set(watch)];
3596
+ });
3597
+ var watchDirectories = Effect7.fn("Emit.watchDirectories")(function* (project) {
3598
+ const fs = yield* FileSystem3.FileSystem;
3599
+ const directories = /* @__PURE__ */ new Set();
3600
+ for (const pattern of project.schema ?? []) {
3601
+ const magic = pattern.search(/[*?[\]{}()\\]|^!/u);
3602
+ if (magic < 0) {
3603
+ directories.add(dirname4(resolve3(project.root, pattern)));
3604
+ continue;
3605
+ }
3606
+ const prefix = pattern.slice(0, magic);
3607
+ const root = resolve3(project.root, prefix.slice(0, prefix.lastIndexOf("/") + 1) || ".");
3608
+ directories.add(dirname4(root));
3609
+ const visit = (directory) => Effect7.gen(function* () {
3610
+ directories.add(directory);
3611
+ const entries = yield* fs.readDirectory(directory);
3612
+ for (const name of entries) {
3613
+ if (name === "node_modules" || name === ".git") continue;
3614
+ const child = join6(directory, name);
3615
+ const info = yield* Effect7.option(fs.stat(child));
3616
+ if (Option6.isSome(info) && info.value.type === "Directory") yield* visit(child);
3617
+ }
3618
+ }).pipe(
3619
+ // A root that does not exist yet is still watched, so its creation is seen.
3620
+ Effect7.catchReason("PlatformError", "NotFound", () => Effect7.void)
3621
+ );
3622
+ yield* visit(root);
3623
+ }
3624
+ return [...directories];
3625
+ });
3626
+
3627
+ // src/esbuild.ts
3628
+ import { host } from "./host.js";
3629
+ function place2(text, bytes2) {
3630
+ const before = Buffer.from(text, "utf8").subarray(0, Math.max(0, bytes2)).toString("utf8");
3631
+ const line = before.split("\n").length;
3632
+ const column = before.length - before.lastIndexOf("\n");
3633
+ return `${line}:${column}`;
3634
+ }
3635
+ var load = Effect8.fn("Esbuild.load")(function* (path) {
3636
+ const fs = yield* FileSystem4.FileSystem;
3637
+ const sql = yield* fs.readFileString(path);
3638
+ const emitted = yield* emitModule(path, sql);
3639
+ const watchFiles2 = [
3640
+ path,
3641
+ ...emitted.entry === void 0 ? [] : [emitted.entry],
3642
+ ...emitted.watch
3643
+ ];
3644
+ const watchDirs = [...emitted.watchDirs ?? []];
3645
+ if (!emitted.ok) {
3646
+ const details = emitted.diagnostics.map((diagnostic) => `${path}:${place2(sql, diagnostic.start)}: ${diagnostic.message}`).join("\n");
3647
+ return { errors: [{ text: details }], watchFiles: watchFiles2, watchDirs };
3648
+ }
3649
+ return { contents: emitted.code, loader: "js", watchFiles: watchFiles2, watchDirs };
3650
+ });
3651
+ function reyloEffect() {
3652
+ return {
3653
+ name: "reylo-effect",
3654
+ setup(build) {
3655
+ build.onLoad({ filter: /\.sql$/ }, (args) => host.runPromise(load(args.path)));
3656
+ build.onEnd(() => host.runPromise(releaseAnalysis));
3657
+ }
3658
+ };
3659
+ }
3660
+ export {
3661
+ reyloEffect as default
3662
+ };