@palbase/backend 33.0.2 → 34.1.0

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.
Files changed (84) hide show
  1. package/dist/bin/palbase-backend.cjs +2273 -358
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +9 -7
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-YIQ4RS4F.js → chunk-5UGPXKMJ.js} +331 -5
  6. package/dist/chunk-5UGPXKMJ.js.map +1 -0
  7. package/dist/chunk-GHNC2UHS.js +20 -0
  8. package/dist/chunk-GHNC2UHS.js.map +1 -0
  9. package/dist/{chunk-AAT5G7KY.js → chunk-JJW3L62T.js} +2 -2
  10. package/dist/chunk-JJW3L62T.js.map +1 -0
  11. package/dist/{chunk-SI4KGEM3.js → chunk-P7MGSAFT.js} +102 -96
  12. package/dist/chunk-P7MGSAFT.js.map +1 -0
  13. package/dist/{chunk-XOX6RFPZ.js → chunk-PRH45NZU.js} +46 -3
  14. package/dist/chunk-PRH45NZU.js.map +1 -0
  15. package/dist/{chunk-26GH54D2.js → chunk-RZLY4BOY.js} +5 -3
  16. package/dist/{chunk-26GH54D2.js.map → chunk-RZLY4BOY.js.map} +1 -1
  17. package/dist/{chunk-KATPXCJ5.js → chunk-UMESJJRS.js} +19 -40
  18. package/dist/chunk-UMESJJRS.js.map +1 -0
  19. package/dist/chunk-VXPNPVAG.js +40 -0
  20. package/dist/chunk-VXPNPVAG.js.map +1 -0
  21. package/dist/{chunk-WWUG2QXF.js → chunk-X2UYIXDS.js} +59 -4
  22. package/dist/chunk-X2UYIXDS.js.map +1 -0
  23. package/dist/{chunk-C6COAB3E.js → chunk-XZPDPYVY.js} +1905 -195
  24. package/dist/chunk-XZPDPYVY.js.map +1 -0
  25. package/dist/{chunk-TVCCR6SO.js → chunk-YO5GYM73.js} +58 -9
  26. package/dist/chunk-YO5GYM73.js.map +1 -0
  27. package/dist/db/env.cjs.map +1 -1
  28. package/dist/db/env.d.cts +2 -0
  29. package/dist/db/env.d.ts +2 -0
  30. package/dist/db/index.cjs +285 -2
  31. package/dist/db/index.cjs.map +1 -1
  32. package/dist/db/index.d.cts +46 -3
  33. package/dist/db/index.d.ts +46 -3
  34. package/dist/db/index.js +14 -5
  35. package/dist/{index-DMZG3kpo.d.ts → endpoint-BXPdsTA5.d.ts} +370 -58
  36. package/dist/{index-DB_nW-AV.d.cts → endpoint-DqhUP7RT.d.cts} +370 -58
  37. package/dist/engine/index.cjs +2279 -356
  38. package/dist/engine/index.cjs.map +1 -1
  39. package/dist/engine/index.d.cts +5 -5
  40. package/dist/engine/index.d.ts +5 -5
  41. package/dist/engine/index.js +17 -7
  42. package/dist/{index-DLveQoOf.d.cts → index-Co0Hj_aO.d.cts} +108 -50
  43. package/dist/{index-D-3duy8Y.d.ts → index-MhCL7ll4.d.ts} +108 -50
  44. package/dist/index.cjs +806 -386
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +7 -6
  47. package/dist/index.d.ts +7 -6
  48. package/dist/index.js +27 -15
  49. package/dist/index.js.map +1 -1
  50. package/dist/openapi/index.cjs +7 -0
  51. package/dist/openapi/index.cjs.map +1 -1
  52. package/dist/openapi/index.d.cts +3 -3
  53. package/dist/openapi/index.d.ts +3 -3
  54. package/dist/openapi/index.js +4 -3
  55. package/dist/openapi/index.js.map +1 -1
  56. package/dist/{registry-DSTThhKf.d.cts → registry-BkIVHPzB.d.cts} +3 -1
  57. package/dist/{registry-JjF5lcj4.d.ts → registry-CvGLR5j1.d.ts} +3 -1
  58. package/dist/test/index.cjs +103 -66
  59. package/dist/test/index.cjs.map +1 -1
  60. package/dist/test/index.d.cts +3 -3
  61. package/dist/test/index.d.ts +3 -3
  62. package/dist/test/index.js +33 -7
  63. package/dist/test/index.js.map +1 -1
  64. package/docs/README.md +1 -1
  65. package/docs/auth.md +58 -0
  66. package/docs/database.md +308 -0
  67. package/docs/examples/check-query-contracts.ts +5 -0
  68. package/docs/examples/database-dx.contract.json +65 -0
  69. package/docs/examples/database-dx.ts +63 -0
  70. package/docs/examples/money-transfer.ts +73 -0
  71. package/docs/llms-full.txt +382 -1
  72. package/docs/schema.md +15 -0
  73. package/package.json +1 -2
  74. package/template/db/public.ts +12 -0
  75. package/template/package.json +1 -1
  76. package/dist/chunk-AAT5G7KY.js.map +0 -1
  77. package/dist/chunk-C6COAB3E.js.map +0 -1
  78. package/dist/chunk-KATPXCJ5.js.map +0 -1
  79. package/dist/chunk-SI4KGEM3.js.map +0 -1
  80. package/dist/chunk-TVCCR6SO.js.map +0 -1
  81. package/dist/chunk-WWUG2QXF.js.map +0 -1
  82. package/dist/chunk-XOX6RFPZ.js.map +0 -1
  83. package/dist/chunk-YIQ4RS4F.js.map +0 -1
  84. package/stack-images.json +0 -24
@@ -1,17 +1,23 @@
1
1
  import {
2
2
  DeclarationRefused,
3
3
  assertNoOrphanEntryPoints,
4
+ assertUniqueLookup,
4
5
  buildContainer,
5
- buildRelations
6
- } from "./chunk-WWUG2QXF.js";
6
+ buildRelations,
7
+ uniqueKeysOf
8
+ } from "./chunk-X2UYIXDS.js";
9
+ import {
10
+ validateInsertManyOptions
11
+ } from "./chunk-GHNC2UHS.js";
7
12
  import {
8
13
  __requestALS,
9
14
  __runStartHooks,
10
15
  __runWithRuntime
11
- } from "./chunk-TVCCR6SO.js";
16
+ } from "./chunk-YO5GYM73.js";
12
17
  import {
13
- CODECS
14
- } from "./chunk-XOX6RFPZ.js";
18
+ CODECS,
19
+ assertReadContract
20
+ } from "./chunk-PRH45NZU.js";
15
21
  import {
16
22
  assertNoExpressionHandles,
17
23
  assertUsableFilter,
@@ -24,21 +30,31 @@ import {
24
30
  isSqlFragment,
25
31
  looksLikeUnbrandedPlanRef,
26
32
  looksLikeUnbrandedRef,
27
- qualifiedTableKey
28
- } from "./chunk-SI4KGEM3.js";
33
+ qualifiedTableKey,
34
+ retryBudget,
35
+ validateAtomicOptions,
36
+ validateWorkload
37
+ } from "./chunk-P7MGSAFT.js";
29
38
  import {
30
39
  getRoutes,
31
40
  resolveEffectiveAuth
32
- } from "./chunk-AAT5G7KY.js";
41
+ } from "./chunk-JJW3L62T.js";
33
42
  import {
43
+ BadRequest,
34
44
  DeadlockDetected,
45
+ HttpError,
46
+ SQLSTATE,
35
47
  SerializationFailure,
48
+ TooManyRequests,
36
49
  UniqueViolation,
37
- __name,
38
50
  isEngineRaised,
39
51
  isHttpError,
52
+ isRetryable,
40
53
  markEngineRaised
41
- } from "./chunk-KATPXCJ5.js";
54
+ } from "./chunk-UMESJJRS.js";
55
+ import {
56
+ __name
57
+ } from "./chunk-VXPNPVAG.js";
42
58
 
43
59
  // src/engine/config.ts
44
60
  var BootRefused = class extends Error {
@@ -90,6 +106,8 @@ ${detail}`);
90
106
  uploadSecret: env.PALBASE_UPLOAD_SECRET ?? "",
91
107
  anonKey: env.PALBASE_ANON_KEY ?? "",
92
108
  serviceRoleKey: env.PALBASE_SERVICE_ROLE_KEY ?? "",
109
+ cursorSecret: env.PALBASE_CURSOR_SECRET,
110
+ enableCommands: env.PALBASE_DB_COMMANDS === "true",
93
111
  realtimeSecret: env.REALTIME_INGESTION_SECRET ?? "",
94
112
  port,
95
113
  dbRole: env.DB_ROLE ?? "backend_authenticated",
@@ -98,7 +116,14 @@ ${detail}`);
98
116
  // (`roleBackendServiceRole = "backend_service_role"`, NOLOGIN BYPASSRLS),
99
117
  // and the live database agrees (pg_roles.rolbypassrls = true).
100
118
  dbServiceRole: env.DB_SERVICE_ROLE ?? "backend_service_role",
101
- poolMax
119
+ poolMax,
120
+ ...env.PALBASE_DB_DIAGNOSTICS === "true" ? {
121
+ databaseDiagnostics: {
122
+ onComplete: /* @__PURE__ */ __name((diagnostics) => {
123
+ console.log("palbase.database", JSON.stringify(diagnostics));
124
+ }, "onComplete")
125
+ }
126
+ } : {}
102
127
  };
103
128
  }
104
129
  __name(loadConfig, "loadConfig");
@@ -196,14 +221,14 @@ var AuthVerifier = class {
196
221
  return null;
197
222
  }
198
223
  if (header.alg !== "ES256" || !header.kid) return null;
199
- const key = await this.key(header.kid);
200
- if (!key) return null;
224
+ const key2 = await this.key(header.kid);
225
+ if (!key2) return null;
201
226
  let ok = false;
202
227
  try {
203
228
  ok = await crypto.subtle.verify({
204
229
  name: "ECDSA",
205
230
  hash: "SHA-256"
206
- }, key, b64urlToBytes(sig), new TextEncoder().encode(`${h}.${p}`));
231
+ }, key2, b64urlToBytes(sig), new TextEncoder().encode(`${h}.${p}`));
207
232
  } catch {
208
233
  return null;
209
234
  }
@@ -269,12 +294,12 @@ var RateLimiter = class {
269
294
  * wait (never 0 — a caller told to wait 0 comes straight back to the same
270
295
  * refusal).
271
296
  */
272
- check(rule, key, now) {
297
+ check(rule, key2, now) {
273
298
  if (!rule || !(rule.max > 0) || !(rule.window > 0)) return null;
274
- const bucket = this.buckets.get(key);
299
+ const bucket = this.buckets.get(key2);
275
300
  if (!bucket || now >= bucket.resetAt) {
276
301
  if (this.buckets.size >= this.maxKeys) this.evict(now);
277
- this.buckets.set(key, {
302
+ this.buckets.set(key2, {
278
303
  count: 1,
279
304
  resetAt: now + rule.window * 1e3
280
305
  });
@@ -317,11 +342,11 @@ function makeMemoryCache(opts = {}) {
317
342
  const now = opts.now ?? (() => Date.now());
318
343
  const store = /* @__PURE__ */ new Map();
319
344
  const inflight = /* @__PURE__ */ new Map();
320
- const live = /* @__PURE__ */ __name((key) => {
321
- const e = store.get(key);
345
+ const live = /* @__PURE__ */ __name((key2) => {
346
+ const e = store.get(key2);
322
347
  if (!e) return void 0;
323
348
  if (e.expiresAt !== 0 && e.expiresAt <= now()) {
324
- store.delete(key);
349
+ store.delete(key2);
325
350
  return void 0;
326
351
  }
327
352
  return e;
@@ -344,61 +369,742 @@ function makeMemoryCache(opts = {}) {
344
369
  if (victim) store.delete(victim[0]);
345
370
  }
346
371
  }, "evict");
347
- const set = /* @__PURE__ */ __name(async (key, value, ttl) => {
348
- if (store.size >= maxEntries && !store.has(key)) evict();
349
- store.set(key, {
372
+ const set = /* @__PURE__ */ __name(async (key2, value, ttl) => {
373
+ if (store.size >= maxEntries && !store.has(key2)) evict();
374
+ store.set(key2, {
350
375
  value,
351
376
  expiresAt: ttl && ttl > 0 ? now() + ttl * 1e3 : 0
352
377
  });
353
378
  }, "set");
354
379
  return {
355
- async get(key) {
356
- const e = live(key);
380
+ async get(key2) {
381
+ const e = live(key2);
357
382
  return e ? e.value : null;
358
383
  },
359
384
  set,
360
- async del(key) {
361
- store.delete(key);
385
+ async del(key2) {
386
+ store.delete(key2);
362
387
  },
363
- async incr(key) {
364
- const e = live(key);
388
+ async incr(key2) {
389
+ const e = live(key2);
365
390
  const next = (typeof e?.value === "number" ? e.value : 0) + 1;
366
- store.set(key, {
391
+ store.set(key2, {
367
392
  value: next,
368
393
  expiresAt: e?.expiresAt ?? 0
369
394
  });
370
395
  return next;
371
396
  },
372
- async getOrSet(key, ttl, fn) {
373
- const hit = live(key);
397
+ async getOrSet(key2, ttl, fn) {
398
+ const hit = live(key2);
374
399
  if (hit) return hit.value;
375
- const running = inflight.get(key);
400
+ const running = inflight.get(key2);
376
401
  if (running) return running;
377
402
  const fill = (async () => {
378
403
  try {
379
404
  const value = await fn();
380
- await set(key, value, ttl);
405
+ await set(key2, value, ttl);
381
406
  return value;
382
407
  } finally {
383
- inflight.delete(key);
408
+ inflight.delete(key2);
384
409
  }
385
410
  })();
386
- inflight.set(key, fill);
411
+ inflight.set(key2, fill);
387
412
  return fill;
388
413
  }
389
414
  };
390
415
  }
391
416
  __name(makeMemoryCache, "makeMemoryCache");
392
417
 
418
+ // src/engine/db.ts
419
+ import { AsyncLocalStorage } from "async_hooks";
420
+
421
+ // src/engine/schema-cache.ts
422
+ function memoTable(cache, schema, table, compute) {
423
+ if (!schema.tables || !Object.hasOwn(schema.tables, table)) return compute();
424
+ let tables = cache.get(schema);
425
+ if (!tables) {
426
+ tables = /* @__PURE__ */ new Map();
427
+ cache.set(schema, tables);
428
+ }
429
+ if (!tables.has(table)) tables.set(table, compute());
430
+ return tables.get(table);
431
+ }
432
+ __name(memoTable, "memoTable");
433
+ function snapshotDeclaration(value, seen = /* @__PURE__ */ new WeakMap()) {
434
+ if (value === null || typeof value !== "object") return value;
435
+ const previous = seen.get(value);
436
+ if (previous) return previous;
437
+ if (value instanceof Date) return new Date(value.getTime());
438
+ const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
439
+ seen.set(value, result);
440
+ for (const key2 of Reflect.ownKeys(value)) {
441
+ if (Array.isArray(value) && key2 === "length") continue;
442
+ const descriptor = Object.getOwnPropertyDescriptor(value, key2);
443
+ if ("value" in descriptor) descriptor.value = snapshotDeclaration(descriptor.value, seen);
444
+ Object.defineProperty(result, key2, descriptor);
445
+ }
446
+ return Object.freeze(result);
447
+ }
448
+ __name(snapshotDeclaration, "snapshotDeclaration");
449
+
450
+ // src/engine/cursor.ts
451
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
452
+ var key = null;
453
+ var invalidPage = /* @__PURE__ */ __name((message, field = "page") => new BadRequest({
454
+ fields: [
455
+ {
456
+ field,
457
+ message
458
+ }
459
+ ]
460
+ }, message), "invalidPage");
461
+ function setCursorKey(secret) {
462
+ key = secret ? createHash("sha256").update("palbase:cursor:v1\0").update(secret).digest() : null;
463
+ }
464
+ __name(setCursorKey, "setCursorKey");
465
+ function cursorKey() {
466
+ if (!key) throw new Error("Database.page needs PALBASE_CURSOR_SECRET or PALBASE_SERVICE_ROLE_KEY shared by all replicas");
467
+ return key;
468
+ }
469
+ __name(cursorKey, "cursorKey");
470
+ function fingerprint(value) {
471
+ const stable = /* @__PURE__ */ __name((v) => {
472
+ if (typeof v === "bigint") return {
473
+ $bigint: v.toString()
474
+ };
475
+ if (v instanceof Date) return {
476
+ $date: v.toISOString()
477
+ };
478
+ if (Array.isArray(v)) return v.map(stable);
479
+ if (v && typeof v === "object") {
480
+ return Object.fromEntries(Object.keys(v).sort().map((k) => [
481
+ k,
482
+ stable(v[k])
483
+ ]));
484
+ }
485
+ return v;
486
+ }, "stable");
487
+ return createHash("sha256").update(JSON.stringify(stable(value))).digest("base64url");
488
+ }
489
+ __name(fingerprint, "fingerprint");
490
+ function cursorIdentity(role, claimsJson) {
491
+ const claims = JSON.parse(claimsJson);
492
+ const { iat, exp, nbf, jti, ...authorization } = claims;
493
+ return fingerprint({
494
+ role,
495
+ authorization
496
+ });
497
+ }
498
+ __name(cursorIdentity, "cursorIdentity");
499
+ function encodeCursor(secret, scope, values) {
500
+ const nonce = randomBytes(12);
501
+ const cipher = createCipheriv("aes-256-gcm", secret, nonce);
502
+ cipher.setAAD(Buffer.from(scope));
503
+ const body = Buffer.concat([
504
+ cipher.update(JSON.stringify(values), "utf8"),
505
+ cipher.final()
506
+ ]);
507
+ const token = `p1.${Buffer.concat([
508
+ nonce,
509
+ cipher.getAuthTag(),
510
+ body
511
+ ]).toString("base64url")}`;
512
+ if (token.length > 16384) throw invalidPage("page cursor values are too large; choose shorter ordering columns");
513
+ return token;
514
+ }
515
+ __name(encodeCursor, "encodeCursor");
516
+ function decodeCursor(secret, scope, token, count) {
517
+ try {
518
+ if (typeof token !== "string" || token.length > 16384 || !/^p1\.[\w-]+$/.test(token)) throw new Error();
519
+ const data = Buffer.from(token.slice(3), "base64url");
520
+ if (data.length < 30 || data.toString("base64url") !== token.slice(3)) throw new Error();
521
+ const cipher = createDecipheriv("aes-256-gcm", secret, data.subarray(0, 12));
522
+ cipher.setAAD(Buffer.from(scope));
523
+ cipher.setAuthTag(data.subarray(12, 28));
524
+ const values = JSON.parse(Buffer.concat([
525
+ cipher.update(data.subarray(28)),
526
+ cipher.final()
527
+ ]).toString("utf8"));
528
+ if (!Array.isArray(values) || values.length !== count || values.some((v) => v !== null && typeof v !== "string")) throw new Error();
529
+ return values;
530
+ } catch {
531
+ throw invalidPage("Invalid cursor for this query or identity; restart from the first page", "cursor");
532
+ }
533
+ }
534
+ __name(decodeCursor, "decodeCursor");
535
+ function pageOrder(columns, primaryKey, opts) {
536
+ const raw = opts;
537
+ if ("limit" in raw || "offset" in raw) throw invalidPage("page uses first/after or last/before, not limit/offset");
538
+ const backward = opts.last !== void 0 || opts.before !== void 0;
539
+ if (backward && (opts.first !== void 0 || opts.after !== void 0)) throw invalidPage("Use first/after or last/before in one page request");
540
+ const size = (backward ? opts.last : opts.first) ?? 50;
541
+ if (!Number.isInteger(size) || size < 1 || size > 1e3) throw invalidPage("page size must be an integer between 1 and 1000");
542
+ if (primaryKey.length === 0) throw new Error("page requires a declared primary key for a unique, stable ordering");
543
+ const input = opts.orderBy === void 0 ? [] : Array.isArray(opts.orderBy) ? opts.orderBy : [
544
+ opts.orderBy
545
+ ];
546
+ if (input.length > 16) throw invalidPage("page supports at most 16 ordering columns");
547
+ const seen = /* @__PURE__ */ new Set();
548
+ const order = [];
549
+ const add = /* @__PURE__ */ __name((o) => {
550
+ const def = columns[o?.column];
551
+ if (!Object.hasOwn(columns, o?.column) || !def) throw invalidPage(`page order column ${o?.column} is not declared`);
552
+ if (!(/* @__PURE__ */ new Set([
553
+ "uuid",
554
+ "text",
555
+ "integer",
556
+ "bigint",
557
+ "boolean",
558
+ "timestamp",
559
+ "numeric",
560
+ "enum"
561
+ ])).has(def.type ?? "")) {
562
+ throw invalidPage(`page cannot order ${o.column}: ${def.type ?? "unknown"} is not a supported scalar column`);
563
+ }
564
+ if (seen.has(o.column)) throw invalidPage(`page order column ${o.column} is repeated`);
565
+ if (o.direction !== void 0 && o.direction !== "asc" && o.direction !== "desc") throw invalidPage("page direction must be asc or desc");
566
+ if (o.nulls !== void 0 && o.nulls !== "first" && o.nulls !== "last") throw invalidPage("page nulls must be first or last");
567
+ const direction = o.direction ?? "asc";
568
+ seen.add(o.column);
569
+ order.push({
570
+ column: o.column,
571
+ direction,
572
+ nulls: o.nulls ?? (direction === "asc" ? "last" : "first"),
573
+ nullable: def.nullable !== false
574
+ });
575
+ }, "add");
576
+ for (const o of input) add(o);
577
+ for (const column of primaryKey) if (!seen.has(column)) add({
578
+ column,
579
+ direction: order.at(-1)?.direction ?? "asc"
580
+ });
581
+ return {
582
+ order,
583
+ size,
584
+ backward,
585
+ token: backward ? opts.before : opts.after
586
+ };
587
+ }
588
+ __name(pageOrder, "pageOrder");
589
+ function pageBoundary(order, values, backward, bind, column, inclusive = false) {
590
+ const refs = values.map((v) => bind(v));
591
+ if (order.every((o) => !o.nullable && o.direction === order[0].direction) && values.every((v) => v !== null)) {
592
+ const greater = order[0].direction === "asc" !== backward;
593
+ return `(${order.map((o) => column(o.column)).join(", ")}) ${greater ? ">" : "<"}${inclusive ? "=" : ""} (${refs.join(", ")})`;
594
+ }
595
+ const parts = [];
596
+ const prefix = [];
597
+ for (let i = 0; i < order.length; i++) {
598
+ const o = order[i], c = column(o.column), r = refs[i];
599
+ const nullsLast = o.nulls === "last" !== backward;
600
+ const cmp = o.direction === "asc" !== backward ? ">" : "<";
601
+ const tail = values[i] === null ? nullsLast ? "false" : `${c} IS NOT NULL` : `(${c} ${cmp} ${r}${nullsLast ? ` OR ${c} IS NULL` : ""})`;
602
+ parts.push(`(${[
603
+ ...prefix,
604
+ tail
605
+ ].join(" AND ")})`);
606
+ prefix.push(`${c} IS NOT DISTINCT FROM ${r}`);
607
+ }
608
+ if (inclusive) parts.push(`(${prefix.join(" AND ")})`);
609
+ return `(${parts.join(" OR ")})`;
610
+ }
611
+ __name(pageBoundary, "pageBoundary");
612
+
613
+ // src/engine/commands.ts
614
+ var installed = /* @__PURE__ */ new WeakSet();
615
+ var hasCommandExecutor = /* @__PURE__ */ __name((sql) => installed.has(sql), "hasCommandExecutor");
616
+ var COMMAND_EXECUTOR_DDL = `
617
+ CREATE SCHEMA IF NOT EXISTS palbase_runtime;
618
+ CREATE OR REPLACE FUNCTION palbase_runtime.command_v1(steps jsonb) RETURNS jsonb
619
+ LANGUAGE plpgsql VOLATILE SECURITY INVOKER SET search_path = pg_catalog, public AS $pb_command$
620
+ DECLARE
621
+ results jsonb := '[]'::jsonb;
622
+ step jsonb; chunk jsonb; binding jsonb; source jsonb; rows jsonb;
623
+ statement text; rendered text; detail text; guard jsonb;
624
+ index integer := 0; affected integer; passed boolean;
625
+ BEGIN
626
+ FOR step IN SELECT value FROM jsonb_array_elements(steps) LOOP
627
+ IF step->>'empty' = 'true' THEN rows := '[]'::jsonb;
628
+ ELSE
629
+ statement := '';
630
+ FOR chunk IN SELECT value FROM jsonb_array_elements(step->'chunks') LOOP
631
+ IF jsonb_typeof(chunk) = 'string' THEN statement := statement || (chunk #>> '{}');
632
+ ELSE
633
+ binding := step->'bindings'->((chunk->>'bind')::integer);
634
+ IF binding ? 'ref' THEN
635
+ source := results->((binding->'ref'->>'op')::integer)->'rows'->0;
636
+ IF source IS NULL OR NOT source ? (binding->'ref'->>'field') THEN
637
+ RAISE EXCEPTION 'Command reference has no source row/column; use expectOne before reading it'
638
+ USING ERRCODE = 'PBC02', DETAIL = jsonb_build_object('error_code','tx_ref_unresolved','op',index)::text;
639
+ END IF;
640
+ source := source->(binding->'ref'->>'field');
641
+ ELSE source := binding->'value'; END IF;
642
+ rendered := CASE WHEN source IS NULL OR source = 'null'::jsonb THEN NULL
643
+ WHEN binding->>'json' = 'true' THEN source::text
644
+ WHEN jsonb_typeof(source) = 'string' THEN source #>> '{}' ELSE source::text END;
645
+ -- PostgreSQL quotes the value itself. Values reach this function as
646
+ -- one bound JSON parameter, never as application-generated SQL text.
647
+ statement := statement || quote_nullable(rendered);
648
+ END IF;
649
+ END LOOP;
650
+ EXECUTE statement INTO rows;
651
+ END IF;
652
+ affected := jsonb_array_length(rows);
653
+ guard := step->'guard';
654
+ IF guard IS NOT NULL AND guard <> 'null'::jsonb THEN
655
+ passed := CASE guard->>'kind'
656
+ WHEN 'one' THEN affected = 1 WHEN 'none' THEN affected = 0
657
+ WHEN 'atLeast' THEN affected >= (guard->>'n')::integer
658
+ WHEN 'atMost' THEN affected <= (guard->>'n')::integer ELSE false END;
659
+ IF NOT passed THEN
660
+ RAISE EXCEPTION 'Command expectation failed'
661
+ USING ERRCODE = 'PBC01', DETAIL = jsonb_build_object('error_code','tx_guard_failed','slot',guard->'slot','op',index)::text;
662
+ END IF;
663
+ END IF;
664
+ results := results || jsonb_build_array(jsonb_build_object('rows',rows,'rows_affected',affected));
665
+ index := index + 1;
666
+ END LOOP;
667
+ RETURN jsonb_build_object('results',results);
668
+ EXCEPTION WHEN SQLSTATE 'PBC01' OR SQLSTATE 'PBC02' THEN
669
+ GET STACKED DIAGNOSTICS detail = PG_EXCEPTION_DETAIL;
670
+ RETURN jsonb_build_object('error',detail::jsonb);
671
+ END
672
+ $pb_command$;
673
+ REVOKE ALL ON FUNCTION palbase_runtime.command_v1(jsonb) FROM PUBLIC;
674
+ CREATE OR REPLACE FUNCTION palbase_runtime.command_rows_v1(rows jsonb, guard jsonb) RETURNS jsonb
675
+ LANGUAGE plpgsql VOLATILE SECURITY INVOKER SET search_path = pg_catalog, public AS $pb_rows$
676
+ DECLARE affected integer := jsonb_array_length(rows); passed boolean;
677
+ BEGIN
678
+ IF guard IS NOT NULL AND guard <> 'null'::jsonb THEN
679
+ passed := CASE guard->>'kind'
680
+ WHEN 'one' THEN affected = 1 WHEN 'none' THEN affected = 0
681
+ WHEN 'atLeast' THEN affected >= (guard->>'n')::integer
682
+ WHEN 'atMost' THEN affected <= (guard->>'n')::integer ELSE false END;
683
+ IF NOT passed THEN
684
+ RAISE EXCEPTION 'Command expectation failed'
685
+ USING ERRCODE = 'PBC01', DETAIL = jsonb_build_object('error_code','tx_guard_failed','slot',guard->'slot')::text;
686
+ END IF;
687
+ END IF;
688
+ RETURN rows;
689
+ END
690
+ $pb_rows$;
691
+ REVOKE ALL ON FUNCTION palbase_runtime.command_rows_v1(jsonb,jsonb) FROM PUBLIC;
692
+ `;
693
+ async function installCommandExecutor(sql, roles) {
694
+ const quote = /* @__PURE__ */ __name((s) => `"${s.replace(/"/g, '""')}"`, "quote");
695
+ if (roles.length === 0) throw new Error("Command executor needs explicit runtime roles");
696
+ await sql.begin(async (tx) => {
697
+ await tx.unsafe("SELECT pg_advisory_xact_lock(196754721, 1)");
698
+ await tx.unsafe(COMMAND_EXECUTOR_DDL);
699
+ const list = [
700
+ ...new Set(roles)
701
+ ].map(quote).join(", ");
702
+ await tx.unsafe(`GRANT USAGE ON SCHEMA palbase_runtime TO ${list}`);
703
+ await tx.unsafe(`GRANT EXECUTE ON FUNCTION palbase_runtime.command_v1(jsonb) TO ${list}`);
704
+ await tx.unsafe(`GRANT EXECUTE ON FUNCTION palbase_runtime.command_rows_v1(jsonb,jsonb) TO ${list}`);
705
+ });
706
+ installed.add(sql);
707
+ }
708
+ __name(installCommandExecutor, "installCommandExecutor");
709
+ function parameterChunks(sql) {
710
+ const chunks = [];
711
+ let start = 0, quote = null;
712
+ for (let i = 0; i < sql.length; i++) {
713
+ const c = sql[i];
714
+ if (quote) {
715
+ if (c === quote) {
716
+ if (sql[i + 1] === quote) i++;
717
+ else quote = null;
718
+ }
719
+ continue;
720
+ }
721
+ if (c === "'" || c === '"') {
722
+ quote = c;
723
+ continue;
724
+ }
725
+ if (c !== "$") continue;
726
+ const parameter = /^\$(\d+)\b/.exec(sql.slice(i));
727
+ if (!parameter) continue;
728
+ chunks.push(sql.slice(start, i), {
729
+ bind: Number(parameter[1]) - 1
730
+ });
731
+ i += parameter[0].length - 1;
732
+ start = i + 1;
733
+ }
734
+ chunks.push(sql.slice(start));
735
+ return chunks;
736
+ }
737
+ __name(parameterChunks, "parameterChunks");
738
+
739
+ // src/engine/admission.ts
740
+ var WorkloadAdmission = class {
741
+ static {
742
+ __name(this, "WorkloadAdmission");
743
+ }
744
+ definition;
745
+ pending = [];
746
+ active = 0;
747
+ activeKeys = /* @__PURE__ */ new Set();
748
+ tenants = /* @__PURE__ */ new Map();
749
+ draining = false;
750
+ constructor(definition) {
751
+ this.definition = definition;
752
+ validateWorkload(definition);
753
+ }
754
+ get snapshot() {
755
+ return {
756
+ active: this.active,
757
+ queued: this.pending.length,
758
+ tenants: this.tenants.size,
759
+ keys: this.activeKeys.size
760
+ };
761
+ }
762
+ acquire(tenant, keys, signal) {
763
+ if (signal?.aborted) return Promise.reject(signal.reason ?? new Error("Database admission cancelled"));
764
+ const normalized = [
765
+ ...new Set(keys)
766
+ ].sort();
767
+ if (normalized.length > 64 || normalized.some((k) => typeof k !== "string" || k.length === 0 || k.length > 256)) return Promise.reject(new Error("conflictKeys must contain at most 64 nonempty strings of at most 256 characters"));
768
+ const scoped = normalized;
769
+ const globalFull = this.pending.length >= this.definition.queueLimit;
770
+ const tenantFull = this.pending.filter((w) => w.tenant === tenant).length >= this.definition.tenantQueueLimit;
771
+ const canStart = this.active < this.definition.concurrency && (this.tenants.get(tenant) ?? 0) < this.definition.tenantConcurrency && scoped.every((k) => !this.activeKeys.has(k) && !this.pending.some((w) => w.keys.includes(k)));
772
+ if (!canStart && (globalFull || tenantFull)) {
773
+ return Promise.reject(new TooManyRequests({
774
+ retryAfter: 1
775
+ }, `Database workload ${this.definition.name}: ${tenantFull ? "tenant" : "global"} queue is full`));
776
+ }
777
+ return new Promise((grant, reject) => {
778
+ const waiter = {
779
+ tenant,
780
+ keys: scoped,
781
+ entered: performance.now(),
782
+ grant,
783
+ reject,
784
+ cleanup: /* @__PURE__ */ __name(() => {
785
+ }, "cleanup")
786
+ };
787
+ const remove = /* @__PURE__ */ __name((reason) => {
788
+ const index = this.pending.indexOf(waiter);
789
+ if (index === -1) return;
790
+ this.pending.splice(index, 1);
791
+ waiter.cleanup();
792
+ reject(reason);
793
+ this.drain();
794
+ }, "remove");
795
+ const abort = /* @__PURE__ */ __name(() => remove(signal?.reason ?? new Error("Database admission cancelled")), "abort");
796
+ const timer = setTimeout(() => remove(new TooManyRequests({
797
+ retryAfter: 1
798
+ }, `Database workload ${this.definition.name}: queue deadline exceeded`)), this.definition.queueTimeoutMs);
799
+ waiter.cleanup = () => {
800
+ clearTimeout(timer);
801
+ signal?.removeEventListener("abort", abort);
802
+ };
803
+ signal?.addEventListener("abort", abort, {
804
+ once: true
805
+ });
806
+ this.pending.push(waiter);
807
+ if (signal?.aborted) abort();
808
+ else this.drain();
809
+ });
810
+ }
811
+ drain() {
812
+ if (this.draining) return;
813
+ this.draining = true;
814
+ try {
815
+ const blockedKeys = /* @__PURE__ */ new Set();
816
+ for (let index = 0; index < this.pending.length && this.active < this.definition.concurrency; ) {
817
+ const waiter = this.pending[index];
818
+ if ((this.tenants.get(waiter.tenant) ?? 0) >= this.definition.tenantConcurrency || waiter.keys.some((key2) => this.activeKeys.has(key2) || blockedKeys.has(key2))) {
819
+ for (const key2 of waiter.keys) blockedKeys.add(key2);
820
+ index++;
821
+ continue;
822
+ }
823
+ this.pending.splice(index, 1);
824
+ waiter.cleanup();
825
+ this.active++;
826
+ this.tenants.set(waiter.tenant, (this.tenants.get(waiter.tenant) ?? 0) + 1);
827
+ for (const key2 of waiter.keys) this.activeKeys.add(key2);
828
+ let released = false;
829
+ waiter.grant({
830
+ queueMs: performance.now() - waiter.entered,
831
+ release: /* @__PURE__ */ __name(() => {
832
+ if (released) return;
833
+ released = true;
834
+ this.active--;
835
+ const left = this.tenants.get(waiter.tenant) - 1;
836
+ if (left === 0) this.tenants.delete(waiter.tenant);
837
+ else this.tenants.set(waiter.tenant, left);
838
+ for (const key2 of waiter.keys) this.activeKeys.delete(key2);
839
+ this.drain();
840
+ }, "release")
841
+ });
842
+ }
843
+ } finally {
844
+ this.draining = false;
845
+ }
846
+ }
847
+ };
848
+ var registries = /* @__PURE__ */ new WeakMap();
849
+ function workloadAdmission(owner, definition) {
850
+ validateWorkload(definition);
851
+ let registry = registries.get(owner);
852
+ if (!registry) {
853
+ registry = /* @__PURE__ */ new Map();
854
+ registries.set(owner, registry);
855
+ }
856
+ const existing = registry.get(definition.name);
857
+ if (existing) {
858
+ const d = existing.definition;
859
+ if (Object.keys(d).some((k) => d[k] !== definition[k])) throw new Error(`Workload ${definition.name} was already registered with different limits`);
860
+ return existing;
861
+ }
862
+ if (registry.size >= 64) throw new Error("At most 64 named database workloads may be registered per engine; declare and reuse them at module scope");
863
+ const admission = new WorkloadAdmission(Object.freeze({
864
+ ...definition
865
+ }));
866
+ registry.set(definition.name, admission);
867
+ return admission;
868
+ }
869
+ __name(workloadAdmission, "workloadAdmission");
870
+ function workloadTenant(definition, claimsJson) {
871
+ const claims = JSON.parse(claimsJson);
872
+ const value = claims[definition.tenantClaim] ?? claims.sub;
873
+ return typeof value === "string" && value.length > 0 ? value : "__anonymous__";
874
+ }
875
+ __name(workloadTenant, "workloadTenant");
876
+
877
+ // src/engine/transaction-control.ts
878
+ var TransactionControl = class {
879
+ static {
880
+ __name(this, "TransactionControl");
881
+ }
882
+ controller = new AbortController();
883
+ active = /* @__PURE__ */ new Set();
884
+ cancelBackend;
885
+ cancellation = Promise.resolve();
886
+ timer;
887
+ unlink;
888
+ rejection;
889
+ deadline;
890
+ get signal() {
891
+ return this.controller.signal;
892
+ }
893
+ constructor(timeoutMs, external) {
894
+ this.deadline = timeoutMs === void 0 ? void 0 : performance.now() + timeoutMs;
895
+ this.rejection = new Promise((_, reject) => {
896
+ this.signal.addEventListener("abort", () => {
897
+ reject(this.signal.reason);
898
+ if (this.active.size && this.cancelBackend) {
899
+ this.cancellation = this.cancelBackend().catch(() => void 0);
900
+ } else {
901
+ for (const query of this.active) {
902
+ try {
903
+ query.cancel?.();
904
+ } catch {
905
+ }
906
+ }
907
+ }
908
+ }, {
909
+ once: true
910
+ });
911
+ });
912
+ void this.rejection.catch(() => void 0);
913
+ const abort = /* @__PURE__ */ __name(() => this.controller.abort(external?.reason ?? new DOMException("Database transaction cancelled", "AbortError")), "abort");
914
+ external?.addEventListener("abort", abort, {
915
+ once: true
916
+ });
917
+ this.unlink = () => external?.removeEventListener("abort", abort);
918
+ if (external?.aborted) abort();
919
+ if (timeoutMs !== void 0) this.timer = setTimeout(() => this.controller.abort(new HttpError(503, "database_deadline_exceeded", "Database transaction exceeded its deadline")), timeoutMs);
920
+ }
921
+ check() {
922
+ if (this.deadline !== void 0 && performance.now() >= this.deadline && !this.signal.aborted) {
923
+ this.controller.abort(new HttpError(503, "database_deadline_exceeded", "Database transaction exceeded its deadline"));
924
+ }
925
+ this.signal.throwIfAborted();
926
+ }
927
+ remainingMs() {
928
+ this.check();
929
+ return this.deadline === void 0 ? void 0 : Math.max(1, Math.ceil(this.deadline - performance.now()));
930
+ }
931
+ race(work) {
932
+ return Promise.race([
933
+ work,
934
+ this.rejection
935
+ ]);
936
+ }
937
+ bindBackend(cancel) {
938
+ this.cancelBackend = cancel;
939
+ }
940
+ /** Keep the application's connection parked until the cancellation request
941
+ * and all statements settle; a late cancel must not hit its next borrower. */
942
+ async drainCancellation() {
943
+ await this.cancellation;
944
+ await Promise.allSettled([
945
+ ...this.active
946
+ ]);
947
+ }
948
+ async watch(query) {
949
+ this.active.add(query);
950
+ try {
951
+ if (this.signal.aborted) query.cancel?.();
952
+ return await query;
953
+ } finally {
954
+ this.active.delete(query);
955
+ }
956
+ }
957
+ dispose() {
958
+ if (this.timer !== void 0) clearTimeout(this.timer);
959
+ this.unlink();
960
+ }
961
+ };
962
+
963
+ // src/engine/diagnostics.ts
964
+ import { createHash as createHash2 } from "crypto";
965
+ var DatabaseTrace = class {
966
+ static {
967
+ __name(this, "DatabaseTrace");
968
+ }
969
+ options;
970
+ start = performance.now();
971
+ ended;
972
+ failure;
973
+ state;
974
+ constructor(options) {
975
+ this.options = options;
976
+ for (const key2 of [
977
+ "maxQueries",
978
+ "maxRows",
979
+ "maxBytes"
980
+ ]) {
981
+ const value = options[key2];
982
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < 0)) throw new Error(`database ${key2} must be a nonnegative safe integer`);
983
+ }
984
+ this.state = {
985
+ endpoint: options.endpoint,
986
+ requestId: options.requestId,
987
+ outcome: "active",
988
+ elapsedMs: 0,
989
+ queries: 0,
990
+ rows: 0,
991
+ bytes: 0,
992
+ queueMs: 0,
993
+ poolMs: 0,
994
+ setupMs: 0,
995
+ sqlMs: 0,
996
+ commitMs: 0,
997
+ rollbackMs: 0,
998
+ commits: 0,
999
+ commitFailures: 0,
1000
+ rollbackRequests: 0,
1001
+ retries: 0,
1002
+ statements: []
1003
+ };
1004
+ }
1005
+ check() {
1006
+ if (this.failure) throw this.failure;
1007
+ }
1008
+ time(key2, started) {
1009
+ this.state[key2] += performance.now() - started;
1010
+ }
1011
+ retry() {
1012
+ this.state.retries++;
1013
+ }
1014
+ committed() {
1015
+ this.state.commits++;
1016
+ }
1017
+ commitFailed() {
1018
+ this.state.commitFailures++;
1019
+ }
1020
+ failed() {
1021
+ this.state.outcome = "failed";
1022
+ }
1023
+ rolledBack() {
1024
+ this.state.rollbackRequests++;
1025
+ }
1026
+ refuse(kind) {
1027
+ this.failure ??= new HttpError(500, "database_budget_exceeded", `Database ${kind} budget exceeded${this.options.endpoint ? ` in ${this.options.endpoint}` : ""}`);
1028
+ throw this.failure;
1029
+ }
1030
+ emit(callback, event) {
1031
+ try {
1032
+ const result = callback?.(event);
1033
+ if (result) void Promise.resolve(result).catch(() => void 0);
1034
+ } catch {
1035
+ }
1036
+ }
1037
+ async query(sql, name, run) {
1038
+ this.check();
1039
+ if (this.options.maxQueries !== void 0 && this.state.queries >= this.options.maxQueries) this.refuse("query count");
1040
+ const ordinal = ++this.state.queries;
1041
+ const event = {
1042
+ ordinal,
1043
+ fingerprint: createHash2("sha256").update(sql).digest("hex").slice(0, 24),
1044
+ ...name === void 0 ? {} : {
1045
+ name: name.slice(0, 128)
1046
+ },
1047
+ ...this.options.includeSql ? {
1048
+ sql: sql.slice(0, 4096)
1049
+ } : {},
1050
+ durationMs: 0,
1051
+ rows: 0,
1052
+ bytes: 0
1053
+ };
1054
+ if (this.options.captureSource) event.source = new Error().stack?.split("\n").slice(1, 9).join("\n").slice(0, 2500);
1055
+ const started = performance.now();
1056
+ let measured = false;
1057
+ try {
1058
+ const result = await run();
1059
+ event.durationMs = performance.now() - started;
1060
+ measured = true;
1061
+ if (Array.isArray(result)) {
1062
+ event.rows = result.length;
1063
+ event.bytes = Buffer.byteLength(JSON.stringify(result, (_, value) => typeof value === "bigint" ? value.toString() : value));
1064
+ }
1065
+ this.state.rows += event.rows;
1066
+ this.state.bytes += event.bytes;
1067
+ if (this.options.maxRows !== void 0 && this.state.rows > this.options.maxRows) this.refuse("returned rows");
1068
+ if (this.options.maxBytes !== void 0 && this.state.bytes > this.options.maxBytes) this.refuse("result bytes");
1069
+ return result;
1070
+ } catch (error) {
1071
+ const e = error;
1072
+ event.errorCode = String(e?.sqlstate ?? e?.errno ?? e?.code ?? "query_failed").slice(0, 80);
1073
+ throw error;
1074
+ } finally {
1075
+ if (!measured) event.durationMs = performance.now() - started;
1076
+ this.state.sqlMs += event.durationMs;
1077
+ const frozen = Object.freeze(event);
1078
+ if (this.state.statements.length < 200) this.state.statements.push(frozen);
1079
+ this.emit(this.options.onQuery, frozen);
1080
+ }
1081
+ }
1082
+ get snapshot() {
1083
+ return Object.freeze({
1084
+ ...this.state,
1085
+ elapsedMs: (this.ended ?? performance.now()) - this.start,
1086
+ statements: Object.freeze([
1087
+ ...this.state.statements
1088
+ ].sort((a, b) => a.ordinal - b.ordinal))
1089
+ });
1090
+ }
1091
+ complete(outcome) {
1092
+ if (this.ended !== void 0) return;
1093
+ this.ended = performance.now();
1094
+ this.state.outcome = outcome;
1095
+ this.emit(this.options.onComplete, this.snapshot);
1096
+ }
1097
+ };
1098
+
393
1099
  // src/engine/db.ts
394
1100
  function quoteIdent(name) {
395
1101
  return `"${name.replace(/"/g, '""')}"`;
396
1102
  }
397
1103
  __name(quoteIdent, "quoteIdent");
398
- function quoteTable(key) {
399
- const dot = key.indexOf(".");
400
- if (dot === -1) return quoteIdent(key);
401
- return `${quoteIdent(key.slice(0, dot))}.${quoteIdent(key.slice(dot + 1))}`;
1104
+ function quoteTable(key2) {
1105
+ const dot = key2.indexOf(".");
1106
+ if (dot === -1) return quoteIdent(key2);
1107
+ return `${quoteIdent(key2.slice(0, dot))}.${quoteIdent(key2.slice(dot + 1))}`;
402
1108
  }
403
1109
  __name(quoteTable, "quoteTable");
404
1110
  function encodePgArray(value) {
@@ -411,9 +1117,10 @@ function encodePgArray(value) {
411
1117
  }
412
1118
  __name(encodePgArray, "encodePgArray");
413
1119
  var BIND_SQL = "select set_config('role',$1,true), set_config('search_path','public',true), set_config('request.jwt.claims',$2,true)";
1120
+ var atomicScope = new AsyncLocalStorage();
414
1121
  function createLazyTransaction(sql, role, claimsJson, options = {}) {
415
1122
  const { lockTimeout } = options;
416
- const bindSql = lockTimeout ? `${BIND_SQL}, set_config('lock_timeout',$3,true)` : BIND_SQL;
1123
+ let bindSql = lockTimeout ? `${BIND_SQL}, set_config('lock_timeout',$3,true)` : BIND_SQL;
417
1124
  const bindParams = lockTimeout ? [
418
1125
  role,
419
1126
  claimsJson,
@@ -422,25 +1129,118 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
422
1129
  role,
423
1130
  claimsJson
424
1131
  ];
1132
+ if (options.statementTimeout !== void 0) {
1133
+ bindParams.push(options.statementTimeout);
1134
+ bindSql += `, set_config('statement_timeout',$${bindParams.length},true)`;
1135
+ }
425
1136
  let opening = null;
426
1137
  let release = null;
427
1138
  let fail = null;
428
1139
  let settled = null;
429
- const ensure = /* @__PURE__ */ __name(() => {
1140
+ let closed = false;
1141
+ const assertAccess = /* @__PURE__ */ __name(() => {
1142
+ if (closed) throw new Error("Database transaction has settled; its handle cannot be used again");
1143
+ options.control?.check();
1144
+ options.trace?.check();
1145
+ if (atomicScope.getStore() !== options.scope) {
1146
+ throw new Error("Database.$atomic cannot use a handle from another transaction or identity; use its tx argument");
1147
+ }
1148
+ }, "assertAccess");
1149
+ let savepointALS;
1150
+ let activeSavepoint;
1151
+ const pendingSavepoints = /* @__PURE__ */ new Set();
1152
+ const scoped = /* @__PURE__ */ __name((tx, checkParent = assertAccess, owner, children = /* @__PURE__ */ new Set()) => {
1153
+ const access = /* @__PURE__ */ __name(() => {
1154
+ checkParent();
1155
+ if (activeSavepoint !== owner || savepointALS?.getStore() !== owner) {
1156
+ throw new Error("Database connection belongs to another active savepoint; use its tx argument and await it before using the parent handle");
1157
+ }
1158
+ }, "access");
1159
+ return {
1160
+ async unsafe(text, params, metadata) {
1161
+ access();
1162
+ const run = /* @__PURE__ */ __name(() => {
1163
+ const query = tx.unsafe(text, params);
1164
+ return options.control ? options.control.watch(query) : query;
1165
+ }, "run");
1166
+ return options.trace ? options.trace.query(text, metadata?.name, run) : run();
1167
+ },
1168
+ async savepoint(fn) {
1169
+ access();
1170
+ const token = /* @__PURE__ */ Symbol("savepoint"), descendants = /* @__PURE__ */ new Set();
1171
+ savepointALS ??= new AsyncLocalStorage();
1172
+ activeSavepoint = token;
1173
+ const work = (async () => {
1174
+ try {
1175
+ return await tx.savepoint(async (sp) => {
1176
+ let ended = false;
1177
+ const check = /* @__PURE__ */ __name(() => {
1178
+ checkParent();
1179
+ if (ended) throw new Error("Database savepoint has settled; its handle cannot be used again");
1180
+ }, "check");
1181
+ try {
1182
+ const callback = savepointALS.run(token, () => fn(scoped(sp, check, token, descendants)));
1183
+ return await (options.control ? options.control.race(callback) : callback);
1184
+ } finally {
1185
+ if (descendants.size) await Promise.allSettled([
1186
+ ...descendants
1187
+ ]);
1188
+ ended = true;
1189
+ }
1190
+ });
1191
+ } finally {
1192
+ activeSavepoint = owner;
1193
+ }
1194
+ })();
1195
+ pendingSavepoints.add(work);
1196
+ children.add(work);
1197
+ try {
1198
+ return await work;
1199
+ } finally {
1200
+ pendingSavepoints.delete(work);
1201
+ children.delete(work);
1202
+ }
1203
+ }
1204
+ };
1205
+ }, "scoped");
1206
+ const ensure = /* @__PURE__ */ __name(async () => {
1207
+ assertAccess();
430
1208
  if (opening) return opening;
431
1209
  opening = new Promise((resolveTx2, rejectTx) => {
432
1210
  const parked = new Promise((res, rej) => {
433
1211
  release = res;
434
1212
  fail = rej;
435
1213
  });
1214
+ void parked.catch(() => void 0);
1215
+ const openingAt = options.trace ? performance.now() : 0;
436
1216
  settled = sql.begin(async (tx) => {
437
- await tx.unsafe(bindSql, bindParams);
438
- resolveTx2(tx);
1217
+ options.trace?.time("poolMs", openingAt);
1218
+ const setupAt = options.trace ? performance.now() : 0;
1219
+ options.control?.check();
1220
+ if (options.isolation !== void 0 || options.readOnly !== void 0) {
1221
+ const isolation = options.isolation ?? "read committed";
1222
+ validateAtomicOptions({
1223
+ isolation,
1224
+ readOnly: options.readOnly
1225
+ });
1226
+ await tx.unsafe(`SET TRANSACTION ISOLATION LEVEL ${isolation.toUpperCase()}${options.readOnly === void 0 ? "" : options.readOnly ? " READ ONLY" : " READ WRITE"}`);
1227
+ }
1228
+ if (options.control && sql.cancelBackend) {
1229
+ const ticket = await tx.unsafe("SELECT pg_backend_pid() AS pid, backend_start::text AS started FROM pg_stat_activity WHERE pid=pg_backend_pid()");
1230
+ if (!Number.isInteger(ticket[0]?.pid) || typeof ticket[0]?.started !== "string") throw new Error("Database cancellation cannot identify its own backend");
1231
+ options.control.bindBackend(() => sql.cancelBackend(ticket[0].pid, ticket[0].started));
1232
+ options.control.check();
1233
+ }
1234
+ const binding = tx.unsafe(bindSql, bindParams);
1235
+ await (options.control ? options.control.watch(binding) : binding);
1236
+ options.trace?.time("setupMs", setupAt);
1237
+ resolveTx2(scoped(tx));
439
1238
  await parked;
440
1239
  }).catch((e) => {
441
1240
  rejectTx(e);
442
1241
  throw e;
443
1242
  });
1243
+ void settled.catch(() => void 0);
444
1244
  });
445
1245
  return opening;
446
1246
  }, "ensure");
@@ -450,14 +1250,37 @@ function createLazyTransaction(sql, role, claimsJson, options = {}) {
450
1250
  return opening !== null;
451
1251
  },
452
1252
  async commit() {
1253
+ if (pendingSavepoints.size) await Promise.allSettled([
1254
+ ...pendingSavepoints
1255
+ ]);
1256
+ options.control?.check();
1257
+ options.trace?.check();
1258
+ closed = true;
453
1259
  if (!opening) return;
1260
+ const started = options.trace ? performance.now() : 0;
454
1261
  release();
455
- await settled;
1262
+ try {
1263
+ await settled;
1264
+ options.trace?.committed();
1265
+ } catch (error) {
1266
+ options.trace?.commitFailed();
1267
+ throw error;
1268
+ } finally {
1269
+ options.trace?.time("commitMs", started);
1270
+ }
456
1271
  },
457
1272
  async rollback(reason) {
1273
+ closed = true;
458
1274
  if (!opening) return;
1275
+ await options.control?.drainCancellation();
1276
+ if (pendingSavepoints.size) await Promise.allSettled([
1277
+ ...pendingSavepoints
1278
+ ]);
1279
+ const started = options.trace ? performance.now() : 0;
459
1280
  fail(reason);
460
1281
  await settled?.catch(() => void 0);
1282
+ options.trace?.time("rollbackMs", started);
1283
+ options.trace?.rolledBack();
461
1284
  }
462
1285
  };
463
1286
  }
@@ -472,7 +1295,7 @@ __name(asWireValue, "asWireValue");
472
1295
  function asWireRow(row) {
473
1296
  if (row === null || typeof row !== "object") return row;
474
1297
  const out = {};
475
- for (const [key, value] of Object.entries(row)) out[key] = asWireValue(value);
1298
+ for (const [key2, value] of Object.entries(row)) out[key2] = asWireValue(value);
476
1299
  return out;
477
1300
  }
478
1301
  __name(asWireRow, "asWireRow");
@@ -484,7 +1307,7 @@ function toVectorLiteral(v) {
484
1307
  return `[${v.join(",")}]`;
485
1308
  }
486
1309
  __name(toVectorLiteral, "toVectorLiteral");
487
- function vectorColumnsOf(schema, table) {
1310
+ function computeVectorColumns(schema, table) {
488
1311
  const out = /* @__PURE__ */ new Set();
489
1312
  const def = tableDefOf(table, schema);
490
1313
  if (def) {
@@ -495,9 +1318,9 @@ function vectorColumnsOf(schema, table) {
495
1318
  }
496
1319
  return out;
497
1320
  }
498
- __name(vectorColumnsOf, "vectorColumnsOf");
499
- function tableDefOf(key, schema = currentSchema) {
500
- return schema.tables?.[key] ?? null;
1321
+ __name(computeVectorColumns, "computeVectorColumns");
1322
+ function tableDefOf(key2, schema = currentSchema) {
1323
+ return schema.tables?.[key2] ?? null;
501
1324
  }
502
1325
  __name(tableDefOf, "tableDefOf");
503
1326
  function reviveVectors(row, vectorCols) {
@@ -510,7 +1333,7 @@ function reviveVectors(row, vectorCols) {
510
1333
  return row;
511
1334
  }
512
1335
  __name(reviveVectors, "reviveVectors");
513
- function transformsOf(schema, table) {
1336
+ function computeTransforms(schema, table) {
514
1337
  const out = /* @__PURE__ */ new Map();
515
1338
  const def = tableDefOf(table, schema);
516
1339
  if (def) {
@@ -533,7 +1356,7 @@ function transformsOf(schema, table) {
533
1356
  }
534
1357
  return out;
535
1358
  }
536
- __name(transformsOf, "transformsOf");
1359
+ __name(computeTransforms, "computeTransforms");
537
1360
  function applyFromDb(row, transforms) {
538
1361
  if (transforms.size === 0) return row;
539
1362
  for (const [col, t] of transforms) {
@@ -547,13 +1370,27 @@ function applyFromDb(row, transforms) {
547
1370
  __name(applyFromDb, "applyFromDb");
548
1371
  function asTableRow(table, row) {
549
1372
  const revived = reviveVectors(asWireRow(row), vectorColumnsOf(currentSchema, table));
550
- return applyFromDb(revived, transformsOf(currentSchema, table));
1373
+ return foldCan(table, applyFromDb(revived, transformsOf(currentSchema, table)), false);
551
1374
  }
552
1375
  __name(asTableRow, "asTableRow");
553
- function asTableRows(table, rows) {
1376
+ function asTableRows(table, rows, withSpec, partial = false) {
554
1377
  const vectorCols = vectorColumnsOf(currentSchema, table);
555
1378
  const transforms = transformsOf(currentSchema, table);
556
- return rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
1379
+ const result = rows.map((row) => applyFromDb(reviveVectors(asWireRow(row), vectorCols), transforms));
1380
+ if (withSpec) for (const rel of currentRelations.get(table) ?? []) {
1381
+ const spec = withSpec[rel.name];
1382
+ if (!spec) continue;
1383
+ const nested = typeof spec === "object" ? spec.with : void 0;
1384
+ for (const row of result) {
1385
+ const value = row[rel.name];
1386
+ if (Array.isArray(value)) row[rel.name] = asTableRows(rel.to, value, nested);
1387
+ else if (value && typeof value === "object") row[rel.name] = asTableRows(rel.to, [
1388
+ value
1389
+ ], nested)[0];
1390
+ }
1391
+ }
1392
+ for (const row of result) foldCan(table, row, partial);
1393
+ return result;
557
1394
  }
558
1395
  __name(asTableRows, "asTableRows");
559
1396
  function asBindParams(table, cols, data, caller) {
@@ -796,7 +1633,7 @@ function excludeSql(pk, ids, add) {
796
1633
  return ` AND t.${quoteIdent(pk)}::text <> ALL(ARRAY[${ids.map((x) => add(x)).join(", ")}])`;
797
1634
  }
798
1635
  __name(excludeSql, "excludeSql");
799
- function primaryKeyOf(table) {
1636
+ function computePrimaryKey(table) {
800
1637
  const t = tableDefOf(table);
801
1638
  const columns = t?.columns ?? {};
802
1639
  const cols = Object.keys(columns);
@@ -816,7 +1653,7 @@ function primaryKeyOf(table) {
816
1653
  const pks = cols.filter((c) => pkOf(columns[c]));
817
1654
  return pks.length === 1 ? pks[0] : null;
818
1655
  }
819
- __name(primaryKeyOf, "primaryKeyOf");
1656
+ __name(computePrimaryKey, "computePrimaryKey");
820
1657
  function isSinglePkEquality(where, pk) {
821
1658
  if (pk === null) return false;
822
1659
  const keys = Object.keys(where);
@@ -831,12 +1668,270 @@ function assertNotAppendOnly(caller, table) {
831
1668
  throw new Error(`${caller}(${table}): "${table}" append-only \u2014 sat\u0131r g\xFCncellenemez ya da silinemez. D\xFCzeltme i\xE7in TELAF\u0130 KAYDI ekleyin: insert("${table}", { \u2026 }) ile ters i\u015Fareti ta\u015F\u0131yan yeni bir sat\u0131r yaz\u0131n; ge\xE7mi\u015F oldu\u011Fu gibi kals\u0131n diye tablo b\xF6yle bildirilmi\u015F (appendOnly: true).`);
832
1669
  }
833
1670
  __name(assertNotAppendOnly, "assertNotAppendOnly");
834
- function schemaColumns(table) {
1671
+ function computeSchemaColumns(table) {
835
1672
  const t = tableDefOf(table);
836
1673
  const cols = t?.columns ? Object.keys(t.columns) : [];
837
1674
  return cols.length > 0 ? new Set(cols) : null;
838
1675
  }
839
- __name(schemaColumns, "schemaColumns");
1676
+ __name(computeSchemaColumns, "computeSchemaColumns");
1677
+ function timestampResult(expr) {
1678
+ return `CASE WHEN isfinite(${expr}) AND extract(year FROM ${expr} AT TIME ZONE 'UTC') BETWEEN 1 AND 9999 THEN to_char(${expr} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') ELSE ${expr}::text END`;
1679
+ }
1680
+ __name(timestampResult, "timestampResult");
1681
+ function columnResult(table, column, alias) {
1682
+ const expr = `${alias ? `${quoteIdent(alias)}.` : ""}${quoteIdent(column)}`;
1683
+ return columnTypeOf(table, column) === "timestamp" ? timestampResult(expr) : expr;
1684
+ }
1685
+ __name(columnResult, "columnResult");
1686
+ function projectedColumn(table, column, alias) {
1687
+ const expr = columnResult(table, column, alias);
1688
+ return columnTypeOf(table, column) === "timestamp" ? `${expr} AS ${quoteIdent(column)}` : expr;
1689
+ }
1690
+ __name(projectedColumn, "projectedColumn");
1691
+ var CAN_PREFIX = "can:";
1692
+ var canMemo = /* @__PURE__ */ new WeakMap();
1693
+ function canRoleGate(roles) {
1694
+ if (roles.length === 0) return null;
1695
+ const names = roles.flatMap((r) => [
1696
+ r,
1697
+ `backend_${r}`
1698
+ ]).map((r) => `'${r.replace(/'/g, "''")}'`);
1699
+ return `(current_user = ANY (ARRAY[${names.join(", ")}]))`;
1700
+ }
1701
+ __name(canRoleGate, "canRoleGate");
1702
+ function canEntries(def, alias) {
1703
+ const policies = def.policies ?? [];
1704
+ const dropped = [];
1705
+ const restrictive = [];
1706
+ for (const p of policies) {
1707
+ if (p.permissive !== false || p.command !== "update" && p.command !== "all") continue;
1708
+ const r = renderCanUsing(p, alias, def.name);
1709
+ if (r === null) {
1710
+ return {
1711
+ entries: [],
1712
+ dropped: policies.filter((q) => q.command === "update" && q.permissive !== false).map((q) => ({
1713
+ name: q.name,
1714
+ reason: `restrictive policy "${p.name}" cannot be evaluated as a column`
1715
+ }))
1716
+ };
1717
+ }
1718
+ const rGate = canRoleGate(p.roles ?? []);
1719
+ restrictive.push(rGate === null ? r : `((NOT ${rGate}) OR ${r})`);
1720
+ }
1721
+ const entries = [];
1722
+ for (const p of policies) {
1723
+ if (p.command !== "update" || p.permissive === false) continue;
1724
+ if (Buffer.byteLength(CAN_PREFIX + p.name, "utf8") > 63) {
1725
+ dropped.push({
1726
+ name: p.name,
1727
+ reason: "its name is longer than 59 bytes and Postgres would truncate the column alias"
1728
+ });
1729
+ continue;
1730
+ }
1731
+ const using = renderCanUsing(p, alias, def.name);
1732
+ if (using === null) {
1733
+ dropped.push({
1734
+ name: p.name,
1735
+ reason: "its using expression cannot be selected as a column (null; a raw string that is table-qualified, carries a comment or unbalanced parentheses, or calls auth.has_permission outside a (select \u2026) wrapper; or a shape this engine does not render)"
1736
+ });
1737
+ continue;
1738
+ }
1739
+ const gate = canRoleGate(p.roles ?? []);
1740
+ const parts = [
1741
+ ...gate === null ? [] : [
1742
+ gate
1743
+ ],
1744
+ using,
1745
+ ...restrictive
1746
+ ];
1747
+ entries.push({
1748
+ name: p.name,
1749
+ using: parts.length === 1 ? using : `(${parts.join(" AND ")})`
1750
+ });
1751
+ }
1752
+ return {
1753
+ entries,
1754
+ dropped
1755
+ };
1756
+ }
1757
+ __name(canEntries, "canEntries");
1758
+ var CanRenderError = class CanRenderError2 extends Error {
1759
+ static {
1760
+ __name(this, "CanRenderError");
1761
+ }
1762
+ };
1763
+ var ENGINE_ALIASES = /^(t|locked|w\d+|wj\d+|x|q|g|p|upd)$/;
1764
+ function renderCanUsing(p, qualifier, tableName) {
1765
+ if (p.using === null || p.using === void 0) return null;
1766
+ if (typeof p.using === "string") {
1767
+ const withoutLiterals = p.using.replace(/'(?:[^']|'')*'/g, "''");
1768
+ if (/--|\/\*/.test(withoutLiterals)) return null;
1769
+ if (withoutLiterals.replace(/''/g, "").includes("'")) return null;
1770
+ let depth = 0;
1771
+ for (const ch of withoutLiterals) {
1772
+ if (ch === "(") depth++;
1773
+ else if (ch === ")" && --depth < 0) return null;
1774
+ }
1775
+ if (depth !== 0) return null;
1776
+ const tbl = tableName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1777
+ const qualified = new RegExp(`(^|[^A-Za-z0-9_])(?:"?[A-Za-z_][A-Za-z0-9_]*"?\\s*\\.\\s*)?"?${tbl}"?\\s*\\.`);
1778
+ if (qualified.test(withoutLiterals)) return null;
1779
+ const calls = withoutLiterals.match(/auth\s*\.\s*"?has_permission"?\s*\(/gi) ?? [];
1780
+ const wrapped = withoutLiterals.match(/\(\s*select\s*[(\s]*auth\s*\.\s*"?has_permission"?\s*\(/gi) ?? [];
1781
+ if (calls.length !== wrapped.length) return null;
1782
+ return `(${p.using})`;
1783
+ }
1784
+ try {
1785
+ const rendered = canExprSql(p.using, qualifier);
1786
+ const selfParenthesized = p.using.kind === "binop" || p.using.kind === "and" || p.using.kind === "or" || p.using.kind === "not" || p.using.kind === "isNull" || p.using.kind === "exists";
1787
+ return selfParenthesized ? rendered : `(${rendered})`;
1788
+ } catch (err) {
1789
+ if (err instanceof CanRenderError) return null;
1790
+ throw err;
1791
+ }
1792
+ }
1793
+ __name(renderCanUsing, "renderCanUsing");
1794
+ var CAN_BINOPS = {
1795
+ eq: "=",
1796
+ neq: "<>",
1797
+ gt: ">",
1798
+ gte: ">=",
1799
+ lt: "<",
1800
+ lte: "<="
1801
+ };
1802
+ function canLiteral(v) {
1803
+ if (v === null) return "NULL";
1804
+ if (typeof v === "boolean") return v ? "TRUE" : "FALSE";
1805
+ if (typeof v === "number") {
1806
+ if (!Number.isFinite(v)) throw new CanRenderError("can: non-finite literal");
1807
+ return String(v);
1808
+ }
1809
+ return `'${v.replace(/'/g, "''")}'`;
1810
+ }
1811
+ __name(canLiteral, "canLiteral");
1812
+ function canExprSql(e, qualifier) {
1813
+ switch (e.kind) {
1814
+ case "col":
1815
+ return e.alias !== void 0 && e.alias !== "" ? `${quoteIdent(e.alias)}.${quoteIdent(e.name)}` : `${qualifier}.${quoteIdent(e.name)}`;
1816
+ case "authUid":
1817
+ return "(SELECT auth.uid())";
1818
+ case "lit":
1819
+ return canLiteral(e.value);
1820
+ case "binop": {
1821
+ const op = CAN_BINOPS[e.op];
1822
+ if (op === void 0) throw new CanRenderError(`can: unknown operator ${String(e.op)}`);
1823
+ return `(${canExprSql(e.left, qualifier)} ${op} ${canExprSql(e.right, qualifier)})`;
1824
+ }
1825
+ case "and":
1826
+ return e.parts.length === 0 ? "(true)" : `(${e.parts.map((p) => canExprSql(p, qualifier)).join(" AND ")})`;
1827
+ case "or":
1828
+ return e.parts.length === 0 ? "(false)" : `(${e.parts.map((p) => canExprSql(p, qualifier)).join(" OR ")})`;
1829
+ case "not":
1830
+ return `(NOT ${canExprSql(e.expr, qualifier)})`;
1831
+ case "isNull":
1832
+ return `(${canExprSql(e.expr, qualifier)} IS ${e.negated ? "NOT " : ""}NULL)`;
1833
+ case "exists":
1834
+ if (e.alias === "" || ENGINE_ALIASES.test(e.alias)) {
1835
+ throw new CanRenderError(`can: exists alias "${e.alias}" collides with the engine's own aliases \u2014 the correlation would silently collapse`);
1836
+ }
1837
+ return `EXISTS (SELECT 1 FROM ${quoteTable(e.table)} ${quoteIdent(e.alias)} WHERE ${canExprSql(e.predicate, qualifier)})`;
1838
+ default:
1839
+ throw new CanRenderError(`can: unknown policy expression kind ${String(e.kind)}`);
1840
+ }
1841
+ }
1842
+ __name(canExprSql, "canExprSql");
1843
+ function canEntriesOf(table, alias) {
1844
+ const byAlias = memoTable(canMemo, currentSchema, table, () => /* @__PURE__ */ new Map());
1845
+ const hit = byAlias.get(alias);
1846
+ if (hit !== void 0) return hit;
1847
+ const def = tableDefOf(table);
1848
+ const warnOnce = /* @__PURE__ */ __name((msg) => {
1849
+ if (byAlias.has("__warned")) return;
1850
+ byAlias.set("__warned", []);
1851
+ console.warn(msg);
1852
+ }, "warnOnce");
1853
+ if (def === null || def.policies === void 0) {
1854
+ byAlias.set(alias, []);
1855
+ return [];
1856
+ }
1857
+ if (hasCanColumn(table)) {
1858
+ warnOnce(`can(${table}): the table declares a column or relation named "can", so the policy echo is disabled for it \u2014 rename it to get can`);
1859
+ byAlias.set(alias, []);
1860
+ return [];
1861
+ }
1862
+ const { entries, dropped } = canEntries(def, alias === "" ? quoteTable(table) : alias);
1863
+ if (dropped.length > 0) {
1864
+ warnOnce(dropped.map((d) => `can(${table}): policy "${d.name}" is not exposed in can \u2014 ${d.reason}; the database still enforces it`).join("\n"));
1865
+ }
1866
+ byAlias.set(alias, entries);
1867
+ return entries;
1868
+ }
1869
+ __name(canEntriesOf, "canEntriesOf");
1870
+ function canOuter(table) {
1871
+ const cols = canProjection(table, "q");
1872
+ return cols.length === 0 ? "" : `, ${cols.join(", ")}`;
1873
+ }
1874
+ __name(canOuter, "canOuter");
1875
+ function canProjection(table, alias) {
1876
+ return canEntriesOf(table, alias).map((e) => `${e.using} AS ${quoteIdent(CAN_PREFIX + e.name)}`);
1877
+ }
1878
+ __name(canProjection, "canProjection");
1879
+ function canJsonPairs(table, alias) {
1880
+ return canEntriesOf(table, alias).map((e) => `${quoteLiteral(CAN_PREFIX + e.name)}, ${e.using}`);
1881
+ }
1882
+ __name(canJsonPairs, "canJsonPairs");
1883
+ function foldCan(table, row, partial) {
1884
+ let can = null;
1885
+ for (const key2 of Object.keys(row)) {
1886
+ if (!key2.startsWith(CAN_PREFIX)) continue;
1887
+ can ??= [];
1888
+ if (row[key2] === true) can.push(key2.slice(CAN_PREFIX.length));
1889
+ delete row[key2];
1890
+ }
1891
+ if (hasCanColumn(table)) return row;
1892
+ if (can !== null) row.can = can;
1893
+ else if (!partial && tableDefOf(table) !== null) {
1894
+ if (canEntriesOf(table, "").length > 0) {
1895
+ const flags = memoTable(canMemo, currentSchema, table, () => /* @__PURE__ */ new Map());
1896
+ if (!flags.has("__missing_warned")) {
1897
+ flags.set("__missing_warned", []);
1898
+ console.warn(`can(${table}): a full-row read returned no "can:*" columns although the table declares update policies \u2014 this statement shape does not carry can; the row gets can: []`);
1899
+ }
1900
+ }
1901
+ row.can = [];
1902
+ }
1903
+ return row;
1904
+ }
1905
+ __name(foldCan, "foldCan");
1906
+ function hasCanColumn(table) {
1907
+ const cols = tableDefOf(table)?.columns;
1908
+ if (cols !== void 0 && Object.prototype.hasOwnProperty.call(cols, "can")) return true;
1909
+ return (currentRelations.get(table) ?? []).some((rel) => rel.name === "can");
1910
+ }
1911
+ __name(hasCanColumn, "hasCanColumn");
1912
+ function rowProjection(table, alias, wildcard = alias ? `${alias}.*` : "*") {
1913
+ const cols = schemaColumns(table);
1914
+ let base = wildcard;
1915
+ if (cols && memoTable(timestampMemo, currentSchema, table, () => [
1916
+ ...cols
1917
+ ].some((c) => columnTypeOf(table, c) === "timestamp"))) {
1918
+ const projections = memoTable(projectionMemo, currentSchema, table, () => /* @__PURE__ */ new Map());
1919
+ const key2 = alias ?? "";
1920
+ let sql = projections.get(key2);
1921
+ if (sql === void 0) {
1922
+ sql = [
1923
+ ...cols
1924
+ ].map((c) => projectedColumn(table, c, alias)).join(", ");
1925
+ projections.set(key2, sql);
1926
+ }
1927
+ base = sql;
1928
+ }
1929
+ const can = canProjection(table, alias ?? "");
1930
+ if (can.length === 0) return base;
1931
+ const rows = base === "*" && alias !== void 0 ? `${alias}.*` : base;
1932
+ return `${rows}, ${can.join(", ")}`;
1933
+ }
1934
+ __name(rowProjection, "rowProjection");
840
1935
  function colRefSql(table, known, ref, caller, alias = "t") {
841
1936
  const name = ref.$col;
842
1937
  const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
@@ -850,13 +1945,13 @@ function selectClause(table, known, select) {
850
1945
  if (select !== void 0 && select.length === 0) {
851
1946
  throw new Error(`findMany(${table}): select bo\u015F bir liste ald\u0131 \u2014 bu "hepsini ver" DE\u011E\u0130L, "hi\xE7birini saymad\u0131m" demek. Projeksiyon istemiyorsan\u0131z anahtar\u0131 hi\xE7 koymay\u0131n; istiyorsan\u0131z en az bir kolon ad\u0131 verin.`);
852
1947
  }
853
- if (select === void 0) return "*";
1948
+ if (select === void 0) return rowProjection(table, "t", "*");
854
1949
  return select.map((col) => {
855
1950
  const shapeOK = /^[A-Za-z_][A-Za-z0-9_]*$/.test(col);
856
1951
  if (known !== null ? !known.has(col) : !shapeOK) {
857
1952
  throw new Error(`findMany(${table}): select kolonu "${col}" tabloda yok`);
858
1953
  }
859
- return `t.${quoteIdent(col)}`;
1954
+ return columnTypeOf(table, col) === "timestamp" ? projectedColumn(table, col, "t") : `t.${quoteIdent(col)}`;
860
1955
  }).join(", ");
861
1956
  }
862
1957
  __name(selectClause, "selectClause");
@@ -926,7 +2021,7 @@ function compileWhereBare(table, colSet, where, add, caller = "search", alias =
926
2021
  const transforms = transformsOf(currentSchema, table);
927
2022
  const bindFor = /* @__PURE__ */ __name((col) => {
928
2023
  const t = transforms.get(col);
929
- return t?.toDb === void 0 ? add : (v) => add(v === null || v === void 0 ? v : t.toDb(v));
2024
+ return (v) => add(t?.toDb === void 0 || v === null || v === void 0 ? v : t.toDb(v), col, table);
930
2025
  }, "bindFor");
931
2026
  assertUsableFilter(caller, table, where);
932
2027
  if (isSqlFragment(where)) {
@@ -957,8 +2052,7 @@ function compileWhereBare(table, colSet, where, add, caller = "search", alias =
957
2052
  const known = rels.map((r) => r.name).sort();
958
2053
  throw new Error(`${caller}(${table}): has.${relName} diye bir ili\u015Fki yok` + (known.length > 0 ? ` (bilinenler: ${known.join(", ")}). \u0130li\u015Fkiler yabanc\u0131 anahtarlardan T\xDCRET\u0130L\u0130R; ad\u0131n\u0131 de\u011Fi\u015Ftirmek i\xE7in references(..., { as, reverseAs }).` : `. Bu tablo H\u0130\xC7 yabanc\u0131 anahtar bildirmiyor, ve ili\u015Fkiler onlardan T\xDCRET\u0130L\u0130R. Var olan bir veritaban\u0131ndan introspect edilmi\u015F \u015Femalarda bu s\u0131k olur: kolon \`text()\`/\`uuid()\` olarak durur, \`references(() => <tablo>.<kolon>)\` yoktur. Bildirdi\u011Finiz anda ili\u015Fki iki tarafta da adland\u0131r\u0131l\u0131r \u2014 ama bu CANLI veritaban\u0131na giden bir k\u0131s\u0131tt\u0131r ve mevcut yetim sat\u0131rlar onu d\xFC\u015F\xFCr\xFCr; \xF6nce onlar\u0131 say\u0131n.`));
959
2054
  }
960
- const ourCol = rel.kind === "many" ? primaryKeyOf(table) : rel.via;
961
- const theirCol = rel.kind === "many" ? rel.via : primaryKeyOf(rel.to);
2055
+ const [ourCol, theirCol] = relationColumns(table, rel);
962
2056
  if (ourCol === null || theirCol === null) {
963
2057
  const missing = ourCol === null ? table : rel.to;
964
2058
  throw new Error(`${caller}(${table}): has.${relName} kurulam\u0131yor \u2014 "${missing}" tablosunun tek kolonlu bir primary key'i yok. Korelasyon bir anahtar ister; \xE7ok kolonlu anahtarda ili\u015Fki \xFCzerinden s\xFCzmek yerine sqlFragment ile yaz\u0131n.`);
@@ -1096,8 +2190,8 @@ async function embedQuery(embed, text) {
1096
2190
  if (secretReader === null) {
1097
2191
  throw new Error(`query embed: secret reader ba\u011Flanmam\u0131\u015F \u2014 ${embed.apiKeyName} okunam\u0131yor`);
1098
2192
  }
1099
- const key = await secretReader(embed.apiKeyName);
1100
- if (key === null || key === "") {
2193
+ const key2 = await secretReader(embed.apiKeyName);
2194
+ if (key2 === null || key2 === "") {
1101
2195
  throw new Error(`query embed: vault'ta ${embed.apiKeyName} yok (FR-021/FR-025)`);
1102
2196
  }
1103
2197
  const url = (embed.baseURL ?? "https://api.openai.com/v1").replace(/\/$/, "") + "/embeddings";
@@ -1108,7 +2202,7 @@ async function embedQuery(embed, text) {
1108
2202
  method: "POST",
1109
2203
  headers: {
1110
2204
  "Content-Type": "application/json",
1111
- Authorization: `Bearer ${key}`
2205
+ Authorization: `Bearer ${key2}`
1112
2206
  },
1113
2207
  body: JSON.stringify({
1114
2208
  model: embed.model,
@@ -1166,14 +2260,14 @@ function buildValueTuple(table, cols, data, add, caller) {
1166
2260
  }).join(", ");
1167
2261
  }
1168
2262
  __name(buildValueTuple, "buildValueTuple");
1169
- function columnTypeOf(table, col) {
2263
+ function computeColumnType(table, col) {
1170
2264
  const def = tableDefOf(table);
1171
2265
  const c = def?.columns?.[col];
1172
2266
  if (c === void 0 || c === null) return null;
1173
2267
  const d = typeof c === "object" && "_def" in c ? c._def : c;
1174
2268
  return d !== null && typeof d === "object" && typeof d.type === "string" ? d.type : null;
1175
2269
  }
1176
- __name(columnTypeOf, "columnTypeOf");
2270
+ __name(computeColumnType, "computeColumnType");
1177
2271
  var NUMERIC_PG_TYPES = /* @__PURE__ */ new Set([
1178
2272
  "integer",
1179
2273
  "bigint",
@@ -1183,23 +2277,44 @@ function aggregateExpr(table, fn, col, alias) {
1183
2277
  const q = `t.${quoteIdent(col)}`;
1184
2278
  const t = columnTypeOf(table, col);
1185
2279
  if (fn === "sum" || fn === "avg") return `${fn}(${q})::text AS ${quoteIdent(alias)}`;
2280
+ if (t === "timestamp") return `${timestampResult(`${fn}(${q})`)} AS ${quoteIdent(alias)}`;
1186
2281
  return t !== null && NUMERIC_PG_TYPES.has(t) && t !== "integer" ? `${fn}(${q})::text AS ${quoteIdent(alias)}` : `${fn}(${q}) AS ${quoteIdent(alias)}`;
1187
2282
  }
1188
2283
  __name(aggregateExpr, "aggregateExpr");
1189
- function jsonObjectFor(table, cols, alias) {
2284
+ function jsonObjectFor(table, cols, alias, extra = []) {
1190
2285
  const pairs = cols.map((c) => {
1191
2286
  const t = columnTypeOf(table, c);
1192
- const expr = t !== null && (t === "numeric" || t === "bigint") ? `${alias}.${quoteIdent(c)}::text` : `${alias}.${quoteIdent(c)}`;
2287
+ const expr = t !== null && (t === "numeric" || t === "bigint") ? `${alias}.${quoteIdent(c)}::text` : t === "timestamp" ? timestampResult(`${alias}.${quoteIdent(c)}`) : `${alias}.${quoteIdent(c)}`;
1193
2288
  return `${quoteLiteral(c)}, ${expr}`;
1194
2289
  });
1195
- return `json_build_object(${pairs.join(", ")})`;
2290
+ return `json_build_object(${[
2291
+ ...pairs,
2292
+ ...extra
2293
+ ].join(", ")})`;
1196
2294
  }
1197
2295
  __name(jsonObjectFor, "jsonObjectFor");
1198
- function withClauses(table, spec, add, caller) {
2296
+ function relationColumns(table, relation) {
2297
+ const child = relation.kind === "many" ? relation.to : table;
2298
+ const value = tableDefOf(child)?.columns?.[relation.via];
2299
+ const column = value && typeof value === "object" && "_def" in value ? value._def : value;
2300
+ const target = column?.references?.column ?? primaryKeyOf(relation.kind === "many" ? table : relation.to);
2301
+ return relation.kind === "many" ? [
2302
+ target,
2303
+ relation.via
2304
+ ] : [
2305
+ relation.via,
2306
+ target
2307
+ ];
2308
+ }
2309
+ __name(relationColumns, "relationColumns");
2310
+ function withClauses(table, spec, add, caller, parentAlias = "t", depth = 0, counter = {
2311
+ value: 0
2312
+ }) {
2313
+ if (depth >= 3 && Object.values(spec).some(Boolean)) throw new Error(`${caller}(${table}): with supports at most three relation levels`);
1199
2314
  const rels = currentRelations.get(table) ?? [];
1200
2315
  const select = [];
1201
2316
  const joins = [];
1202
- let i = 0;
2317
+ const jsonPairs = [];
1203
2318
  for (const [name, opt] of Object.entries(spec)) {
1204
2319
  if (opt === false || opt === void 0) continue;
1205
2320
  const rel = rels.find((r) => r.name === name);
@@ -1207,13 +2322,13 @@ function withClauses(table, spec, add, caller) {
1207
2322
  const known = rels.map((r) => r.name).sort();
1208
2323
  throw new Error(`${caller}(${table}): with.${name} diye bir ili\u015Fki yok` + (known.length > 0 ? ` (bilinenler: ${known.join(", ")})` : `. Bu tablo hi\xE7 yabanc\u0131 anahtar bildirmiyor ve ili\u015Fkiler onlardan T\xDCRET\u0130L\u0130R`) + `. \u0130li\u015Fkiler references(() => <tablo>.<kolon>) ile bildirilir.`);
1209
2324
  }
1210
- const ourCol = rel.kind === "many" ? primaryKeyOf(table) : rel.via;
1211
- const theirCol = rel.kind === "many" ? rel.via : primaryKeyOf(rel.to);
2325
+ const [ourCol, theirCol] = relationColumns(table, rel);
1212
2326
  if (ourCol === null || theirCol === null) {
1213
2327
  throw new Error(`${caller}(${table}): with.${name} birincil anahtar bilmeden kurulamaz \u2014 ili\u015Fkinin bir ucu tablonun PK's\u0131 ve bu tablo tek kolonlu bir PK bildirmiyor.`);
1214
2328
  }
1215
2329
  const o = typeof opt === "object" && opt !== null ? opt : {};
1216
- const childAlias = `w${++i}`;
2330
+ const i = ++counter.value;
2331
+ const childAlias = `w${i}`;
1217
2332
  const childKnown = schemaColumns(rel.to);
1218
2333
  const childCols = Array.isArray(o["select"]) && o["select"].length > 0 ? o["select"] : childKnown !== null ? [
1219
2334
  ...childKnown
@@ -1226,28 +2341,103 @@ function withClauses(table, spec, add, caller) {
1226
2341
  throw new Error(`${caller}(${table}): with.${name}.select kolonu "${c}" "${rel.to}" tablosunda yok`);
1227
2342
  }
1228
2343
  }
1229
- const obj = jsonObjectFor(rel.to, childCols, childAlias);
2344
+ const nestedSpec = o["with"];
2345
+ if (nestedSpec !== void 0 && (nestedSpec === null || typeof nestedSpec !== "object" || Array.isArray(nestedSpec))) {
2346
+ throw new Error(`${caller}(${table}): with.${name}.with must be a relation object`);
2347
+ }
2348
+ const nested = nestedSpec === void 0 ? {
2349
+ joins: [],
2350
+ jsonPairs: []
2351
+ } : withClauses(rel.to, nestedSpec, add, caller, childAlias, depth + 1, counter);
2352
+ const nestedJoins = nested.joins.length ? ` ${nested.joins.join(" ")}` : "";
2353
+ const obj = jsonObjectFor(rel.to, childCols, childAlias, [
2354
+ ...nested.jsonPairs,
2355
+ ...canJsonPairs(rel.to, childAlias)
2356
+ ]);
1230
2357
  const innerWhere = o["where"] !== void 0 && o["where"] !== null ? compileWhereBare(rel.to, childKnown, o["where"], add, caller, childAlias, i) : "";
1231
- const link = `${childAlias}.${quoteIdent(theirCol)} = t.${quoteIdent(ourCol)}`;
2358
+ const link = `${childAlias}.${quoteIdent(theirCol)} = ${parentAlias}.${quoteIdent(ourCol)}`;
1232
2359
  const cond = innerWhere === "" ? link : `${link} AND ${innerWhere}`;
1233
- const inner = orderClauseFor(rel.to, childKnown, o["orderBy"], childAlias) + limitClause(o["limit"]);
2360
+ const requestedLimit = o["limit"];
2361
+ limitClause(requestedLimit);
2362
+ const relationLimit = rel.kind === "many" ? requestedLimit : Math.min(1, requestedLimit ?? 1);
2363
+ const inner = orderClauseFor(rel.to, childKnown, o["orderBy"], childAlias) + limitClause(relationLimit);
1234
2364
  const outAlias = `wj${i}`;
1235
2365
  if (rel.kind === "many") {
1236
- joins.push(`LEFT JOIN LATERAL (SELECT coalesce(json_agg(x.o), '[]'::json) AS d FROM (SELECT ${obj} AS o FROM ${quoteTable(rel.to)} ${childAlias} WHERE ${cond}${inner}) x) ${quoteIdent(outAlias)} ON true`);
2366
+ joins.push(`LEFT JOIN LATERAL (SELECT coalesce(json_agg(x.o), '[]'::json) AS d FROM (SELECT ${obj} AS o FROM ${quoteTable(rel.to)} ${childAlias}${nestedJoins} WHERE ${cond}${inner}) x) ${quoteIdent(outAlias)} ON true`);
1237
2367
  } else {
1238
- joins.push(`LEFT JOIN LATERAL (SELECT ${obj} AS d FROM ${quoteTable(rel.to)} ${childAlias} WHERE ${cond}${inner} LIMIT 1) ${quoteIdent(outAlias)} ON true`);
2368
+ joins.push(`LEFT JOIN LATERAL (SELECT ${obj} AS d FROM ${quoteTable(rel.to)} ${childAlias}${nestedJoins} WHERE ${cond}${inner}) ${quoteIdent(outAlias)} ON true`);
1239
2369
  }
1240
2370
  select.push(`${quoteIdent(outAlias)}."d" AS ${quoteIdent(name)}`);
2371
+ jsonPairs.push(`${quoteLiteral(name)}, ${quoteIdent(outAlias)}."d"`);
1241
2372
  }
1242
2373
  return {
1243
2374
  select,
1244
- joins
2375
+ joins,
2376
+ jsonPairs
1245
2377
  };
1246
2378
  }
1247
2379
  __name(withClauses, "withClauses");
1248
- function createOps(tx) {
2380
+ function compileFindMany(table, query = {}, opts = {}) {
2381
+ const params = [];
2382
+ const add = /* @__PURE__ */ __name((v) => {
2383
+ params.push(v);
2384
+ return `$${params.length}`;
2385
+ }, "add");
2386
+ const known = schemaColumns(table);
2387
+ const whereSql = compileWhere(table, known, query, add, "findMany");
2388
+ const order = orderClause(table, known, opts.orderBy);
2389
+ const limit = limitClause(opts.limit);
2390
+ const offset = offsetClause(opts.offset, opts.limit);
2391
+ const cols = selectClause(table, known, opts.select);
2392
+ const w = opts.with !== void 0 && Object.keys(opts.with).length > 0 ? withClauses(table, opts.with, add, "findMany") : {
2393
+ select: [],
2394
+ joins: []
2395
+ };
2396
+ const selectList = w.select.length > 0 ? `${cols === "*" ? "t.*" : cols}, ${w.select.join(", ")}` : cols;
2397
+ const joinSql = w.joins.length > 0 ? ` ${w.joins.join(" ")}` : "";
2398
+ const sql = `SELECT ${selectList} FROM ${quoteTable(table)} t${joinSql} WHERE true${whereSql}${order}${limit}${offset}`;
2399
+ return {
2400
+ sql,
2401
+ params
2402
+ };
2403
+ }
2404
+ __name(compileFindMany, "compileFindMany");
2405
+ var uniqueKeysCache = /* @__PURE__ */ new WeakMap();
2406
+ var pageSchemaCache = /* @__PURE__ */ new WeakMap();
2407
+ function pageSchema(table) {
2408
+ return memoTable(pageSchemaCache, currentSchema, table, () => {
2409
+ const def = tableDefOf(table);
2410
+ if (!def?.columns) throw new Error(`page(${table}) requires an installed table declaration`);
2411
+ const columns = /* @__PURE__ */ Object.create(null);
2412
+ const inlineKeys = [];
2413
+ for (const [name, column] of Object.entries(def.columns)) {
2414
+ const c = column;
2415
+ const d = c._def ?? c;
2416
+ columns[name] = {
2417
+ type: d.type,
2418
+ nullable: d.nullable
2419
+ };
2420
+ if (d.primaryKey) inlineKeys.push(name);
2421
+ }
2422
+ const primaryKey = def.primaryKey ?? inlineKeys;
2423
+ for (const name of primaryKey) if (columns[name]) columns[name].nullable = false;
2424
+ return {
2425
+ columns,
2426
+ primaryKey,
2427
+ stamp: fingerprint({
2428
+ columns,
2429
+ primaryKey
2430
+ })
2431
+ };
2432
+ });
2433
+ }
2434
+ __name(pageSchema, "pageSchema");
2435
+ function createOps(tx, context = {}) {
1249
2436
  const at = /* @__PURE__ */ __name(() => resolveTx(tx), "at");
1250
2437
  const ops = {
2438
+ diagnostics() {
2439
+ return context.trace?.snapshot ?? null;
2440
+ },
1251
2441
  // Generic dönüş, çağıranın İDDİASIDIR (FR-021). Motor her zaman satır objeleri
1252
2442
  // üretir; `T` onlara bir ad vermekten ibarettir ve verilmezse `unknown` kalır —
1253
2443
  // yani "bilmiyorum", ki ham SQL için doğru cevap odur.
@@ -1261,7 +2451,7 @@ function createOps(tx) {
1261
2451
  const params = [];
1262
2452
  const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1263
2453
  const tuple = buildValueTuple(table, cols, data, add, "insert");
1264
- const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`;
2454
+ const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING ${rowProjection(table)}`;
1265
2455
  const rows = await (await at()).unsafe(sql, params);
1266
2456
  const inserted = rows[0];
1267
2457
  if (!inserted) {
@@ -1283,8 +2473,9 @@ function createOps(tx) {
1283
2473
  * bir satırın fazla kolonu SESSİZCE YAZILMAZDI. Eksik kolon için `null`
1284
2474
  * yazın ya da varsayılanı istiyorsanız o satırları ayrı çağrıda ekleyin.
1285
2475
  */
1286
- async insertMany(table, rows, opts) {
1287
- if (rows.length === 0) return [];
2476
+ insertMany: /* @__PURE__ */ __name(async function(table, rows, opts) {
2477
+ validateInsertManyOptions(opts);
2478
+ if (rows.length === 0) return opts?.returning === false ? 0 : [];
1288
2479
  const first = rows[0];
1289
2480
  const cols = Object.keys(first);
1290
2481
  if (cols.length === 0) throw new Error(`insertMany into ${table}: no columns given`);
@@ -1299,11 +2490,15 @@ function createOps(tx) {
1299
2490
  const params = [];
1300
2491
  const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1301
2492
  const tuples = rows.map((r) => `(${buildValueTuple(table, cols, r, add, "insertMany")})`);
1302
- const tail = opts !== void 0 && opts.onConflict.length > 0 ? ` ${onConflictTail(cols, opts.onConflict, opts.action ?? "ignore")}` : "";
1303
- const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
2493
+ const tail = opts?.onConflict !== void 0 && opts.onConflict.length > 0 ? ` ${onConflictTail(cols, opts.onConflict, opts.action ?? "ignore")}` : "";
2494
+ const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail}${opts?.returning === false ? "" : ` RETURNING ${rowProjection(table)}`}`;
1304
2495
  const out = await (await at()).unsafe(sql, params);
2496
+ if (opts?.returning === false) {
2497
+ if (!Number.isSafeInteger(out.count) || out.count < 0) throw new Error("insertMany returning:false requires a driver with an affected row count");
2498
+ return out.count;
2499
+ }
1305
2500
  return out.map((r) => asTableRow(table, r));
1306
- },
2501
+ }, "insertMany"),
1307
2502
  /**
1308
2503
  * Bir ADI kilitle — satırı değil (FR-054).
1309
2504
  *
@@ -1317,9 +2512,9 @@ function createOps(tx) {
1317
2512
  * Transaction kapsamlısı commit/rollback'te Postgres tarafından bırakılır,
1318
2513
  * yani unutulması imkânsız.
1319
2514
  */
1320
- async advisoryXactLock(key) {
2515
+ async advisoryXactLock(key2) {
1321
2516
  await (await at()).unsafe("select pg_advisory_xact_lock(hashtextextended($1, 0))", [
1322
- key
2517
+ key2
1323
2518
  ]);
1324
2519
  },
1325
2520
  /**
@@ -1393,7 +2588,7 @@ function createOps(tx) {
1393
2588
  const params = [];
1394
2589
  const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1395
2590
  const tuple = buildValueTuple(table, cols, data, add, "claim");
1396
- const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
2591
+ const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING ${rowProjection(table)}`, params);
1397
2592
  if (!rows[0]) {
1398
2593
  throw new Error(`claim(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si).`);
1399
2594
  }
@@ -1410,7 +2605,7 @@ function createOps(tx) {
1410
2605
  return `$${params.length}`;
1411
2606
  }, "add");
1412
2607
  const where = keyCols.map((c) => `t.${quoteIdent(c)} = ${add(unique[c])}`).join(" AND ");
1413
- const found = await (await at()).unsafe(`SELECT * FROM ${quoteTable(table)} t WHERE ${where} LIMIT 1`, params);
2608
+ const found = await (await at()).unsafe(`SELECT ${rowProjection(table, "t", "*")} FROM ${quoteTable(table)} t WHERE ${where} LIMIT 1`, params);
1414
2609
  if (!found[0]) {
1415
2610
  const named = constraintOf(e);
1416
2611
  throw new Error(`claim(${table}): benzersizlik ihlali${named ? ` (${named})` : ""} yakaland\u0131 ama ${keyCols.join(", ")} ile sat\u0131r bulunamad\u0131. Ya ihlal BA\u015EKA bir unique k\u0131s\u0131ttan geliyor \u2014 o zaman claim() bu \xE7a\u011Fr\u0131 i\xE7in yanl\u0131\u015F ara\xE7 \u2014 ya da sat\u0131r var ve bir RLS policy'si onu bu \xE7a\u011F\u0131randan gizliyor.`);
@@ -1447,13 +2642,13 @@ function createOps(tx) {
1447
2642
  const bindData = [];
1448
2643
  const addPut = /* @__PURE__ */ __name((v) => `$${bindData.push(v)}`, "addPut");
1449
2644
  const ph = buildValueTuple(table, cols, data, addPut, "put");
1450
- const insertSql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING *`;
2645
+ const insertSql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${ph}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) WHERE "valid_to" IS NULL DO NOTHING RETURNING ${rowProjection(table)}`;
1451
2646
  const live = await at();
1452
2647
  const first = await live.unsafe(insertSql, bindData);
1453
2648
  if (first[0]) return asTableRow(table, first[0]);
1454
2649
  const condSql = conflict.map((c, i) => `${quoteIdent(c)} = $${i + 1}`).join(" AND ");
1455
2650
  const condBind = conflict.map((c) => data[c]);
1456
- const cur = await live.unsafe(`SELECT * FROM ${quoteTable(table)} WHERE ${condSql} AND "valid_to" IS NULL`, condBind);
2651
+ const cur = await live.unsafe(`SELECT ${rowProjection(table)} FROM ${quoteTable(table)} WHERE ${condSql} AND "valid_to" IS NULL`, condBind);
1457
2652
  const old = cur[0];
1458
2653
  if (old === void 0) {
1459
2654
  throw new Error(`upsert into ${table}: e\u015Fzamanl\u0131 supersede yar\u0131\u015F\u0131 \u2014 ge\xE7erli sat\u0131r bulunamad\u0131, iste\u011Fi yineleyin (D-020)`);
@@ -1479,7 +2674,7 @@ function createOps(tx) {
1479
2674
  const conflictSet = new Set(conflict);
1480
2675
  const assignments = cols.filter((c) => !conflictSet.has(c)).map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`);
1481
2676
  const action = assignments.length ? `DO UPDATE SET ${assignments.join(", ")}` : `DO UPDATE SET ${quoteIdent(conflict[0])} = EXCLUDED.${quoteIdent(conflict[0])}`;
1482
- const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING *`;
2677
+ const sql = `INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflict.map(quoteIdent).join(", ")}) ${action} RETURNING ${rowProjection(table)}`;
1483
2678
  const rows = await (await at()).unsafe(sql, params);
1484
2679
  const row = rows[0];
1485
2680
  if (!row) {
@@ -1497,7 +2692,7 @@ function createOps(tx) {
1497
2692
  return `$${params.length}`;
1498
2693
  }, "add");
1499
2694
  const assignments = buildSetAssignments(table, cols, data, add, "update");
1500
- const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = ${add(id)} RETURNING *`;
2695
+ const sql = `UPDATE ${quoteTable(table)} SET ${assignments} WHERE id = ${add(id)} RETURNING ${rowProjection(table)}`;
1501
2696
  const rows = await (await at()).unsafe(sql, params);
1502
2697
  return rows[0] ? asTableRow(table, rows[0]) : null;
1503
2698
  },
@@ -1549,10 +2744,10 @@ function createOps(tx) {
1549
2744
  assertHasPredicate(whereSql, "updateMany", table);
1550
2745
  const pk = primaryKeyOf(table);
1551
2746
  const wantRows = opts?.returning !== false;
1552
- const tail = wantRows ? "RETURNING *" : "RETURNING 1";
2747
+ const tail = wantRows ? `RETURNING ${rowProjection(table, "t", "*")}` : "RETURNING 1";
1553
2748
  const simple = isSinglePkEquality(where, pk) || pk === null;
1554
- const body = simple ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} ${tail}` : `UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked.${quoteIdent(pk)} ` + (wantRows ? "RETURNING t.*" : "RETURNING 1");
1555
- const lockedCte = simple ? "" : `WITH locked AS (SELECT t.${quoteIdent(pk)} FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) `;
2749
+ const body = simple ? `UPDATE ${quoteTable(table)} AS t SET ${assignments} WHERE true${whereSql} ${tail}` : `UPDATE ${quoteTable(table)} AS t SET ${assignments} FROM locked WHERE t.${quoteIdent(pk)} = locked."__pb_lock" ` + (wantRows ? `RETURNING ${rowProjection(table, "t")}` : "RETURNING 1");
2750
+ const lockedCte = simple ? "" : `WITH locked AS (SELECT t.${quoteIdent(pk)} AS "__pb_lock" FROM ${quoteTable(table)} t WHERE true${whereSql} ORDER BY t.${quoteIdent(pk)} FOR NO KEY UPDATE) `;
1556
2751
  const sql = wantRows ? `${lockedCte}${body}` : simple ? `WITH upd AS (${body}) SELECT count(*)::int AS n FROM upd` : `${lockedCte.replace(/ $/, "")}, upd AS (${body}) SELECT count(*)::int AS n FROM upd`;
1557
2752
  const out = await (await at()).unsafe(sql, params);
1558
2753
  if (!wantRows) return Number(out[0]?.["n"] ?? 0);
@@ -1636,7 +2831,7 @@ function createOps(tx) {
1636
2831
  }
1637
2832
  const groups = q.groupBy ?? [];
1638
2833
  for (const col of groups) check(col, "groupBy");
1639
- const groupSel = groups.map((c) => `t.${quoteIdent(c)} AS ${quoteIdent(`g__${c}`)}`);
2834
+ const groupSel = groups.map((c) => `${columnTypeOf(table, c) === "timestamp" ? columnResult(table, c, "t") : `t.${quoteIdent(c)}`} AS ${quoteIdent(`g__${c}`)}`);
1640
2835
  const params = [];
1641
2836
  const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1642
2837
  const whereSql = compileWhere(table, known, q.where ?? {}, add, "aggregate");
@@ -1670,32 +2865,114 @@ function createOps(tx) {
1670
2865
  const rows = await (await at()).unsafe(sql, params);
1671
2866
  return Number(rows[0]?.n ?? 0);
1672
2867
  },
2868
+ async findUnique(table, where, opts = {}) {
2869
+ const keys = memoTable(uniqueKeysCache, currentSchema, table, () => uniqueKeysOf(tableDefOf(table) ?? {}));
2870
+ assertUniqueLookup(table, keys, where);
2871
+ const rows = await ops.findMany(table, where, {
2872
+ ...opts,
2873
+ select: opts.select === void 0 ? void 0 : [
2874
+ ...opts.select
2875
+ ],
2876
+ limit: 2
2877
+ });
2878
+ if (rows.length > 1) throw new Error(`findUnique(${table}): deployed constraints disagree with the schema; multiple rows matched`);
2879
+ return rows[0] ?? null;
2880
+ },
1673
2881
  async findById(table, id) {
1674
- const rows = await (await at()).unsafe(`SELECT * FROM ${quoteTable(table)} WHERE id = $1`, [
2882
+ const rows = await (await at()).unsafe(`SELECT ${rowProjection(table)} FROM ${quoteTable(table)} WHERE id = $1`, [
1675
2883
  id
1676
2884
  ]);
1677
2885
  return rows[0] ? asTableRow(table, rows[0]) : null;
1678
2886
  },
2887
+ compileQuery(query) {
2888
+ assertReadContract(query, tableDefOf(query.table)?.columns);
2889
+ return compileFindMany(query.table, query.where, query.options);
2890
+ },
2891
+ async readQuery(query) {
2892
+ const compiled = ops.compileQuery(query);
2893
+ return asTableRows(query.table, await (await at()).unsafe(compiled.sql, compiled.params, {
2894
+ name: query.name
2895
+ }), void 0, true);
2896
+ },
2897
+ async explainQuery(query) {
2898
+ const compiled = ops.compileQuery(query);
2899
+ return await (await at()).unsafe(`EXPLAIN (FORMAT JSON) ${compiled.sql}`, compiled.params, {
2900
+ name: `${query.name}.explain`
2901
+ });
2902
+ },
1679
2903
  async findMany(table, query = {}, opts = {}) {
2904
+ const compiled = compileFindMany(table, query, opts);
2905
+ return asTableRows(table, await (await at()).unsafe(compiled.sql, compiled.params), opts.with, opts.select !== void 0);
2906
+ },
2907
+ async page(table, query = {}, opts = {}) {
2908
+ const secret = cursorKey();
2909
+ const schema = pageSchema(table);
2910
+ const { order, size, backward, token } = pageOrder(schema.columns, schema.primaryKey, opts);
1680
2911
  const params = [];
1681
- const add = /* @__PURE__ */ __name((v) => {
1682
- params.push(v);
1683
- return `$${params.length}`;
1684
- }, "add");
2912
+ const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
1685
2913
  const known = schemaColumns(table);
1686
- const whereSql = compileWhere(table, known, query, add, "findMany");
1687
- const order = orderClause(table, known, opts.orderBy);
1688
- const limit = limitClause(opts.limit);
1689
- const offset = offsetClause(opts.offset, opts.limit);
1690
- const cols = selectClause(table, known, opts.select);
1691
- const w = opts.with !== void 0 && Object.keys(opts.with).length > 0 ? withClauses(table, opts.with, add, "findMany") : {
2914
+ const where = compileWhere(table, known, query, add, "page");
2915
+ const cols = selectClause(table, known, opts.select === void 0 ? void 0 : [
2916
+ ...opts.select
2917
+ ]);
2918
+ const scope = fingerprint({
2919
+ table,
2920
+ schema: schema.stamp,
2921
+ order,
2922
+ where,
2923
+ params,
2924
+ select: opts.select,
2925
+ with: opts.with,
2926
+ identity: context.cursorScope ?? "direct"
2927
+ });
2928
+ const values = token === void 0 ? void 0 : decodeCursor(secret, scope, token, order.length);
2929
+ const column = /* @__PURE__ */ __name((name) => `t.${quoteIdent(name)}`, "column");
2930
+ const boundary = values ? ` AND ${pageBoundary(order, values, backward, add, column)}` : "";
2931
+ const opposite = values ? `EXISTS (SELECT 1 FROM ${quoteTable(table)} t WHERE true${where} AND ${pageBoundary(order, values, !backward, add, column, true)})` : "false";
2932
+ let prefix = "__pb_page_";
2933
+ const names = [
2934
+ ...Object.keys(schema.columns),
2935
+ ...Object.keys(opts.with ?? {})
2936
+ ];
2937
+ while (names.some((n) => n.startsWith(prefix))) prefix += "_";
2938
+ const ordinal = `${prefix}ordinal`, other = `${prefix}opposite`;
2939
+ const hidden = order.map((o, i) => `${prefix}${i}`);
2940
+ const cursorCols = order.map((o, i) => `${columnTypeOf(table, o.column) === "timestamp" ? timestampResult(column(o.column)) : `${column(o.column)}::text`} AS ${quoteIdent(hidden[i])}`);
2941
+ const w = opts.with && Object.keys(opts.with).length ? withClauses(table, opts.with, add, "page") : {
1692
2942
  select: [],
1693
2943
  joins: []
1694
2944
  };
1695
- const selectList = w.select.length > 0 ? `${cols === "*" ? "t.*" : cols}, ${w.select.join(", ")}` : cols;
1696
- const joinSql = w.joins.length > 0 ? ` ${w.joins.join(" ")}` : "";
1697
- const sql = `SELECT ${selectList} FROM ${quoteTable(table)} t${joinSql} WHERE true${whereSql}${order}${limit}${offset}`;
1698
- return asTableRows(table, await (await at()).unsafe(sql, params));
2945
+ const nativeOrder = order.map((o) => `${column(o.column)} ${o.direction === "asc" !== backward ? "ASC" : "DESC"} NULLS ${o.nulls === "first" !== backward ? "FIRST" : "LAST"}`).join(", ");
2946
+ const visible = cols === "*" ? "t.*" : cols;
2947
+ const inner = `SELECT ${[
2948
+ visible,
2949
+ ...w.select,
2950
+ ...cursorCols,
2951
+ `row_number() OVER (ORDER BY ${nativeOrder}) AS ${quoteIdent(ordinal)}`
2952
+ ].join(", ")} FROM ${quoteTable(table)} t ${w.joins.join(" ")} WHERE true${where}${boundary} ORDER BY ${nativeOrder} LIMIT ${size + 1}`;
2953
+ const sql = `SELECT p.*, b.${quoteIdent(other)} FROM (SELECT ${opposite} AS ${quoteIdent(other)}) b LEFT JOIN LATERAL (${inner}) p ON true ORDER BY p.${quoteIdent(ordinal)}`;
2954
+ const result = await (await at()).unsafe(sql, params);
2955
+ const hasOpposite = result[0]?.[other] === true;
2956
+ let rows = result.filter((row) => row[ordinal] !== null && row[ordinal] !== void 0);
2957
+ const hasMore = rows.length > size;
2958
+ rows = rows.slice(0, size);
2959
+ if (backward) rows.reverse();
2960
+ const cursor = /* @__PURE__ */ __name((row) => row ? encodeCursor(secret, scope, hidden.map((name) => row[name] === null ? null : String(row[name]))) : null, "cursor");
2961
+ const pageInfo = {
2962
+ startCursor: cursor(rows[0]),
2963
+ endCursor: cursor(rows.at(-1)),
2964
+ hasNextPage: backward ? hasOpposite : hasMore,
2965
+ hasPreviousPage: backward ? hasMore : hasOpposite
2966
+ };
2967
+ for (const row of rows) for (const name of [
2968
+ ...hidden,
2969
+ ordinal,
2970
+ other
2971
+ ]) delete row[name];
2972
+ return {
2973
+ rows: asTableRows(table, rows, opts.with, opts.select !== void 0),
2974
+ pageInfo
2975
+ };
1699
2976
  },
1700
2977
  /**
1701
2978
  * Tek-SQL hibrit arama (FR-014): iki kol CTE + FULL OUTER JOIN + RRF
@@ -1800,7 +3077,9 @@ function createOps(tx) {
1800
3077
  kwC = `SELECT c.${pidQ} AS pid, c.chunk_seq, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(c.palbase_fts, websearch_to_tsquery('simple', ${qpC})) DESC) AS r FROM ${quoteTable(ck.table)} c${parentJoinC}c.palbase_fts @@ websearch_to_tsquery('simple', ${qpC}) ORDER BY r LIMIT ${cpool}`;
1801
3078
  }
1802
3079
  const msC = params.minScore === void 0 ? "" : addC(params.minScore);
1803
- const colListC = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
3080
+ const colListC = [
3081
+ ...cfg.cols.map((c) => columnTypeOf(table, c) === "timestamp" ? projectedColumn(table, c, "t") : `t.${quoteIdent(c)}`)
3082
+ ].join(", ");
1804
3083
  const scoreC = scoreMul === "" ? "g._score" : `(g._score)${scoreMul}`;
1805
3084
  const assembleC = /* @__PURE__ */ __name((kwIn, withKwn) => {
1806
3085
  const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
@@ -1813,7 +3092,7 @@ function createOps(tx) {
1813
3092
  fusedSrc = `WITH kw AS (${kwIn}), fused AS (SELECT kw.pid, kw.chunk_seq, (1.0/(${K2} + kw.r))::float8 AS cscore FROM kw)`;
1814
3093
  }
1815
3094
  const inner = `${fusedSrc}, ranked AS (SELECT f.pid, f.chunk_seq, f.cscore, ROW_NUMBER() OVER (PARTITION BY f.pid ORDER BY f.cscore DESC, f.chunk_seq) AS rn FROM fused f), grouped AS (SELECT r.pid, MAX(r.cscore) AS _score, json_agg(json_build_object('content', c.content, 'score', r.cscore, 'chunkSeq', r.chunk_seq, 'charStart', c.char_start) ORDER BY r.cscore DESC, r.chunk_seq) FILTER (WHERE r.rn <= ${bpr}) AS blocks FROM ranked r JOIN ${quoteTable(ck.table)} c ON c.${pidQ} = r.pid AND c.chunk_seq = r.chunk_seq GROUP BY r.pid) SELECT ${colListC}, ${scoreC}::float8 AS _score, g.blocks AS blocks${kwnCol} FROM grouped g JOIN ${quoteTable(table)} t ON t.${quoteIdent(cfg.pk)} = g.pid`;
1816
- return msC === "" ? `SELECT * FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
3095
+ return msC === "" ? `SELECT q.*${canOuter(table)} FROM (${inner}) q ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT q.*${canOuter(table)} FROM (${inner}) q WHERE q._score >= ${msC} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
1817
3096
  }, "assembleC");
1818
3097
  let rowsC = await liveC.unsafe(assembleC(kwC, kwC !== ""), bindC);
1819
3098
  if (wantTextC && kwC !== "" && (rowsC.length === 0 || rowsC[0]._kwn === 0)) {
@@ -1882,26 +3161,26 @@ function createOps(tx) {
1882
3161
  qpRef = qp;
1883
3162
  kwSql = `SELECT t.${quoteIdent(cfg.pk)} AS id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(t.palbase_fts, websearch_to_tsquery('simple', ${qp})) DESC) AS r FROM ${quoteTable(table)} t WHERE t.palbase_fts @@ websearch_to_tsquery('simple', ${qp})${whereSql} ORDER BY r LIMIT ${pool}`;
1884
3163
  }
1885
- const colList = cfg.cols.map((c) => `t.${quoteIdent(c)}`).join(", ");
3164
+ const colList = [
3165
+ ...cfg.cols.map((c) => columnTypeOf(table, c) === "timestamp" ? projectedColumn(table, c, "t") : `t.${quoteIdent(c)}`)
3166
+ ].join(", ");
1886
3167
  const msP = params.minScore === void 0 ? "" : add(params.minScore);
1887
3168
  const hlCol = params.highlight === true && wantText ? `, ts_headline('simple', ${ftsExprOf(cfg.ftsCols)}, websearch_to_tsquery('simple', ${qpRef})) AS _highlight` : "";
1888
3169
  const assemble = /* @__PURE__ */ __name((kwIn, withKwn) => {
1889
3170
  const kwnCol = withKwn ? `, (SELECT count(*) FROM kw)::int AS _kwn` : "";
1890
3171
  let inner;
1891
- let orderScore;
1892
3172
  if (semSql !== "" && kwIn !== "") {
1893
3173
  const score = scoreMul === "" ? "fused._score" : `(fused._score)${scoreMul}`;
1894
3174
  inner = `WITH sem AS (${semSql}), kw AS (${kwIn}), fused AS (SELECT COALESCE(sem.id, kw.id) AS id, (COALESCE(1.0/(${K} + sem.r), 0) + COALESCE(1.0/(${K} + kw.r), 0))::float8 AS _score FROM sem FULL OUTER JOIN kw ON sem.id = kw.id) SELECT ${colList}, ${score} AS _score${hlCol}${kwnCol} FROM fused JOIN ${quoteTable(table)} t ON t.${quoteIdent(cfg.pk)} = fused.id`;
1895
- orderScore = scoreMul === "" ? "fused._score" : "_score";
1896
3175
  } else {
1897
3176
  const single = semSql !== "" ? `sem AS (${semSql})` : `kw AS (${kwIn})`;
1898
3177
  const alias = semSql !== "" ? "sem" : "kw";
1899
3178
  const base = `(1.0/(${K} + ${alias}.r))::float8`;
1900
3179
  const score = scoreMul === "" ? base : `(${base})${scoreMul}`;
1901
3180
  inner = `WITH ${single} SELECT ${colList}, ${score} AS _score${hlCol}${semSql !== "" ? "" : kwnCol} FROM ${alias} JOIN ${quoteTable(table)} t ON t.${quoteIdent(cfg.pk)} = ${alias}.id`;
1902
- orderScore = "_score";
1903
3181
  }
1904
- return msP === "" ? `${inner} ORDER BY ${orderScore} DESC, t.${quoteIdent(cfg.pk)} LIMIT ${limit}` : `SELECT * FROM (${inner}) q WHERE q._score >= ${msP} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
3182
+ const ms = msP === "" ? "" : ` WHERE q._score >= ${msP}`;
3183
+ return `SELECT q.*${canOuter(table)} FROM (${inner}) q${ms} ORDER BY q._score DESC, q.${quoteIdent(cfg.pk)} LIMIT ${limit}`;
1905
3184
  }, "assemble");
1906
3185
  let rows = await live.unsafe(assemble(kwSql, kwSql !== ""), bind);
1907
3186
  if (wantText && kwSql !== "" && (rows.length === 0 || rows[0]._kwn === 0)) {
@@ -2090,7 +3369,7 @@ function createOps(tx) {
2090
3369
  const params = [];
2091
3370
  const add = /* @__PURE__ */ __name((v) => `$${params.push(v)}`, "add");
2092
3371
  const tuple = buildValueTuple(table, cols, data, add, "supersede");
2093
- const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING *`, params);
3372
+ const rows = await sp.unsafe(`INSERT INTO ${quoteTable(table)} (${cols.map(quoteIdent).join(", ")}) VALUES (${tuple}) RETURNING ${rowProjection(table)}`, params);
2094
3373
  if (!rows[0]) {
2095
3374
  throw new Error(`supersede(${table}): INSERT sat\u0131r d\xF6nd\xFCrmedi \u2014 yazma reddedildi (b\xFCy\xFCk olas\u0131l\u0131kla bir RLS policy'si)`);
2096
3375
  }
@@ -2100,7 +3379,7 @@ function createOps(tx) {
2100
3379
  /** A real SAVEPOINT inside the request's transaction. */
2101
3380
  async transaction(cb) {
2102
3381
  const live = await at();
2103
- return live.savepoint(async (sp) => cb(withTables(createOps(sp), currentSchema)));
3382
+ return live.savepoint(async (sp) => cb(withTables(createOps(sp, context), currentSchema)));
2104
3383
  },
2105
3384
  /**
2106
3385
  * Run `fn` against a handle bound to a SAVEPOINT, so a failure inside it
@@ -2121,7 +3400,7 @@ function createOps(tx) {
2121
3400
  */
2122
3401
  async attempt(fn) {
2123
3402
  const live = await at();
2124
- return live.savepoint(async (sp) => fn(withTables(createOps(sp), currentSchema)));
3403
+ return live.savepoint(async (sp) => fn(withTables(createOps(sp, context), currentSchema)));
2125
3404
  },
2126
3405
  /**
2127
3406
  * Execute a whole transaction plan — what `Database.$transaction(fn)` builds.
@@ -2141,6 +3420,62 @@ function createOps(tx) {
2141
3420
  * "transport.txPlan is not a function" while every test that covered
2142
3421
  * transactions passed against a mock that did implement it.
2143
3422
  */
3423
+ async command(plan, options = {}) {
3424
+ if (options.mode !== void 0 && ![
3425
+ "auto",
3426
+ "compiled",
3427
+ "sequential"
3428
+ ].includes(options.mode)) throw new Error("command mode must be auto, compiled or sequential");
3429
+ if (options.mode === "sequential") return ops.txPlan(plan);
3430
+ const compiled = context.commands ? compileCommand(plan) : {
3431
+ reason: "the PostgreSQL command executor is not installed"
3432
+ };
3433
+ if ("reason" in compiled) {
3434
+ if (options.mode === "compiled") throw new Error(`Command cannot compile: ${compiled.reason}. Use auto/sequential mode or installCommandExecutor at boot.`);
3435
+ return ops.txPlan(plan);
3436
+ }
3437
+ if (compiled.steps.length === 0) return {
3438
+ results: []
3439
+ };
3440
+ if (compiled.pipeline) {
3441
+ return (await at()).savepoint(async (sp) => {
3442
+ const settled = await Promise.allSettled(compiled.pipeline.map((statement) => sp.unsafe(statement.sql, statement.params)));
3443
+ const failed = settled.find((result) => result.status === "rejected");
3444
+ if (failed?.status === "rejected") {
3445
+ const error = failed.reason;
3446
+ if (sqlstateOf(error) === SQLSTATE.commandGuardFailed && typeof error.detail === "string") {
3447
+ const detail = JSON.parse(error.detail);
3448
+ if (detail.error_code === "tx_guard_failed" && Number.isInteger(detail.slot)) {
3449
+ throw Object.assign(new Error("Command expectation failed"), detail);
3450
+ }
3451
+ }
3452
+ throw error;
3453
+ }
3454
+ return {
3455
+ results: settled.map((result, i) => {
3456
+ const rows2 = result.value[0].rows;
3457
+ return {
3458
+ rows: asTableRows(plan.ops[i].table, rows2),
3459
+ rows_affected: rows2.length
3460
+ };
3461
+ })
3462
+ };
3463
+ });
3464
+ }
3465
+ const rows = await (await at()).savepoint((sp) => sp.unsafe("SELECT palbase_runtime.command_v1($1::jsonb -> 'steps') AS result", [
3466
+ {
3467
+ steps: compiled.steps
3468
+ }
3469
+ ]));
3470
+ const response = rows[0].result;
3471
+ if (response.error) throw Object.assign(new Error(response.error.error_code === "tx_ref_unresolved" ? "Command reference has no source row/column; use expectOne before reading it" : "Command expectation failed"), response.error);
3472
+ return {
3473
+ results: response.results.map((result, i) => ({
3474
+ ...result,
3475
+ rows: asTableRows(plan.ops[i].table, result.rows)
3476
+ }))
3477
+ };
3478
+ },
2144
3479
  async txPlan(plan) {
2145
3480
  const live = await at();
2146
3481
  return live.savepoint(async (sp) => {
@@ -2164,27 +3499,55 @@ function createOps(tx) {
2164
3499
  }
2165
3500
  __name(createOps, "createOps");
2166
3501
  var currentSchema = {};
3502
+ var columnsMemo = /* @__PURE__ */ new WeakMap();
3503
+ var primaryMemo = /* @__PURE__ */ new WeakMap();
3504
+ var vectorMemo = /* @__PURE__ */ new WeakMap();
3505
+ var transformMemo = /* @__PURE__ */ new WeakMap();
3506
+ var typeMemo = /* @__PURE__ */ new WeakMap();
3507
+ var timestampMemo = /* @__PURE__ */ new WeakMap();
3508
+ var projectionMemo = /* @__PURE__ */ new WeakMap();
3509
+ function schemaColumns(table) {
3510
+ return memoTable(columnsMemo, currentSchema, table, () => computeSchemaColumns(table));
3511
+ }
3512
+ __name(schemaColumns, "schemaColumns");
3513
+ function primaryKeyOf(table) {
3514
+ return memoTable(primaryMemo, currentSchema, table, () => computePrimaryKey(table));
3515
+ }
3516
+ __name(primaryKeyOf, "primaryKeyOf");
3517
+ function vectorColumnsOf(schema, table) {
3518
+ return memoTable(vectorMemo, schema, table, () => computeVectorColumns(schema, table));
3519
+ }
3520
+ __name(vectorColumnsOf, "vectorColumnsOf");
3521
+ function transformsOf(schema, table) {
3522
+ return memoTable(transformMemo, schema, table, () => computeTransforms(schema, table));
3523
+ }
3524
+ __name(transformsOf, "transformsOf");
3525
+ function columnTypeOf(table, col) {
3526
+ const types = memoTable(typeMemo, currentSchema, table, () => new Map([
3527
+ ...schemaColumns(table) ?? []
3528
+ ].map((c) => [
3529
+ c,
3530
+ computeColumnType(table, c)
3531
+ ])));
3532
+ return types.get(col) ?? null;
3533
+ }
3534
+ __name(columnTypeOf, "columnTypeOf");
2167
3535
  var currentRelations = /* @__PURE__ */ new Map();
2168
3536
  function setSchema(schemas) {
2169
- cachedVectorSchema = null;
2170
- cachedTrgmSchema = null;
2171
- const tables = {};
3537
+ const tables = /* @__PURE__ */ Object.create(null);
2172
3538
  for (const entry of schemas) {
2173
3539
  const mod = entry;
2174
3540
  const def = (mod && "default" in mod ? mod.default : mod) ?? {};
2175
3541
  const schemaName = def.name ?? "public";
2176
- for (const [key, table] of Object.entries(def.tables ?? {})) {
2177
- tables[qualifiedTableKey(schemaName, table.name ?? key)] = table;
3542
+ for (const [key2, table] of Object.entries(def.tables ?? {})) {
3543
+ tables[qualifiedTableKey(schemaName, table.name ?? key2)] = table;
2178
3544
  }
2179
3545
  }
2180
- currentSchema = {
2181
- tables
2182
- };
2183
- currentRelations = buildRelations(schemas.map((entry) => {
3546
+ const relations = buildRelations(schemas.map((entry) => {
2184
3547
  const mod = entry;
2185
3548
  const def = (mod && "default" in mod ? mod.default : mod) ?? {};
2186
- const tables2 = {};
2187
- for (const [key, t] of Object.entries(def.tables ?? {})) {
3549
+ const tables2 = /* @__PURE__ */ Object.create(null);
3550
+ for (const [key2, t] of Object.entries(def.tables ?? {})) {
2188
3551
  const table = t ?? {};
2189
3552
  const columns = {};
2190
3553
  for (const [cname, c] of Object.entries(table.columns ?? {})) {
@@ -2194,9 +3557,9 @@ function setSchema(schemas) {
2194
3557
  _def: cd
2195
3558
  };
2196
3559
  }
2197
- tables2[key] = {
3560
+ tables2[key2] = {
2198
3561
  ...table,
2199
- name: table.name ?? key,
3562
+ name: table.name ?? key2,
2200
3563
  columns
2201
3564
  };
2202
3565
  }
@@ -2205,40 +3568,86 @@ function setSchema(schemas) {
2205
3568
  tables: tables2
2206
3569
  };
2207
3570
  }));
3571
+ for (const key2 of Object.keys(tables)) tables[key2] = snapshotDeclaration(tables[key2]);
3572
+ currentSchema = Object.freeze({
3573
+ tables: Object.freeze(tables)
3574
+ });
3575
+ currentRelations = relations;
3576
+ cachedVectorSchema = null;
3577
+ cachedTrgmSchema = null;
2208
3578
  }
2209
3579
  __name(setSchema, "setSchema");
3580
+ var tableNamespaces = /* @__PURE__ */ new WeakMap();
2210
3581
  function withTables(ops, schema = currentSchema) {
2211
- const bind = /* @__PURE__ */ __name((key) => ({
2212
- insert: /* @__PURE__ */ __name((data) => ops.insert(key, data), "insert"),
2213
- update: /* @__PURE__ */ __name((id, data) => ops.update(key, id, data), "update"),
2214
- delete: /* @__PURE__ */ __name((id) => ops.delete(key, id), "delete"),
2215
- findById: /* @__PURE__ */ __name((id) => ops.findById(key, id), "findById"),
2216
- findMany: /* @__PURE__ */ __name((query, opts) => ops.findMany(key, query ?? {}, opts), "findMany"),
2217
- put: /* @__PURE__ */ __name((q) => ops.put(key, q.data, {
3582
+ let namespaces = tableNamespaces.get(schema);
3583
+ if (!namespaces) {
3584
+ namespaces = /* @__PURE__ */ new Map();
3585
+ for (const key2 of Object.keys(schema.tables ?? {})) {
3586
+ const dot = key2.indexOf(".");
3587
+ const name = dot === -1 ? "public" : key2.slice(0, dot);
3588
+ const short = dot === -1 ? key2 : key2.slice(dot + 1);
3589
+ if (!namespaces.has(name)) namespaces.set(name, /* @__PURE__ */ new Map());
3590
+ namespaces.get(name).set(short, key2);
3591
+ }
3592
+ if (Object.isFrozen(schema) && Object.isFrozen(schema.tables)) tableNamespaces.set(schema, namespaces);
3593
+ }
3594
+ const bind = /* @__PURE__ */ __name((key2) => ({
3595
+ insert: /* @__PURE__ */ __name((data) => ops.insert(key2, data), "insert"),
3596
+ update: /* @__PURE__ */ __name((id, data) => ops.update(key2, id, data), "update"),
3597
+ delete: /* @__PURE__ */ __name((id) => ops.delete(key2, id), "delete"),
3598
+ findById: /* @__PURE__ */ __name((id) => ops.findById(key2, id), "findById"),
3599
+ findUnique: /* @__PURE__ */ __name((where, opts) => ops.findUnique(key2, where, opts), "findUnique"),
3600
+ findMany: /* @__PURE__ */ __name((query, opts) => ops.findMany(key2, query ?? {}, opts), "findMany"),
3601
+ page: /* @__PURE__ */ __name((query, opts) => ops.page(key2, query ?? {}, opts), "page"),
3602
+ put: /* @__PURE__ */ __name((q) => ops.put(key2, q.data, {
2218
3603
  onConflict: q.onConflict
2219
3604
  }), "put")
2220
3605
  }), "bind");
2221
- const tables = {};
2222
- for (const key of Object.keys(schema.tables ?? {})) {
2223
- if (!key.includes(".")) tables[key] = bind(key);
2224
- }
3606
+ const surfaces = /* @__PURE__ */ new Map();
2225
3607
  const schemaOf = /* @__PURE__ */ __name((name) => {
2226
- const out = {};
2227
- const prefix = `${name}.`;
2228
- for (const key of Object.keys(schema.tables ?? {})) {
2229
- if (name === "public") {
2230
- if (!key.includes(".")) out[key] = bind(key);
2231
- } else if (key.startsWith(prefix)) {
2232
- out[key.slice(prefix.length)] = bind(key);
3608
+ const cached = surfaces.get(name);
3609
+ if (cached) return cached;
3610
+ const names = namespaces.get(name) ?? /* @__PURE__ */ new Map();
3611
+ const target = /* @__PURE__ */ Object.create(null);
3612
+ const get = /* @__PURE__ */ __name((key2) => {
3613
+ if (!Object.hasOwn(target, key2) && names.has(key2)) {
3614
+ Object.defineProperty(target, key2, {
3615
+ value: bind(names.get(key2)),
3616
+ configurable: true,
3617
+ enumerable: true,
3618
+ writable: true
3619
+ });
2233
3620
  }
2234
- }
2235
- return {
2236
- tables: out
3621
+ return Reflect.get(target, key2);
3622
+ }, "get");
3623
+ const tables = new Proxy(target, {
3624
+ get: /* @__PURE__ */ __name((t, key2, receiver) => typeof key2 === "string" ? get(key2) : Reflect.get(t, key2, receiver), "get"),
3625
+ has: /* @__PURE__ */ __name((t, key2) => typeof key2 === "string" && names.has(key2) || Reflect.has(t, key2), "has"),
3626
+ ownKeys: /* @__PURE__ */ __name((t) => [
3627
+ .../* @__PURE__ */ new Set([
3628
+ ...names.keys(),
3629
+ ...Reflect.ownKeys(t)
3630
+ ])
3631
+ ], "ownKeys"),
3632
+ getOwnPropertyDescriptor: /* @__PURE__ */ __name((t, key2) => Reflect.getOwnPropertyDescriptor(t, key2) ?? (typeof key2 === "string" && names.has(key2) ? {
3633
+ configurable: true,
3634
+ enumerable: true,
3635
+ get: /* @__PURE__ */ __name(() => get(key2), "get")
3636
+ } : void 0), "getOwnPropertyDescriptor"),
3637
+ preventExtensions: /* @__PURE__ */ __name((t) => {
3638
+ for (const key2 of names.keys()) get(key2);
3639
+ return Reflect.preventExtensions(t);
3640
+ }, "preventExtensions")
3641
+ });
3642
+ const surface = {
3643
+ tables
2237
3644
  };
3645
+ if (namespaces.has(name) || name === "public") surfaces.set(name, surface);
3646
+ return surface;
2238
3647
  }, "schemaOf");
2239
3648
  const base = /* @__PURE__ */ Object.create(null);
2240
3649
  return Object.assign(base, ops, {
2241
- tables,
3650
+ tables: schemaOf("public").tables,
2242
3651
  schema: schemaOf
2243
3652
  });
2244
3653
  }
@@ -2296,8 +3705,8 @@ function diagnosingDriver(sql, surface = "service") {
2296
3705
  return markEngineRaised(new UniqueViolation(constraintOf(e)));
2297
3706
  }
2298
3707
  const state = sqlstateOf(e);
2299
- if (state === "40001") return markEngineRaised(new SerializationFailure(state));
2300
- if (state === "40P01") return markEngineRaised(new DeadlockDetected(state));
3708
+ if (state === SQLSTATE.serializationFailure) return markEngineRaised(new SerializationFailure(state));
3709
+ if (state === SQLSTATE.deadlockDetected) return markEngineRaised(new DeadlockDetected(state));
2301
3710
  if (surface === "service" && isForeignKeyViolation(e)) {
2302
3711
  return new Error(`Database.$asService() hit a foreign key with nothing to point at. It runs in its OWN transaction on its OWN connection, so a row this request wrote through \`Database.*\` is not visible to it until the request commits \u2014 and the request cannot commit until the handler returns. If the row it needs is one this request just created, do both writes on ONE surface. (${original(e)})`);
2303
3712
  }
@@ -2307,11 +3716,16 @@ function diagnosingDriver(sql, surface = "service") {
2307
3716
  return e;
2308
3717
  }, "explain");
2309
3718
  const wrapTx = /* @__PURE__ */ __name((tx) => ({
2310
- async unsafe(text, params) {
3719
+ unsafe(text, params) {
2311
3720
  try {
2312
- return await tx.unsafe(text, params);
3721
+ const query = tx.unsafe(text, params);
3722
+ const mapped = query.catch((e) => {
3723
+ throw explain(e);
3724
+ });
3725
+ if (query.cancel) mapped.cancel = () => query.cancel();
3726
+ return mapped;
2313
3727
  } catch (e) {
2314
- throw explain(e);
3728
+ return Promise.reject(explain(e));
2315
3729
  }
2316
3730
  },
2317
3731
  savepoint(cb) {
@@ -2320,34 +3734,120 @@ function diagnosingDriver(sql, surface = "service") {
2320
3734
  }), "wrapTx");
2321
3735
  return {
2322
3736
  unsafe: /* @__PURE__ */ __name((text, params) => sql.unsafe(text, params), "unsafe"),
2323
- begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))), "begin")
3737
+ cancelBackend: sql.cancelBackend?.bind(sql),
3738
+ begin: /* @__PURE__ */ __name((cb) => sql.begin((tx) => cb(wrapTx(tx))).catch((e) => {
3739
+ throw explain(e);
3740
+ }), "begin")
2324
3741
  };
2325
3742
  }
2326
3743
  __name(diagnosingDriver, "diagnosingDriver");
2327
- function createRequestDatabase(sql, identity) {
2328
- const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson);
3744
+ function createRequestDatabase(sql, identity, diagnostics) {
3745
+ const trace = diagnostics === void 0 ? void 0 : new DatabaseTrace(diagnostics);
3746
+ let requestClosed = false;
3747
+ const contextFor = /* @__PURE__ */ __name((role) => {
3748
+ let scope;
3749
+ return {
3750
+ trace,
3751
+ get cursorScope() {
3752
+ return scope ??= cursorIdentity(role, identity.claimsJson);
3753
+ },
3754
+ get commands() {
3755
+ return hasCommandExecutor(sql);
3756
+ }
3757
+ };
3758
+ }, "contextFor");
3759
+ const userContext = contextFor(identity.role);
3760
+ const serviceContext = contextFor(identity.serviceRole);
3761
+ const atomic = /* @__PURE__ */ __name((role, surface) => async (fn, options = {}) => {
3762
+ validateAtomicOptions(options);
3763
+ if (requestClosed) throw new Error("Database request has settled; it cannot open another transaction");
3764
+ if (atomicScope.getStore() !== void 0) throw new Error("Nested Database.$atomic is not supported; use tx.$attempt for a savepoint");
3765
+ const retries = retryBudget(options.retry);
3766
+ const control = options.timeoutMs !== void 0 || options.signal !== void 0 ? new TransactionControl(options.timeoutMs, options.signal) : void 0;
3767
+ let lease;
3768
+ try {
3769
+ control?.check();
3770
+ if (options.workload) {
3771
+ const queuedAt = trace ? performance.now() : 0;
3772
+ try {
3773
+ lease = await workloadAdmission(sql, options.workload).acquire(workloadTenant(options.workload, identity.claimsJson), options.conflictKeys ?? [], control?.signal);
3774
+ } finally {
3775
+ trace?.time("queueMs", queuedAt);
3776
+ }
3777
+ }
3778
+ for (let attempt = 0; ; attempt++) {
3779
+ control?.check();
3780
+ const timeouts = [
3781
+ options.statementTimeoutMs,
3782
+ control?.remainingMs()
3783
+ ].filter((n) => n !== void 0);
3784
+ const scope = /* @__PURE__ */ Symbol("atomic transaction");
3785
+ const root = createLazyTransaction(diagnosingDriver(sql, surface), role, identity.claimsJson, {
3786
+ scope,
3787
+ isolation: options.isolation,
3788
+ readOnly: options.readOnly,
3789
+ control,
3790
+ trace,
3791
+ lockTimeout: `${options.lockTimeoutMs ?? 5e3}ms`,
3792
+ statementTimeout: timeouts.length === 0 ? void 0 : `${Math.min(...timeouts)}ms`
3793
+ });
3794
+ try {
3795
+ const callback = atomicScope.run(scope, () => fn(withTables(createOps(root, surface === "user" ? userContext : serviceContext))));
3796
+ const value = await (control ? control.race(callback) : callback);
3797
+ control?.check();
3798
+ await root.commit();
3799
+ return value;
3800
+ } catch (error) {
3801
+ await root.rollback(error);
3802
+ if (attempt >= retries || !isRetryable(error)) throw error;
3803
+ trace?.retry();
3804
+ const delay = new Promise((resolve) => setTimeout(resolve, Math.min(100, 2 ** attempt)));
3805
+ await (control ? control.race(delay) : delay);
3806
+ }
3807
+ }
3808
+ } finally {
3809
+ control?.dispose();
3810
+ lease?.release();
3811
+ }
3812
+ }, "atomic");
3813
+ const tx = createLazyTransaction(diagnosingDriver(sql, "user"), identity.role, identity.claimsJson, {
3814
+ trace
3815
+ });
2329
3816
  let serviceTx = null;
2330
3817
  let serviceClient = null;
2331
3818
  const asService = /* @__PURE__ */ __name(() => {
2332
3819
  if (serviceClient === null) {
2333
3820
  serviceTx = createLazyTransaction(diagnosingDriver(sql, "service"), identity.serviceRole, identity.claimsJson, {
2334
- lockTimeout: SERVICE_LOCK_TIMEOUT
3821
+ lockTimeout: SERVICE_LOCK_TIMEOUT,
3822
+ trace
3823
+ });
3824
+ serviceClient = Object.assign(withTables(createOps(serviceTx, serviceContext)), {
3825
+ atomic: atomic(identity.serviceRole, "service")
2335
3826
  });
2336
- serviceClient = withTables(createOps(serviceTx));
2337
3827
  }
2338
3828
  return serviceClient;
2339
3829
  }, "asService");
2340
3830
  return {
2341
- client: Object.assign(withTables(createOps(tx)), {
2342
- asService
3831
+ client: Object.assign(withTables(createOps(tx, userContext)), {
3832
+ asService,
3833
+ atomic: atomic(identity.role, "user")
2343
3834
  }),
2344
3835
  async commit() {
2345
- await tx.commit();
2346
- await serviceTx?.commit();
3836
+ requestClosed = true;
3837
+ try {
3838
+ await tx.commit();
3839
+ await serviceTx?.commit();
3840
+ trace?.complete("committed");
3841
+ } catch (error) {
3842
+ trace?.failed();
3843
+ throw error;
3844
+ }
2347
3845
  },
2348
3846
  async rollback(reason) {
3847
+ requestClosed = true;
2349
3848
  await tx.rollback(reason);
2350
3849
  await serviceTx?.rollback(reason);
3850
+ trace?.complete("failed");
2351
3851
  }
2352
3852
  };
2353
3853
  }
@@ -2357,8 +3857,13 @@ var Args = class Args2 {
2357
3857
  __name(this, "Args");
2358
3858
  }
2359
3859
  values = [];
2360
- bind(value) {
3860
+ columns = [];
3861
+ bind(value, column, table) {
2361
3862
  this.values.push(value);
3863
+ this.columns.push({
3864
+ column,
3865
+ table
3866
+ });
2362
3867
  return `$${this.values.length}`;
2363
3868
  }
2364
3869
  };
@@ -2444,7 +3949,7 @@ function planWhere(table, where, args, results) {
2444
3949
  }
2445
3950
  const resolved = resolveRefsDeep(where, results);
2446
3951
  const known = schemaColumns(table);
2447
- const bare = compileWhereBare(table, known, resolved, (v) => args.bind(v), "transaction plan");
3952
+ const bare = compileWhereBare(table, known, resolved, (v, column, from) => args.bind(v, column, from), "transaction plan");
2448
3953
  return {
2449
3954
  sql: bare === "" ? "" : ` AND ${bare}`,
2450
3955
  resolved
@@ -2453,16 +3958,16 @@ function planWhere(table, where, args, results) {
2453
3958
  __name(planWhere, "planWhere");
2454
3959
  function bindMaybeVector(args, column, vectorCols, value, transforms) {
2455
3960
  if (column !== void 0 && vectorCols?.has(column) && Array.isArray(value)) {
2456
- return args.bind(toVectorLiteral(value));
3961
+ return args.bind(toVectorLiteral(value), column);
2457
3962
  }
2458
3963
  const t = column !== void 0 ? transforms?.get(column) : void 0;
2459
3964
  if (t?.toDb !== void 0 && value !== null && value !== void 0) {
2460
- return args.bind(t.toDb(value));
3965
+ return args.bind(t.toDb(value), column);
2461
3966
  }
2462
- return args.bind(value);
3967
+ return args.bind(value, column);
2463
3968
  }
2464
3969
  __name(bindMaybeVector, "bindMaybeVector");
2465
- async function runPlanOp(sp, op, results) {
3970
+ function compilePlanOp(op, results) {
2466
3971
  const opVectorCols = vectorColumnsOf(currentSchema, op.table);
2467
3972
  const opTransforms = transformsOf(currentSchema, op.table);
2468
3973
  const args = new Args();
@@ -2472,7 +3977,7 @@ async function runPlanOp(sp, op, results) {
2472
3977
  case "insert": {
2473
3978
  const cols = Object.keys(op.values ?? {});
2474
3979
  const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols, opTransforms));
2475
- sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING *` : `INSERT INTO ${table} DEFAULT VALUES RETURNING *`;
3980
+ sql = cols.length ? `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) RETURNING ${rowProjection(op.table)}` : `INSERT INTO ${table} DEFAULT VALUES RETURNING ${rowProjection(op.table)}`;
2476
3981
  break;
2477
3982
  }
2478
3983
  case "upsert": {
@@ -2485,17 +3990,17 @@ async function runPlanOp(sp, op, results) {
2485
3990
  });
2486
3991
  }
2487
3992
  const rendered = cols.map((c) => renderValue(op.values[c], c, args, results, opVectorCols, opTransforms));
2488
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING *`;
3993
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES (${rendered.join(", ")}) ${onConflictTail(cols, conflict, "update")} RETURNING ${rowProjection(op.table)}`;
2489
3994
  break;
2490
3995
  }
2491
3996
  case "insertMany": {
2492
3997
  const rows = op.rows ?? [];
2493
- if (rows.length === 0 || !rows[0]) return [];
3998
+ if (rows.length === 0 || !rows[0]) return null;
2494
3999
  const cols = Object.keys(rows[0]);
2495
4000
  const tuples = rows.map((r) => `(${cols.map((c) => renderValue(r[c], c, args, results, opVectorCols, opTransforms)).join(", ")})`);
2496
4001
  const conflictCols = op.onConflict ?? [];
2497
4002
  const tail = op.action !== void 0 && conflictCols.length > 0 ? ` ${onConflictTail(cols, conflictCols, op.action)}` : "";
2498
- sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING *`;
4003
+ sql = `INSERT INTO ${table} (${cols.map(quoteIdent).join(", ")}) VALUES ${tuples.join(", ")}${tail} RETURNING ${rowProjection(op.table)}`;
2499
4004
  break;
2500
4005
  }
2501
4006
  case "update": {
@@ -2511,12 +4016,12 @@ async function runPlanOp(sp, op, results) {
2511
4016
  const assignments = cols.map((c) => `${quoteIdent(c)} = ${renderValue(op.set[c], c, args, results, opVectorCols, opTransforms)}`);
2512
4017
  const planned = planWhere(op.table, op.where, args, results);
2513
4018
  const planPk = primaryKeyOf(op.table);
2514
- sql = isSinglePkEquality(planned.resolved, planPk) || planPk === null ? `UPDATE ${table} AS t SET ${assignments.join(", ")} WHERE true${planned.sql} RETURNING t.*` : `WITH locked AS (SELECT t.${quoteIdent(planPk)} FROM ${table} t WHERE true${planned.sql} ORDER BY t.${quoteIdent(planPk)} FOR NO KEY UPDATE) UPDATE ${table} AS t SET ${assignments.join(", ")} FROM locked WHERE t.${quoteIdent(planPk)} = locked.${quoteIdent(planPk)} RETURNING t.*`;
4019
+ sql = isSinglePkEquality(planned.resolved, planPk) || planPk === null ? `UPDATE ${table} AS t SET ${assignments.join(", ")} WHERE true${planned.sql} RETURNING ${rowProjection(op.table, "t")}` : `WITH locked AS (SELECT t.${quoteIdent(planPk)} AS "__pb_lock" FROM ${table} t WHERE true${planned.sql} ORDER BY t.${quoteIdent(planPk)} FOR NO KEY UPDATE) UPDATE ${table} AS t SET ${assignments.join(", ")} FROM locked WHERE t.${quoteIdent(planPk)} = locked."__pb_lock" RETURNING ${rowProjection(op.table, "t")}`;
2515
4020
  break;
2516
4021
  }
2517
4022
  case "delete": {
2518
4023
  assertNotAppendOnly("deleteWhere", op.table);
2519
- sql = `DELETE FROM ${table} AS t WHERE true${renderWhere(op.table, op.where, args, results)} RETURNING t.*`;
4024
+ sql = `DELETE FROM ${table} AS t WHERE true${renderWhere(op.table, op.where, args, results)} RETURNING ${rowProjection(op.table, "t")}`;
2520
4025
  break;
2521
4026
  }
2522
4027
  case "select": {
@@ -2529,15 +4034,157 @@ async function runPlanOp(sp, op, results) {
2529
4034
  limit = ` LIMIT ${n}`;
2530
4035
  }
2531
4036
  const lock = op.lock === "update" ? " FOR UPDATE" : "";
2532
- sql = `SELECT t.* FROM ${table} t WHERE true${renderWhere(op.table, op.where, args, results)}${limit}${lock}`;
4037
+ sql = `SELECT ${rowProjection(op.table, "t")} FROM ${table} t WHERE true${renderWhere(op.table, op.where, args, results)}${limit}${lock}`;
2533
4038
  break;
2534
4039
  }
2535
4040
  default:
2536
4041
  throw new Error(`unknown operation "${String(op.op)}" in a transaction plan`);
2537
4042
  }
2538
- return await sp.unsafe(sql, args.values);
4043
+ return {
4044
+ sql,
4045
+ params: args.values,
4046
+ columns: args.columns
4047
+ };
4048
+ }
4049
+ __name(compilePlanOp, "compilePlanOp");
4050
+ async function runPlanOp(sp, op, results) {
4051
+ const statement = compilePlanOp(op, results);
4052
+ return statement ? await sp.unsafe(statement.sql, statement.params) : [];
2539
4053
  }
2540
4054
  __name(runPlanOp, "runPlanOp");
4055
+ function compileCommand(plan) {
4056
+ const symbols = /* @__PURE__ */ new Map();
4057
+ const results = [];
4058
+ const steps = [];
4059
+ const pipeline = [];
4060
+ let hasRefs = false;
4061
+ const prefix = `command:${crypto.randomUUID()}:`;
4062
+ const containsFragment = /* @__PURE__ */ __name((v) => isSqlFragment(v) || v !== null && typeof v === "object" && Object.values(v).some(containsFragment), "containsFragment");
4063
+ const hasNullableFilterRef = /* @__PURE__ */ __name((v) => {
4064
+ if (isRef(v)) {
4065
+ const source = plan.ops[v.$ref.op];
4066
+ return source !== void 0 && pageSchema(source.table).columns[v.$ref.field]?.nullable !== false;
4067
+ }
4068
+ return v !== null && typeof v === "object" && Object.values(v).some(hasNullableFilterRef);
4069
+ }, "hasNullableFilterRef");
4070
+ for (const op of plan.ops) {
4071
+ if (!schemaColumns(op.table)?.size) return {
4072
+ reason: `table ${op.table} has no installed declaration`
4073
+ };
4074
+ if (transformsOf(currentSchema, op.table).size || vectorColumnsOf(currentSchema, op.table).size) return {
4075
+ reason: `${op.table} uses JavaScript codecs/transforms or vector values`
4076
+ };
4077
+ if (containsFragment(op)) return {
4078
+ reason: "SQL fragments require the sequential path"
4079
+ };
4080
+ if (hasNullableFilterRef(op.where)) return {
4081
+ reason: "nullable references in filters require the sequential path"
4082
+ };
4083
+ }
4084
+ for (const [index, op] of plan.ops.entries()) {
4085
+ const statement = compilePlanOp(op, results);
4086
+ if (statement === null) {
4087
+ steps.push({
4088
+ empty: true,
4089
+ guard: op.guard
4090
+ });
4091
+ pipeline.push({
4092
+ sql: "SELECT palbase_runtime.command_rows_v1('[]'::jsonb,$1::jsonb) AS rows",
4093
+ params: [
4094
+ op.guard ?? null
4095
+ ]
4096
+ });
4097
+ } else {
4098
+ if (statement.params.some((v) => v === void 0 || typeof v === "symbol" || typeof v === "function" || typeof v === "number" && (!Number.isInteger(v) || v < -2147483648 || v > 2147483647))) {
4099
+ return {
4100
+ reason: "non-integer/wide numeric or non-JSON parameters require the sequential driver path; use exact decimal strings"
4101
+ };
4102
+ }
4103
+ for (const [i, value] of statement.params.entries()) {
4104
+ const target = statement.columns[i];
4105
+ if (target.column === void 0 || columnTypeOf(target.table ?? op.table, target.column) !== "jsonb") continue;
4106
+ const source = typeof value === "string" ? symbols.get(value) : void 0;
4107
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || source && "ref" in source && ![
4108
+ "text",
4109
+ "uuid",
4110
+ "timestamp",
4111
+ "enum"
4112
+ ].includes(columnTypeOf(plan.ops[source.ref.op].table, source.ref.field) ?? "")) {
4113
+ return {
4114
+ reason: "JSONB scalar/ref binding requires the sequential driver path"
4115
+ };
4116
+ }
4117
+ }
4118
+ const wide = [
4119
+ ...schemaColumns(op.table)
4120
+ ].filter((c) => [
4121
+ "bigint",
4122
+ "numeric"
4123
+ ].includes(columnTypeOf(op.table, c) ?? ""));
4124
+ const exact = wide.length ? ` || jsonb_build_object(${wide.map((c) => `${quoteLiteral(c)}, r.${quoteIdent(c)}::text`).join(", ")})` : "";
4125
+ const sql = `WITH command_rows AS (${statement.sql}) SELECT coalesce(jsonb_agg(to_jsonb(r)${exact}), '[]'::jsonb) FROM command_rows r`;
4126
+ pipeline.push({
4127
+ sql: `WITH command_rows AS (${statement.sql}) SELECT palbase_runtime.command_rows_v1(coalesce(jsonb_agg(to_jsonb(r)${exact}), '[]'::jsonb),$${statement.params.length + 1}::jsonb) AS rows FROM command_rows r`,
4128
+ params: [
4129
+ ...statement.params,
4130
+ op.guard ?? null
4131
+ ]
4132
+ });
4133
+ const bindings = statement.params.map((value, i) => {
4134
+ const target = statement.columns[i];
4135
+ const json = target.column !== void 0 && columnTypeOf(target.table ?? op.table, target.column) === "jsonb";
4136
+ if (typeof value === "string" && symbols.has(value)) {
4137
+ hasRefs = true;
4138
+ return {
4139
+ ...symbols.get(value),
4140
+ json
4141
+ };
4142
+ }
4143
+ if (typeof value === "bigint") return {
4144
+ value: value.toString(),
4145
+ json
4146
+ };
4147
+ if (value instanceof Date) return {
4148
+ value: value.toISOString(),
4149
+ json
4150
+ };
4151
+ return {
4152
+ value,
4153
+ json
4154
+ };
4155
+ });
4156
+ steps.push({
4157
+ chunks: parameterChunks(sql),
4158
+ bindings,
4159
+ guard: op.guard
4160
+ });
4161
+ }
4162
+ const row = {};
4163
+ for (const field of schemaColumns(op.table)) {
4164
+ const token = `${prefix}${index}:${field}`;
4165
+ symbols.set(token, {
4166
+ ref: {
4167
+ op: index,
4168
+ field
4169
+ }
4170
+ });
4171
+ row[field] = token;
4172
+ }
4173
+ results.push({
4174
+ rows: [
4175
+ row
4176
+ ],
4177
+ rows_affected: 1
4178
+ });
4179
+ }
4180
+ return {
4181
+ steps,
4182
+ ...hasRefs ? {} : {
4183
+ pipeline
4184
+ }
4185
+ };
4186
+ }
4187
+ __name(compileCommand, "compileCommand");
2541
4188
  function assertGuard(op, result) {
2542
4189
  const guard = op.guard;
2543
4190
  if (!guard) return;
@@ -2551,6 +4198,33 @@ function assertGuard(op, result) {
2551
4198
  }
2552
4199
  __name(assertGuard, "assertGuard");
2553
4200
 
4201
+ // src/engine/cancellation.ts
4202
+ function withQueryCancellation(application, control) {
4203
+ if (application === control) throw new Error("Query cancellation requires a separate control pool");
4204
+ const close = /* @__PURE__ */ __name(async (driver) => {
4205
+ const d = driver;
4206
+ if (d.close) await d.close();
4207
+ else if (d.end) await d.end();
4208
+ }, "close");
4209
+ return {
4210
+ unsafe: /* @__PURE__ */ __name((sql, params) => application.unsafe(sql, params), "unsafe"),
4211
+ begin: /* @__PURE__ */ __name((fn) => application.begin(fn), "begin"),
4212
+ async cancelBackend(pid, startedAt) {
4213
+ await control.unsafe(`SELECT pg_cancel_backend(pid) FROM pg_stat_activity
4214
+ WHERE pid=$1::integer AND backend_start=$2::timestamptz
4215
+ AND usename=session_user AND datname=current_database() AND backend_type='client backend'`, [
4216
+ pid,
4217
+ startedAt
4218
+ ]);
4219
+ },
4220
+ async close() {
4221
+ await close(application);
4222
+ await close(control);
4223
+ }
4224
+ };
4225
+ }
4226
+ __name(withQueryCancellation, "withQueryCancellation");
4227
+
2554
4228
  // src/engine/router.ts
2555
4229
  var ROOM_META = /* @__PURE__ */ Symbol.for("palbase.backend.room");
2556
4230
  var CONTROLLER_META = /* @__PURE__ */ Symbol.for("palbase.backend.controllerMeta");
@@ -3055,10 +4729,13 @@ async function defaultSqlDriver(config) {
3055
4729
  if (!g.Bun?.SQL) {
3056
4730
  throw new BootRefused([], "boot refused: no SQL driver. Running outside Bun means the driver must be supplied \u2014 pass `sql` to createApp().");
3057
4731
  }
3058
- return new g.Bun.SQL({
4732
+ return withQueryCancellation(new g.Bun.SQL({
3059
4733
  url: config.databaseUrl,
3060
4734
  max: config.poolMax
3061
- });
4735
+ }), new g.Bun.SQL({
4736
+ url: config.databaseUrl,
4737
+ max: 2
4738
+ }));
3062
4739
  }
3063
4740
  __name(defaultSqlDriver, "defaultSqlDriver");
3064
4741
  var AUTHOR_ANSWERED = /* @__PURE__ */ new Set([
@@ -3075,13 +4752,25 @@ async function refuseUnlessAuthorized(client, spec, requestId, rolesOfCaller) {
3075
4752
  spec.permission
3076
4753
  ]);
3077
4754
  if (row?.ok !== true) {
3078
- return envelope("forbidden", `This endpoint requires the "${spec.permission}" permission`, 403, requestId);
4755
+ return envelope(
4756
+ "forbidden",
4757
+ `This endpoint requires the "${spec.permission}" permission`,
4758
+ 403,
4759
+ requestId,
4760
+ // FR-008 / D-7: reddin ÇARESİ gövdede — istemci `required`'ı görünce ilgili
4761
+ // nesneyi yeniden çeker (Entra CAE deseni); cümle değişmez.
4762
+ {
4763
+ required: spec.permission
4764
+ }
4765
+ );
3079
4766
  }
3080
4767
  }
3081
4768
  if (spec.role !== void 0) {
3082
4769
  const held = await rolesOfCaller();
3083
4770
  if (!held.includes(spec.role)) {
3084
- return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId);
4771
+ return envelope("forbidden", `This endpoint requires the "${spec.role}" role`, 403, requestId, {
4772
+ required_role: spec.role
4773
+ });
3085
4774
  }
3086
4775
  }
3087
4776
  return null;
@@ -3117,6 +4806,11 @@ async function createApp(opts) {
3117
4806
  const sql = opts.sql ?? await defaultSqlDriver(config);
3118
4807
  await sql.unsafe("select 1");
3119
4808
  setSchema(opts.schemas ?? []);
4809
+ setCursorKey(config.cursorSecret || config.serviceRoleKey || null);
4810
+ if (config.enableCommands) await installCommandExecutor(sql, [
4811
+ config.dbRole,
4812
+ config.dbServiceRole
4813
+ ]);
3120
4814
  setSecretReader(opts.secretReader ?? null);
3121
4815
  const auth = new AuthVerifier({
3122
4816
  jwksUrl: config.authJwksUrl,
@@ -3162,6 +4856,9 @@ async function createApp(opts) {
3162
4856
  role: config.dbRole,
3163
4857
  serviceRole: config.dbServiceRole,
3164
4858
  claimsJson: "{}"
4859
+ }, config.databaseDiagnostics === void 0 ? void 0 : {
4860
+ ...config.databaseDiagnostics,
4861
+ endpoint: "scheduled job"
3165
4862
  });
3166
4863
  try {
3167
4864
  const out = await runWithRuntime(buildServices(db), fn);
@@ -3191,6 +4888,10 @@ async function createApp(opts) {
3191
4888
  role: config.dbRole,
3192
4889
  serviceRole: config.dbServiceRole,
3193
4890
  claimsJson: JSON.stringify(callerClaims ?? {})
4891
+ }, config.databaseDiagnostics === void 0 ? void 0 : {
4892
+ ...config.databaseDiagnostics,
4893
+ endpoint: "upload authorization",
4894
+ requestId
3194
4895
  });
3195
4896
  try {
3196
4897
  const refusal = await refuseUnlessAuthorized(uploadDb.client, spec, requestId, makeRoleResolver(uploadDb.client));
@@ -3238,6 +4939,11 @@ async function createApp(opts) {
3238
4939
  role: config.dbRole,
3239
4940
  serviceRole: config.dbServiceRole,
3240
4941
  claimsJson: JSON.stringify(claims ?? {})
4942
+ }, config.databaseDiagnostics === void 0 && meta.options.databaseBudget === void 0 ? void 0 : {
4943
+ ...config.databaseDiagnostics,
4944
+ ...meta.options.databaseBudget,
4945
+ endpoint: hit.entry.id,
4946
+ requestId
3241
4947
  });
3242
4948
  const rolesOfCaller = makeRoleResolver(db.client);
3243
4949
  let dispatched = false;
@@ -3667,11 +5373,15 @@ export {
3667
5373
  effectiveAuth,
3668
5374
  RateLimiter,
3669
5375
  makeMemoryCache,
5376
+ setCursorKey,
5377
+ COMMAND_EXECUTOR_DDL,
5378
+ installCommandExecutor,
3670
5379
  quoteIdent,
3671
5380
  createLazyTransaction,
3672
5381
  createOps,
3673
5382
  withTables,
3674
5383
  createRequestDatabase,
5384
+ withQueryCancellation,
3675
5385
  buildRouteTable,
3676
5386
  matchRoute,
3677
5387
  scrubSecrets,
@@ -3679,4 +5389,4 @@ export {
3679
5389
  installEgressFence,
3680
5390
  createApp
3681
5391
  };
3682
- //# sourceMappingURL=chunk-C6COAB3E.js.map
5392
+ //# sourceMappingURL=chunk-XZPDPYVY.js.map