okmodel 0.7.0 → 0.9.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.
- package/README.md +14 -0
- package/dist/adapters/capabilities.d.ts +32 -0
- package/dist/adapters/failure.d.ts +1 -1
- package/dist/adapters/pg/bun-sql.d.ts +53 -0
- package/dist/adapters/pg/bunsql-extra.d.ts +1 -1
- package/dist/adapters/pg/bunsql.d.ts +1 -1
- package/dist/adapters/pg/neon-extra.d.ts +18 -0
- package/dist/adapters/pg/neon.d.ts +104 -0
- package/dist/adapters/pg/no-inflight-timeout.d.ts +1 -1
- package/dist/contracts/error.d.ts +1 -1
- package/dist/dialects/pg/ext/btree-gin.d.ts +26 -0
- package/dist/dialects/pg/ext/btree-gin.js +20 -0
- package/dist/dialects/pg/ext/btree-gist.d.ts +26 -0
- package/dist/dialects/pg/ext/btree-gist.js +20 -0
- package/dist/dialects/pg/ext/call.d.ts +53 -0
- package/dist/dialects/pg/ext/citext.js +1 -1
- package/dist/dialects/pg/ext/feature.d.ts +26 -0
- package/dist/dialects/pg/ext/fuzzystrmatch.d.ts +34 -0
- package/dist/dialects/pg/ext/fuzzystrmatch.js +33 -0
- package/dist/dialects/pg/ext/hstore.d.ts +41 -0
- package/dist/dialects/pg/ext/hstore.js +29 -0
- package/dist/dialects/pg/ext/index.d.ts +56 -0
- package/dist/dialects/pg/ext/index.js +1 -1
- package/dist/dialects/pg/ext/ltree.d.ts +41 -0
- package/dist/dialects/pg/ext/ltree.js +29 -0
- package/dist/dialects/pg/ext/pg-trgm.js +4 -13
- package/dist/dialects/pg/ext/pgcrypto.d.ts +33 -0
- package/dist/dialects/pg/ext/pgcrypto.js +33 -0
- package/dist/dialects/pg/ext/unaccent.d.ts +26 -0
- package/dist/dialects/pg/ext/unaccent.js +21 -0
- package/dist/dialects/pg/ext/using.d.ts +23 -0
- package/dist/dialects/pg/ext/versions.d.ts +36 -0
- package/dist/dialects/pg/external.d.ts +19 -0
- package/dist/dialects/pg/fn/index.d.ts +39 -1
- package/dist/dialects/pg/fn/index.js +118 -26
- package/dist/dialects/pg/index.d.ts +3 -0
- package/dist/dialects/pg/index.js +394 -14
- package/dist/dialects/pg/introspect.d.ts +11 -0
- package/dist/dialects/pg/operators.d.ts +1 -1
- package/dist/dialects/pg/ops/fragment.d.ts +119 -0
- package/dist/dialects/pg/ops/lane.d.ts +49 -0
- package/dist/dialects/pg/view/client.d.ts +14 -6
- package/dist/dialects/pg/view/index.d.ts +73 -1
- package/dist/dialects/pg/view/index.js +156 -5
- package/dist/dialects/pg/view/scratch.d.ts +11 -5
- package/dist/runtime/artifact-file.d.ts +22 -0
- package/dist/runtime/client.d.ts +10 -0
- package/dist/runtime/drift-detail.d.ts +2 -1
- package/dist/runtime/drift.d.ts +3 -10
- package/dist/runtime/otel.d.ts +105 -0
- package/dist/runtime/otel.js +97 -0
- package/dist/runtime/pg/bun.js +4 -4
- package/dist/runtime/pg/neon.d.ts +38 -0
- package/dist/runtime/pg/neon.js +64 -0
- package/dist/runtime/pg/pg.js +4 -4
- package/dist/runtime/pg/pglite.js +4 -4
- package/dist/runtime/pg/postgresjs.js +4 -4
- package/dist/runtime/plan.d.ts +1 -1
- package/dist/runtime/server-check.d.ts +36 -6
- package/dist/runtime/sql-fragment.d.ts +43 -0
- package/dist/runtime/tenancy/index.js +10 -10
- package/dist/runtime/topology.d.ts +1 -1
- package/dist/runtime/traits/index.js +16 -17
- package/dist/runtime/types.d.ts +15 -1
- package/dist/runtime/validate/index.js +4 -4
- package/dist/shared/{81ebdx9p.js → 0j6qe91y.js} +4 -4
- package/dist/shared/{qq3899ht.js → 12z7aznj.js} +243 -14
- package/dist/shared/{gzaveazc.js → 1d9q9cv2.js} +29 -2
- package/dist/shared/{w79hsckn.js → 1mxm04fy.js} +5 -5
- package/dist/shared/{7k9grvf6.js → 2szgv6yj.js} +5 -5
- package/dist/shared/{gs74dbj5.js → 2zdxkt7b.js} +933 -111
- package/dist/shared/{m6p12kbr.js → 3456242y.js} +23 -23
- package/dist/shared/3bb7wsqt.js +99 -0
- package/dist/shared/{y6njgyda.js → 3t6kw83m.js} +2 -2
- package/dist/shared/{xhfsfkgk.js → 4vbf83xx.js} +9 -2
- package/dist/shared/{pt94kz7f.js → 5kyb3tqx.js} +1 -1
- package/dist/shared/61bmrmhr.js +35 -0
- package/dist/shared/{x933k9a3.js → 62b603gr.js} +10 -10
- package/dist/shared/{jgqj11w4.js → 6f0z3hzh.js} +4 -0
- package/dist/shared/{tsvww22q.js → 6nae0spm.js} +156 -95
- package/dist/shared/{mtaszv5a.js → 71n0jmhc.js} +102 -4
- package/dist/shared/{5e3tf63j.js → 760ppr76.js} +14 -6
- package/dist/shared/7ba5h6pb.js +17 -0
- package/dist/shared/{sfz1ksn6.js → 8jesqsrk.js} +2 -2
- package/dist/shared/{5v3xf80k.js → 8n688yjg.js} +1 -1
- package/dist/shared/{rs73fca9.js → 8qzkd9f3.js} +6 -6
- package/dist/shared/{ax7qv55k.js → 90sg0w5a.js} +202 -25
- package/dist/shared/{zm0ptry0.js → 99qp9vtg.js} +1 -1
- package/dist/shared/{xnxsed62.js → 9dr2qnx7.js} +14 -14
- package/dist/shared/{1w508jbr.js → 9dtgn1z2.js} +21 -17
- package/dist/shared/{h15jx9bc.js → 9dvh1dr6.js} +1 -1
- package/dist/shared/{q5c6ngn3.js → a62rb2da.js} +1 -1
- package/dist/shared/{te5as7z7.js → aksb89mn.js} +3 -3
- package/dist/shared/{bz84tkzm.js → bphrgwz9.js} +2 -2
- package/dist/shared/bsek72fg.js +225 -0
- package/dist/shared/{7mnme5tn.js → c3mvnq7a.js} +1 -1
- package/dist/shared/{p6qc5gcp.js → ccsmhv9r.js} +25 -7
- package/dist/shared/{pbp1ss85.js → cexb7z6b.js} +2 -2
- package/dist/shared/{ab9gmbwy.js → cf8zdz5p.js} +5 -5
- package/dist/shared/{jvd0mqag.js → ckz05z15.js} +5 -5
- package/dist/shared/{dk8v79p3.js → d5nmj7vv.js} +1 -1
- package/dist/shared/{14vx95q4.js → dd5pmnky.js} +7 -7
- package/dist/shared/{vb6qfyve.js → ds229x4r.js} +2 -2
- package/dist/shared/{1qa0qajz.js → e275q7sd.js} +1 -1
- package/dist/shared/{zp9fmend.js → ebhz8vh6.js} +4 -4
- package/dist/shared/{hyh73qk6.js → ecn8mfdq.js} +5 -5
- package/dist/shared/{je4axnm8.js → fdsz3w80.js} +2 -2
- package/dist/shared/{9s0ypp87.js → fk9vef30.js} +85 -74
- package/dist/shared/{g6evj1j3.js → g89fmz5s.js} +1 -1
- package/dist/shared/{aytqh5y2.js → ghqzdp94.js} +2 -2
- package/dist/shared/h9811jjm.js +17 -0
- package/dist/shared/{dkxgzaqd.js → hqpxf4a4.js} +1 -1
- package/dist/shared/{8skf0cam.js → jpybhf98.js} +1 -1
- package/dist/shared/{67txcrcw.js → kgwp4jdd.js} +4 -4
- package/dist/shared/kzgrnckd.js +29 -0
- package/dist/shared/mazpbcdj.js +233 -0
- package/dist/shared/{kb2sdhfb.js → mfvx6kjx.js} +17 -1
- package/dist/shared/{svncdeh5.js → mgaq2sd3.js} +11 -8
- package/dist/shared/{5mtf6cj3.js → mvmm3xpb.js} +5 -2
- package/dist/shared/{b5pftnn2.js → myk8fswx.js} +66 -21
- package/dist/shared/{2shdbn8q.js → n306gqkd.js} +9 -9
- package/dist/shared/{n816peh1.js → n454kcnw.js} +1 -1
- package/dist/shared/n5whk3mt.js +665 -0
- package/dist/shared/n7kg8mmx.js +61 -0
- package/dist/shared/{vqrjpcqn.js → nmnsgj7x.js} +1 -1
- package/dist/shared/{tbrvgw6z.js → npdftanb.js} +19 -26
- package/dist/shared/{ek2wt2dz.js → pg4ezgyf.js} +11 -30
- package/dist/shared/{5fe3kbsh.js → pgsak1yw.js} +1 -1
- package/dist/shared/{bm22fvzr.js → qfw14tna.js} +2 -2
- package/dist/shared/{wa07eas9.js → qvwwp1mp.js} +62 -41
- package/dist/shared/rbxt795q.js +1766 -0
- package/dist/shared/{xjk7hnn5.js → s7he5e5d.js} +2 -2
- package/dist/shared/{w72kgg4q.js → ss2kv2rs.js} +6 -6
- package/dist/shared/stfa9xcw.js +450 -0
- package/dist/shared/{3v7t1yrv.js → th3be82n.js} +2 -2
- package/dist/shared/{79dt02vc.js → tr64w5p0.js} +1 -1
- package/dist/shared/{cn957f8d.js → tw1nvvfj.js} +3 -3
- package/dist/shared/{ed0b8k4s.js → typ3vy7t.js} +1 -1
- package/dist/shared/{9628a9xt.js → vk7rag46.js} +1 -1
- package/dist/shared/{ze4hxsmy.js → w9ejzaqf.js} +1 -1
- package/dist/shared/{vczs8gd4.js → wvtjg5m3.js} +2 -2
- package/dist/shared/{9y3mmmh1.js → xfhe73sb.js} +1 -1
- package/dist/shared/xqacq37x.js +61 -0
- package/dist/shared/{1est4ezg.js → xtsydxjg.js} +3 -3
- package/dist/shared/xv8qj97g.js +96 -0
- package/dist/shared/{bg3rym3g.js → xwhdcrm4.js} +2 -2
- package/dist/shared/{kr0jzbw2.js → yvabyg9k.js} +7 -7
- package/dist/shared/yybhre6n.js +136 -0
- package/dist/shared/zdqf8k5d.js +8 -0
- package/dist/shared/{jznfax0y.js → zq1he88x.js} +4 -4
- package/dist/tooling/cli.js +108 -23
- package/dist/tooling/driver-run.js +4654 -0
- package/dist/tooling/driver-test.d.ts +36 -0
- package/dist/tooling/github/comment.d.ts +50 -0
- package/dist/tooling/github/post-comment.d.ts +17 -0
- package/dist/tooling/migrate/commands.d.ts +3 -2
- package/dist/tooling/migrate/config.d.ts +13 -0
- package/dist/tooling/migrate/export.d.ts +165 -0
- package/dist/tooling/migrate/ext-surface.d.ts +80 -0
- package/dist/tooling/migrate/ext.d.ts +31 -4
- package/dist/tooling/migrate/import-drizzle.d.ts +40 -0
- package/dist/tooling/migrate/index.d.ts +3 -0
- package/dist/tooling/migrate/index.js +55 -10
- package/dist/tooling/migrate/lint-rules.d.ts +18 -1
- package/dist/tooling/migrate/lint.d.ts +14 -1
- package/dist/tooling/migrate/place.d.ts +18 -0
- package/dist/tooling/migrate/plan-json.d.ts +65 -0
- package/dist/tooling/migrate/project.d.ts +13 -1
- package/dist/tooling/migrate/pull-emit.d.ts +98 -0
- package/dist/tooling/migrate/pull-routines.d.ts +126 -0
- package/dist/tooling/migrate/pull-verify.d.ts +26 -0
- package/dist/tooling/migrate/pull.d.ts +62 -0
- package/dist/tooling/migrate/reprint.d.ts +32 -0
- package/dist/tooling/migrate/sandbox.d.ts +8 -2
- package/dist/tooling/migrate/schema-api.d.ts +43 -0
- package/dist/tooling/migrate/sql-lex.d.ts +18 -0
- package/dist/tooling/shared/04svnxxj.js +344 -0
- package/dist/tooling/shared/0j0jtse1.js +13 -0
- package/dist/tooling/shared/0r05zhyq.js +618 -0
- package/dist/tooling/shared/1800eymw.js +49 -0
- package/dist/tooling/shared/35zemrmf.js +12 -0
- package/dist/tooling/shared/3rmaz3pq.js +140 -0
- package/dist/tooling/shared/3yn79q5m.js +12 -0
- package/dist/tooling/shared/54tqkxja.js +51 -0
- package/dist/tooling/shared/57btdkgm.js +209 -0
- package/dist/tooling/shared/5bmksndh.js +118 -0
- package/dist/tooling/shared/5p8km5jb.js +38 -0
- package/dist/tooling/shared/5x3pf400.js +185 -0
- package/dist/tooling/shared/5zrzffnk.js +6 -0
- package/dist/tooling/shared/62fxpc1p.js +332 -0
- package/dist/tooling/shared/68tf18v4.js +20 -0
- package/dist/tooling/shared/6vk28rjp.js +98 -0
- package/dist/tooling/shared/76pv6qff.js +9 -0
- package/dist/tooling/shared/7aews1zg.js +311 -0
- package/dist/tooling/shared/7b3xp9gc.js +195 -0
- package/dist/tooling/shared/7kms6fvj.js +20 -0
- package/dist/tooling/shared/7pxncnzs.js +40 -0
- package/dist/tooling/shared/829fjxyw.js +198 -0
- package/dist/tooling/shared/8m5znpdt.js +66 -0
- package/dist/tooling/shared/8yr7ges1.js +211 -0
- package/dist/tooling/shared/96ps4g1z.js +32 -0
- package/dist/tooling/shared/9qr9rkbh.js +143 -0
- package/dist/tooling/shared/9sfbtwsk.js +49 -0
- package/dist/tooling/shared/9wsnsvbf.js +10 -0
- package/dist/tooling/shared/a2xe04bm.js +18 -0
- package/dist/tooling/shared/a4pzghha.js +20 -0
- package/dist/tooling/shared/am04v8na.js +233 -0
- package/dist/tooling/shared/asgr07mp.js +194 -0
- package/dist/tooling/shared/c0xevn1h.js +312 -0
- package/dist/tooling/shared/dhatassp.js +134 -0
- package/dist/tooling/shared/ezqn88nj.js +1696 -0
- package/dist/tooling/shared/f77nnfqy.js +231 -0
- package/dist/tooling/shared/fh07z1dt.js +15 -0
- package/dist/tooling/shared/gqwevek4.js +81 -0
- package/dist/tooling/shared/hfq3n3bd.js +4116 -0
- package/dist/tooling/shared/htnaj77v.js +17 -0
- package/dist/tooling/shared/j664qjrt.js +64 -0
- package/dist/tooling/shared/jpgmjsx3.js +38 -0
- package/dist/tooling/shared/jr2nmr81.js +497 -0
- package/dist/tooling/shared/jsjjz875.js +139 -0
- package/dist/tooling/shared/kbqnrs1x.js +1425 -0
- package/dist/tooling/shared/kz715295.js +94 -0
- package/dist/tooling/shared/m3xt8b1h.js +22 -0
- package/dist/tooling/shared/m4hhb8aa.js +9 -0
- package/dist/tooling/shared/mw5npye0.js +16 -0
- package/dist/tooling/shared/n4jqhpqz.js +27 -0
- package/dist/tooling/shared/nxq95eb6.js +42 -0
- package/dist/tooling/shared/qartgazh.js +70 -0
- package/dist/tooling/shared/rcywj2w8.js +190 -0
- package/dist/tooling/shared/s6qha57m.js +16 -0
- package/dist/tooling/shared/shjs0379.js +150 -0
- package/dist/tooling/shared/sjwkt1pg.js +16 -0
- package/dist/tooling/shared/stebqyyd.js +401 -0
- package/dist/tooling/shared/t4xveemq.js +250 -0
- package/dist/tooling/shared/t6zjeake.js +261 -0
- package/dist/tooling/shared/tcwpjvxe.js +9 -0
- package/dist/tooling/shared/tpssp7ac.js +160 -0
- package/dist/tooling/shared/v6j1ep6w.js +42 -0
- package/dist/tooling/shared/vwr2y833.js +270 -0
- package/dist/tooling/shared/w17sj05d.js +7 -0
- package/dist/tooling/shared/w5d89rb4.js +131 -0
- package/dist/tooling/shared/wbwq1jsa.js +157 -0
- package/dist/tooling/shared/wezxf3h8.js +17 -0
- package/dist/tooling/shared/yb4kd19s.js +12 -0
- package/dist/tooling/shared/ybdew6j7.js +294 -0
- package/dist/tooling/shared/yfedr1zb.js +6 -0
- package/dist/tooling/shared/yhp829bx.js +177 -0
- package/dist/tooling/shared/ytk1tjs0.js +604 -0
- package/dist/tooling/shared/zs9aymcp.js +210 -0
- package/dist/tooling/shared/zx3v9phv.js +8 -0
- package/dist/tooling/testing/index.js +3 -3
- package/package.json +50 -9
- package/dist/shared/ktz42yez.js +0 -113
- package/dist/shared/ttsqgp4p.js +0 -71
- package/dist/shared/y34a8714.js +0 -150
- package/dist/shared/zy8r7qvz.js +0 -116
- package/dist/shared/{ywr8hmey.js → 606g0nbf.js} +3 -3
- package/dist/shared/{rpycarcr.js → gcefca6p.js} +4 -4
|
@@ -0,0 +1,4654 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OkmError,
|
|
3
|
+
catalogError,
|
|
4
|
+
throwNamed,
|
|
5
|
+
DriverError2,
|
|
6
|
+
mapPostgresError2,
|
|
7
|
+
assertIdentifier,
|
|
8
|
+
fitIdentifier,
|
|
9
|
+
assertStoredText,
|
|
10
|
+
staticNamespace,
|
|
11
|
+
identityKey,
|
|
12
|
+
identityLabel,
|
|
13
|
+
sameNamespace,
|
|
14
|
+
CATALOG_VERSION,
|
|
15
|
+
table,
|
|
16
|
+
column,
|
|
17
|
+
index,
|
|
18
|
+
constraint,
|
|
19
|
+
sequence,
|
|
20
|
+
compareText,
|
|
21
|
+
enumType,
|
|
22
|
+
domainType,
|
|
23
|
+
sameEnumLabels,
|
|
24
|
+
indexObjects,
|
|
25
|
+
orderIndexed,
|
|
26
|
+
quoteLiteral,
|
|
27
|
+
renderCatalog2,
|
|
28
|
+
clientGenerator,
|
|
29
|
+
readClientGenerator,
|
|
30
|
+
definition,
|
|
31
|
+
unavailable,
|
|
32
|
+
rejected,
|
|
33
|
+
ColumnBuilder,
|
|
34
|
+
required,
|
|
35
|
+
openColumn,
|
|
36
|
+
formatType,
|
|
37
|
+
retarget,
|
|
38
|
+
UUIDV7_POSTGRES,
|
|
39
|
+
isPostgresMajor,
|
|
40
|
+
parsePostgresFloor,
|
|
41
|
+
formatPostgresFloor,
|
|
42
|
+
minPostgresDefinition,
|
|
43
|
+
uuidv7NeedsPostgres,
|
|
44
|
+
json,
|
|
45
|
+
jsonb,
|
|
46
|
+
quote,
|
|
47
|
+
primaryUrl
|
|
48
|
+
} from "./shared/hfq3n3bd.js";
|
|
49
|
+
import { createRequire } from "node:module";
|
|
50
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
51
|
+
|
|
52
|
+
// packages/harness/src/driver-run.ts
|
|
53
|
+
import { pathToFileURL } from "node:url";
|
|
54
|
+
import { resolve } from "node:path";
|
|
55
|
+
|
|
56
|
+
// packages/harness/src/expect.ts
|
|
57
|
+
function expect(actual) {
|
|
58
|
+
const fail = (matcher, expected) => {
|
|
59
|
+
throw new Error(`${matcher}: ${preview(actual)} vs ${preview(expected)}`);
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
toBe(expected) {
|
|
63
|
+
if (!Object.is(actual, expected))
|
|
64
|
+
fail("toBe", expected);
|
|
65
|
+
},
|
|
66
|
+
toBeNull() {
|
|
67
|
+
if (actual !== null)
|
|
68
|
+
fail("toBeNull", null);
|
|
69
|
+
},
|
|
70
|
+
toBeUndefined() {
|
|
71
|
+
if (actual !== undefined)
|
|
72
|
+
fail("toBeUndefined", undefined);
|
|
73
|
+
},
|
|
74
|
+
toEqual(expected) {
|
|
75
|
+
if (!deepEqual(actual, expected))
|
|
76
|
+
fail("toEqual", expected);
|
|
77
|
+
},
|
|
78
|
+
toHaveLength(expected) {
|
|
79
|
+
const length = lengthOf(actual);
|
|
80
|
+
if (length !== expected)
|
|
81
|
+
fail("toHaveLength", expected);
|
|
82
|
+
},
|
|
83
|
+
toBeInstanceOf(expected) {
|
|
84
|
+
if (!instanceOf(actual, expected))
|
|
85
|
+
fail("toBeInstanceOf", expected.name);
|
|
86
|
+
},
|
|
87
|
+
toContain(expected) {
|
|
88
|
+
if (typeof actual === "string") {
|
|
89
|
+
if (!actual.includes(expected))
|
|
90
|
+
fail("toContain", expected);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (Array.isArray(actual) && actual.some((item) => Object.is(item, expected)))
|
|
94
|
+
return;
|
|
95
|
+
fail("toContain", expected);
|
|
96
|
+
},
|
|
97
|
+
toBeLessThan(expected) {
|
|
98
|
+
if (typeof actual !== "number" || !(actual < expected))
|
|
99
|
+
fail("toBeLessThan", expected);
|
|
100
|
+
},
|
|
101
|
+
toMatchObject(expected) {
|
|
102
|
+
if (!matchObject(actual, expected))
|
|
103
|
+
fail("toMatchObject", expected);
|
|
104
|
+
},
|
|
105
|
+
not: {
|
|
106
|
+
toBe(expected) {
|
|
107
|
+
if (Object.is(actual, expected))
|
|
108
|
+
fail("not.toBe", expected);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function lengthOf(value) {
|
|
114
|
+
if (typeof value === "string" || Array.isArray(value))
|
|
115
|
+
return value.length;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
function instanceOf(value, expected) {
|
|
119
|
+
return (typeof value === "object" || typeof value === "function") && value instanceof expected;
|
|
120
|
+
}
|
|
121
|
+
function deepEqual(left, right) {
|
|
122
|
+
if (Object.is(left, right))
|
|
123
|
+
return true;
|
|
124
|
+
if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
128
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
129
|
+
return false;
|
|
130
|
+
return left.every((item, index) => deepEqual(item, right[index]));
|
|
131
|
+
}
|
|
132
|
+
const leftRecord = left;
|
|
133
|
+
const rightRecord = right;
|
|
134
|
+
const leftKeys = Object.keys(leftRecord);
|
|
135
|
+
const rightKeys = Object.keys(rightRecord);
|
|
136
|
+
if (leftKeys.length !== rightKeys.length)
|
|
137
|
+
return false;
|
|
138
|
+
return leftKeys.every((key) => Object.hasOwn(rightRecord, key) && deepEqual(leftRecord[key], rightRecord[key]));
|
|
139
|
+
}
|
|
140
|
+
function matchObject(actual, expected) {
|
|
141
|
+
if (typeof actual !== "object" || actual === null)
|
|
142
|
+
return false;
|
|
143
|
+
const record = actual;
|
|
144
|
+
const wanted = expected;
|
|
145
|
+
return Object.keys(wanted).every((key) => {
|
|
146
|
+
const item = wanted[key];
|
|
147
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
148
|
+
return matchObject(record[key], item);
|
|
149
|
+
}
|
|
150
|
+
return deepEqual(record[key], item);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
function preview(value) {
|
|
154
|
+
if (typeof value === "string")
|
|
155
|
+
return JSON.stringify(value);
|
|
156
|
+
if (typeof value === "function")
|
|
157
|
+
return value.name === "" ? "function" : value.name;
|
|
158
|
+
try {
|
|
159
|
+
const text = JSON.stringify(value);
|
|
160
|
+
if (text !== undefined)
|
|
161
|
+
return text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
162
|
+
} catch {}
|
|
163
|
+
return String(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// packages/harness/src/driver-suite.ts
|
|
167
|
+
function skipFor(test, capability, title) {
|
|
168
|
+
const label = title ?? ((name) => name);
|
|
169
|
+
const register = (name, fn, timeoutMs) => {
|
|
170
|
+
if (test.unsupported !== undefined) {
|
|
171
|
+
test.unsupported(name, capability);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
test.skip(label(name), fn, timeoutMs);
|
|
175
|
+
};
|
|
176
|
+
register.skip = (name, fn, timeoutMs) => {
|
|
177
|
+
if (test.unsupported !== undefined) {
|
|
178
|
+
test.unsupported(name, capability);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
test.skip(label(name), fn, timeoutMs);
|
|
182
|
+
};
|
|
183
|
+
return register;
|
|
184
|
+
}
|
|
185
|
+
function registerDriverSuite(suite) {
|
|
186
|
+
const { name } = suite;
|
|
187
|
+
const test = suite.test;
|
|
188
|
+
const optional = (flag, capability) => flag ? test : skipFor(test, capability);
|
|
189
|
+
test(`${name} execute returns rows`, async () => {
|
|
190
|
+
await using(suite.open, async (pool) => {
|
|
191
|
+
const result = await pool.execute("SELECT 1::text AS n");
|
|
192
|
+
expect(result.rows).toEqual([["1"]]);
|
|
193
|
+
expect(result.count).toBe(1);
|
|
194
|
+
expect(result.notices).toEqual([]);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
test(`${name} keeps null as null`, async () => {
|
|
198
|
+
await using(suite.open, async (pool) => {
|
|
199
|
+
const result = await pool.execute("SELECT $1::text, $2::int4", ["a", null]);
|
|
200
|
+
expect(result.rows).toEqual([["a", null]]);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
test(`${name} keeps timestamps as wire text`, async () => {
|
|
204
|
+
await using(suite.open, async (pool) => {
|
|
205
|
+
const zone = "with cfg as (select set_config('TimeZone', 'UTC', true))";
|
|
206
|
+
const equal = await pool.execute(`${zone} select ($1::timestamptz = TIMESTAMPTZ '2020-01-02 03:04:05+00') from cfg`, ["2020-01-02 03:04:05+00"]);
|
|
207
|
+
expect(equal.rows[0]?.[0]).toBe("t");
|
|
208
|
+
const printed = await scalar(pool, `${zone} select $1::timestamptz::text from cfg`, [
|
|
209
|
+
"2020-01-02 03:04:05+00"
|
|
210
|
+
]);
|
|
211
|
+
expect(printed).toContain("2020-01-02");
|
|
212
|
+
expect(printed).toContain("03:04:05");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
test(`${name} keeps numeric as wire text`, async () => {
|
|
216
|
+
await using(suite.open, async (pool) => {
|
|
217
|
+
expect(await scalar(pool, "SELECT $1::numeric", ["12345678901234567890.25"])).toBe("12345678901234567890.25");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
test(`${name} keeps bigint as wire text`, async () => {
|
|
221
|
+
await using(suite.open, async (pool) => {
|
|
222
|
+
expect(await scalar(pool, "SELECT $1::int8", ["9223372036854775807"])).toBe("9223372036854775807");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
test(`${name} keeps json as wire text`, async () => {
|
|
226
|
+
await using(suite.open, async (pool) => {
|
|
227
|
+
const cell = await scalar(pool, "SELECT $1::jsonb", ['{"a":1}']);
|
|
228
|
+
expect(JSON.parse(cell ?? "null")).toEqual({ a: 1 });
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
test(`${name} keeps arrays as wire text`, async () => {
|
|
232
|
+
await using(suite.open, async (pool) => {
|
|
233
|
+
expect(await scalar(pool, "SELECT $1::int[]", ["{1,2,3}"])).toBe("{1,2,3}");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
const notices = suite.notices === false ? skipFor(test, "notices") : test;
|
|
237
|
+
notices(`${name} returns notices`, async () => {
|
|
238
|
+
await using(suite.open, async (pool) => {
|
|
239
|
+
const result = await pool.execute("DO $$ BEGIN RAISE NOTICE 'okm-p13'; END $$");
|
|
240
|
+
expect(result.notices.some((notice) => notice.message.includes("okm-p13"))).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
test(`${name} batch commits every statement`, async () => {
|
|
244
|
+
await using(suite.open, async (pool) => {
|
|
245
|
+
const table = tableName();
|
|
246
|
+
await pool.execute(`CREATE TABLE ${table} (id int)`);
|
|
247
|
+
const results = await pool.batch([insert(table, 1), insert(table, 2)]);
|
|
248
|
+
expect(results).toHaveLength(2);
|
|
249
|
+
expect(await countOf(pool, table)).toBe("2");
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
test(`${name} batch rolls back a failure at each position`, async () => {
|
|
253
|
+
await using(suite.open, async (pool) => {
|
|
254
|
+
for (const index of [0, 1, 2]) {
|
|
255
|
+
const table = tableName();
|
|
256
|
+
await pool.execute(`CREATE TABLE ${table} (id int)`);
|
|
257
|
+
const statements = [insert(table, 1), insert(table, 2), insert(table, 3)];
|
|
258
|
+
statements[index] = { text: "SELECT 1/0" };
|
|
259
|
+
const error = await failure(pool.batch(statements));
|
|
260
|
+
expect(error).toBeInstanceOf(DriverError2);
|
|
261
|
+
expect(error.batchIndex).toBe(index);
|
|
262
|
+
expect(await countOf(pool, table)).toBe("0");
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
test(`${name} batch reports a deferred constraint at commit`, async () => {
|
|
267
|
+
await using(suite.open, async (pool) => {
|
|
268
|
+
const table = tableName();
|
|
269
|
+
await pool.execute(`CREATE TABLE ${table} (n int)`);
|
|
270
|
+
await pool.execute(`ALTER TABLE ${table} ADD CONSTRAINT ${table}_n UNIQUE (n) DEFERRABLE INITIALLY DEFERRED`);
|
|
271
|
+
const error = await failure(pool.batch([insert(table, 1, "n"), insert(table, 1, "n")]));
|
|
272
|
+
expect(error.batchIndex).toBe(null);
|
|
273
|
+
expect(error.sqlstate).toBe("23505");
|
|
274
|
+
expect(await countOf(pool, table)).toBe("0");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
test(`${name} rejects a pre-aborted signal before the statement`, async () => {
|
|
278
|
+
await using(suite.open, async (pool) => {
|
|
279
|
+
const table = tableName();
|
|
280
|
+
await pool.execute(`CREATE TABLE ${table} (id int)`);
|
|
281
|
+
const error = await failure(pool.execute(`INSERT INTO ${table} (id) VALUES (1)`, undefined, {
|
|
282
|
+
signal: AbortSignal.abort()
|
|
283
|
+
}));
|
|
284
|
+
expect(error.kind).toBe("cancelled");
|
|
285
|
+
expect(await countOf(pool, table)).toBe("0");
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
optional(suite.capabilities.cancel, "cancel")(`${name} cancels an in-flight statement`, async () => {
|
|
289
|
+
await using(suite.open, async (pool) => {
|
|
290
|
+
const started = Date.now();
|
|
291
|
+
const error = await failure(pool.execute("SELECT pg_sleep(5)", undefined, { signal: AbortSignal.timeout(40) }));
|
|
292
|
+
expect(error.kind).toBe("cancelled");
|
|
293
|
+
expect(Date.now() - started).toBeLessThan(2000);
|
|
294
|
+
});
|
|
295
|
+
}, 1e4);
|
|
296
|
+
optional(suite.capabilities.cancel, "cancel")(`${name} times out an in-flight statement`, async () => {
|
|
297
|
+
await using(suite.open, async (pool) => {
|
|
298
|
+
const started = Date.now();
|
|
299
|
+
const error = await failure(pool.execute("SELECT pg_sleep(5)", undefined, { timeout: 80 }));
|
|
300
|
+
expect(error.kind).toBe("timeout");
|
|
301
|
+
expect(Date.now() - started).toBeLessThan(2000);
|
|
302
|
+
});
|
|
303
|
+
}, 1e4);
|
|
304
|
+
optional(suite.capabilities.cancel, "cancel")(`${name} cancels a batch and rolls it back`, async () => {
|
|
305
|
+
await using(suite.open, async (pool) => {
|
|
306
|
+
const table = tableName();
|
|
307
|
+
await pool.execute(`CREATE TABLE ${table} (id int)`);
|
|
308
|
+
const error = await failure(pool.batch([insert(table, 1), { text: "SELECT pg_sleep(5)" }], {
|
|
309
|
+
signal: AbortSignal.timeout(40)
|
|
310
|
+
}));
|
|
311
|
+
expect(error.kind).toBe("cancelled");
|
|
312
|
+
expect(await countOf(pool, table)).toBe("0");
|
|
313
|
+
});
|
|
314
|
+
}, 1e4);
|
|
315
|
+
optional(suite.capabilities.cancel, "cancel")(`${name} times out a batch and rolls it back`, async () => {
|
|
316
|
+
await using(suite.open, async (pool) => {
|
|
317
|
+
const table = tableName();
|
|
318
|
+
await pool.execute(`CREATE TABLE ${table} (id int)`);
|
|
319
|
+
const error = await failure(pool.batch([insert(table, 1), { text: "SELECT pg_sleep(5)" }], { timeout: 80 }));
|
|
320
|
+
expect(error.kind).toBe("timeout");
|
|
321
|
+
expect(await countOf(pool, table)).toBe("0");
|
|
322
|
+
});
|
|
323
|
+
}, 1e4);
|
|
324
|
+
test(`${name} runs a batch inside a transaction on a savepoint`, async () => {
|
|
325
|
+
await using(suite.open, async (pool) => {
|
|
326
|
+
const table = tableName();
|
|
327
|
+
const connection = await mustReserve(pool);
|
|
328
|
+
try {
|
|
329
|
+
await connection.execute(`CREATE TABLE ${table} (id int primary key)`);
|
|
330
|
+
await connection.execute("BEGIN");
|
|
331
|
+
await connection.execute(`INSERT INTO ${table} (id) VALUES (1)`);
|
|
332
|
+
const error = await failure(connection.batch([
|
|
333
|
+
{ text: `INSERT INTO ${table} (id) VALUES (2)` },
|
|
334
|
+
{ text: `INSERT INTO ${table} (id) VALUES (2)` }
|
|
335
|
+
]));
|
|
336
|
+
expect(error.batchIndex).toBe(1);
|
|
337
|
+
expect(await scalar(connection, `SELECT count(*)::text FROM ${table}`)).toBe("1");
|
|
338
|
+
await connection.execute("ROLLBACK");
|
|
339
|
+
} finally {
|
|
340
|
+
await connection.release();
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
test(`${name} clears a custom setting and an advisory lock on release`, async () => {
|
|
345
|
+
await using(suite.open, async (pool) => {
|
|
346
|
+
const first = await mustReserve(pool);
|
|
347
|
+
await first.execute("SELECT set_config('okm.p13', 'yes', false)");
|
|
348
|
+
await first.execute("SELECT pg_advisory_lock(42)");
|
|
349
|
+
const pid = await scalar(first, "SELECT pg_backend_pid()::text");
|
|
350
|
+
await first.release();
|
|
351
|
+
const second = await mustReserve(pool);
|
|
352
|
+
try {
|
|
353
|
+
const setting = await scalar(second, "SELECT current_setting('okm.p13', true)");
|
|
354
|
+
expect(setting).not.toBe("yes");
|
|
355
|
+
expect(await scalar(second, "SELECT pg_backend_pid()::text")).toBe(pid);
|
|
356
|
+
expect(await scalar(second, "SELECT count(*)::text FROM pg_locks WHERE locktype = 'advisory' AND pid = pg_backend_pid()")).toBe("0");
|
|
357
|
+
} finally {
|
|
358
|
+
await second.release();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
test(`${name} reports pool stats`, async () => {
|
|
363
|
+
await using(suite.open, async (pool) => {
|
|
364
|
+
expect(pool.stats()).toEqual({
|
|
365
|
+
size: suite.size,
|
|
366
|
+
idle: suite.size,
|
|
367
|
+
inflight: 0,
|
|
368
|
+
waiting: 0
|
|
369
|
+
});
|
|
370
|
+
const connection = await mustReserve(pool);
|
|
371
|
+
expect(pool.stats().idle).toBe(suite.size - 1);
|
|
372
|
+
await connection.release();
|
|
373
|
+
expect(pool.stats().idle).toBe(suite.size);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
test(`${name} close rejects a later execute`, async () => {
|
|
377
|
+
const pool = await suite.open();
|
|
378
|
+
await pool.close();
|
|
379
|
+
expect(await failure(pool.execute("SELECT 1"))).toBeInstanceOf(DriverError2);
|
|
380
|
+
});
|
|
381
|
+
test(`${name} acquire timeout is OKM1846 and does not use another pool`, async () => {
|
|
382
|
+
const limited = await suite.openLimited();
|
|
383
|
+
const other = await suite.openOther();
|
|
384
|
+
try {
|
|
385
|
+
const held = await mustReserve(limited);
|
|
386
|
+
try {
|
|
387
|
+
const error = await failure(mustReserve(limited));
|
|
388
|
+
expect(error).toBeInstanceOf(OkmError);
|
|
389
|
+
if (error instanceof OkmError)
|
|
390
|
+
expect(error.code).toBe("OKM1846");
|
|
391
|
+
expect(await scalar(other, "SELECT 1::text")).toBe("1");
|
|
392
|
+
} finally {
|
|
393
|
+
await held.release();
|
|
394
|
+
}
|
|
395
|
+
} finally {
|
|
396
|
+
await limited.close();
|
|
397
|
+
await other.close();
|
|
398
|
+
}
|
|
399
|
+
}, 1e4);
|
|
400
|
+
optional(suite.capabilities.describe, "describe")(`${name} describes a statement`, async () => {
|
|
401
|
+
await using(suite.open, async (pool) => {
|
|
402
|
+
if (pool.describe === undefined)
|
|
403
|
+
throw new DriverError2("describe is not on the pool");
|
|
404
|
+
const described = await pool.describe("SELECT $1::int AS n");
|
|
405
|
+
expect(described.columns).toEqual(["n"]);
|
|
406
|
+
expect(described.parameterCount).toBe(1);
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
optional(suite.capabilities.stream, "stream")(`${name} streams rows`, async () => {
|
|
410
|
+
await using(suite.open, async (pool) => {
|
|
411
|
+
if (pool.stream === undefined)
|
|
412
|
+
throw new DriverError2("stream is not on the pool");
|
|
413
|
+
const rows = [];
|
|
414
|
+
for await (const chunk of pool.stream("SELECT generate_series(1, 3)::text")) {
|
|
415
|
+
for (const row of chunk) {
|
|
416
|
+
const cell = row[0];
|
|
417
|
+
if (typeof cell === "string")
|
|
418
|
+
rows.push(cell);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
expect(rows).toEqual(["1", "2", "3"]);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
optional(suite.capabilities.listen, "listen")(`${name} delivers a notification`, async () => {
|
|
425
|
+
await using(suite.open, async (pool) => {
|
|
426
|
+
if (pool.listen === undefined)
|
|
427
|
+
throw new DriverError2("listen is not on the pool");
|
|
428
|
+
const payloads = [];
|
|
429
|
+
const stop = await pool.listen("okm_p13", (payload) => {
|
|
430
|
+
payloads.push(payload);
|
|
431
|
+
});
|
|
432
|
+
try {
|
|
433
|
+
await pool.execute("SELECT pg_notify('okm_p13', 'hello')");
|
|
434
|
+
await wait(50);
|
|
435
|
+
expect(payloads).toContain("hello");
|
|
436
|
+
} finally {
|
|
437
|
+
await stop();
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function using(open, fn) {
|
|
443
|
+
const pool = await open();
|
|
444
|
+
try {
|
|
445
|
+
await fn(pool);
|
|
446
|
+
} finally {
|
|
447
|
+
await pool.close();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function tableName() {
|
|
451
|
+
return `okm_${Math.random().toString(36).slice(2, 10)}`;
|
|
452
|
+
}
|
|
453
|
+
function insert(table, id, column = "id") {
|
|
454
|
+
return { text: `INSERT INTO ${table} (${column}) VALUES (${String(id)})` };
|
|
455
|
+
}
|
|
456
|
+
async function countOf(pool, table) {
|
|
457
|
+
return scalar(pool, `SELECT count(*)::text FROM ${table}`);
|
|
458
|
+
}
|
|
459
|
+
async function scalar(runner, text, params) {
|
|
460
|
+
const result = await runner.execute(text, params);
|
|
461
|
+
return result.rows[0]?.[0] ?? null;
|
|
462
|
+
}
|
|
463
|
+
async function failure(pending) {
|
|
464
|
+
try {
|
|
465
|
+
await pending;
|
|
466
|
+
} catch (error) {
|
|
467
|
+
if (error instanceof DriverError2 || error instanceof OkmError)
|
|
468
|
+
return error;
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
throw new DriverError2("expected the call to fail");
|
|
472
|
+
}
|
|
473
|
+
function mustReserve(pool) {
|
|
474
|
+
if (pool.reserve === undefined)
|
|
475
|
+
return Promise.reject(new DriverError2("reserve is not declared"));
|
|
476
|
+
return pool.reserve();
|
|
477
|
+
}
|
|
478
|
+
function wait(ms) {
|
|
479
|
+
return new Promise((resolve) => {
|
|
480
|
+
setTimeout(resolve, ms);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// packages/harness/src/error-suite.ts
|
|
485
|
+
function registerErrorMappingSuite(suite) {
|
|
486
|
+
const { name, test } = suite;
|
|
487
|
+
const optional = (flag, capability) => flag ? test : skipFor(test, capability);
|
|
488
|
+
test(`${name} maps a unique violation`, async () => {
|
|
489
|
+
await using2(suite.open, async (pool) => {
|
|
490
|
+
const table = tableName2();
|
|
491
|
+
await pool.execute(`CREATE TABLE ${table} (email text)`);
|
|
492
|
+
await pool.execute(`ALTER TABLE ${table} ADD CONSTRAINT ${table}_email_taken_key UNIQUE (email)`);
|
|
493
|
+
await pool.execute(`INSERT INTO ${table} (email) VALUES ('ada@example.com')`);
|
|
494
|
+
const error = await mapped(pool.execute(`INSERT INTO ${table} (email) VALUES ('ada@example.com')`));
|
|
495
|
+
expectKind(error, "unique", 409, false);
|
|
496
|
+
expect(error.table).toBe(table);
|
|
497
|
+
expect(error.columns).toEqual(["email"]);
|
|
498
|
+
expect(error.fields()).toEqual({ email: "email_taken" });
|
|
499
|
+
expect(error.summary).toBe(`Unique violation on ${table}.email (email_taken)`);
|
|
500
|
+
expect(error.batchIndex).toBe(null);
|
|
501
|
+
expectNoValue(error, "ada@example.com");
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
test(`${name} maps a not-null violation`, async () => {
|
|
505
|
+
await using2(suite.open, async (pool) => {
|
|
506
|
+
const table = tableName2();
|
|
507
|
+
await pool.execute(`CREATE TABLE ${table} (email text NOT NULL)`);
|
|
508
|
+
const error = await mapped(pool.execute(`INSERT INTO ${table} (email) VALUES (NULL)`));
|
|
509
|
+
expectKind(error, "not_null", 422, false);
|
|
510
|
+
expect(error.table).toBe(table);
|
|
511
|
+
expect(error.columns).toEqual(["email"]);
|
|
512
|
+
expect(error.fields()).toEqual({ email: "not_null" });
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
test(`${name} maps a check violation`, async () => {
|
|
516
|
+
await using2(suite.open, async (pool) => {
|
|
517
|
+
const table = tableName2();
|
|
518
|
+
await pool.execute(`CREATE TABLE ${table} (n int)`);
|
|
519
|
+
await pool.execute(`ALTER TABLE ${table} ADD CONSTRAINT ${table}_positive_check CHECK (n > 0)`);
|
|
520
|
+
const error = await mapped(pool.execute(`INSERT INTO ${table} (n) VALUES (0)`));
|
|
521
|
+
expectKind(error, "check", 422, false);
|
|
522
|
+
expect(error.table).toBe(table);
|
|
523
|
+
expect(error.fields()).toEqual({});
|
|
524
|
+
expect(error.summary).toContain("positive");
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
test(`${name} maps a foreign key violation`, async () => {
|
|
528
|
+
await using2(suite.open, async (pool) => {
|
|
529
|
+
const parent = tableName2();
|
|
530
|
+
const child = tableName2();
|
|
531
|
+
await pool.execute(`CREATE TABLE ${parent} (id int PRIMARY KEY)`);
|
|
532
|
+
await pool.execute(`CREATE TABLE ${child} (list_id int)`);
|
|
533
|
+
await pool.execute(`ALTER TABLE ${child} ADD CONSTRAINT ${child}_list_id_fkey FOREIGN KEY (list_id) REFERENCES ${parent} (id)`);
|
|
534
|
+
const error = await mapped(pool.execute(`INSERT INTO ${child} (list_id) VALUES (1)`));
|
|
535
|
+
expectKind(error, "foreign_key", 422, false);
|
|
536
|
+
expect(error.table).toBe(child);
|
|
537
|
+
expect(error.columns).toEqual(["list_id"]);
|
|
538
|
+
expect(error.fields()).toEqual({ list_id: "list_id" });
|
|
539
|
+
expectNoValue(error, "(1)");
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
test(`${name} maps an exclusion violation`, async () => {
|
|
543
|
+
await using2(suite.open, async (pool) => {
|
|
544
|
+
const table = tableName2();
|
|
545
|
+
await pool.execute(`CREATE TABLE ${table} (r int4range)`);
|
|
546
|
+
await pool.execute(`ALTER TABLE ${table} ADD CONSTRAINT ${table}_span_excl EXCLUDE USING gist (r WITH &&)`);
|
|
547
|
+
await pool.execute(`INSERT INTO ${table} (r) VALUES ('[1,5)')`);
|
|
548
|
+
const error = await mapped(pool.execute(`INSERT INTO ${table} (r) VALUES ('[4,6)')`));
|
|
549
|
+
expectKind(error, "exclusion", 409, false);
|
|
550
|
+
expect(error.table).toBe(table);
|
|
551
|
+
expect(error.columns).toEqual(["r"]);
|
|
552
|
+
expectNoValue(error, "[4,6)");
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
test(`${name} maps serialization, deadlock, and lock timeout`, async () => {
|
|
556
|
+
await using2(suite.open, async (pool) => {
|
|
557
|
+
const serialization = await mapped(raise(pool, "40001"));
|
|
558
|
+
expectKind(serialization, "serialization", 503, true);
|
|
559
|
+
const deadlock = await mapped(raise(pool, "40P01"));
|
|
560
|
+
expectKind(deadlock, "deadlock", 503, true);
|
|
561
|
+
const lockTimeout = await mapped(raise(pool, "55P03"));
|
|
562
|
+
expectKind(lockTimeout, "lock_timeout", 503, true);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
test(`${name} maps a statement timeout`, async () => {
|
|
566
|
+
await using2(suite.open, async (pool) => {
|
|
567
|
+
const error = await mapped(pool.execute("SELECT 1", undefined, { timeout: 0 }));
|
|
568
|
+
expectKind(error, "timeout", 503, true);
|
|
569
|
+
expect(error.kind).not.toBe("cancelled");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
optional(suite.capabilities.cancel, "cancel")(`${name} maps an in-flight statement timeout`, async () => {
|
|
573
|
+
await using2(suite.open, async (pool) => {
|
|
574
|
+
const error = await mapped(pool.execute("SELECT pg_sleep(5)", undefined, { timeout: 80 }));
|
|
575
|
+
expectKind(error, "timeout", 503, true);
|
|
576
|
+
});
|
|
577
|
+
}, 1e4);
|
|
578
|
+
test(`${name} maps a cancelled call and does not retry it`, async () => {
|
|
579
|
+
await using2(suite.open, async (pool) => {
|
|
580
|
+
const error = await mapped(pool.execute("SELECT 1", undefined, { signal: AbortSignal.abort() }));
|
|
581
|
+
expectKind(error, "cancelled", 503, false);
|
|
582
|
+
expect(error.fix.summary).toContain("Do not retry");
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
test(`${name} maps a connection failure`, async () => {
|
|
586
|
+
const pool = await suite.open();
|
|
587
|
+
await pool.close();
|
|
588
|
+
const error = await mapped(pool.execute("SELECT 1"));
|
|
589
|
+
expectKind(error, "unavailable", 503, true);
|
|
590
|
+
});
|
|
591
|
+
test(`${name} carries batchIndex on a batch failure and null at commit`, async () => {
|
|
592
|
+
await using2(suite.open, async (pool) => {
|
|
593
|
+
const table = tableName2();
|
|
594
|
+
await pool.execute(`CREATE TABLE ${table} (email text)`);
|
|
595
|
+
await pool.execute(`ALTER TABLE ${table} ADD CONSTRAINT ${table}_email_taken_key UNIQUE (email)`);
|
|
596
|
+
const failed = await mapped(pool.batch([insert2(table, "one@example.com"), insert2(table, "one@example.com")]));
|
|
597
|
+
expect(failed.kind).toBe("unique");
|
|
598
|
+
expect(failed.batchIndex).toBe(1);
|
|
599
|
+
expectNoValue(failed, "one@example.com");
|
|
600
|
+
const deferred = tableName2();
|
|
601
|
+
await pool.execute(`CREATE TABLE ${deferred} (n int)`);
|
|
602
|
+
await pool.execute(`ALTER TABLE ${deferred} ADD CONSTRAINT ${deferred}_n_key UNIQUE (n) DEFERRABLE INITIALLY DEFERRED`);
|
|
603
|
+
const atCommit = await mapped(pool.batch([
|
|
604
|
+
{ text: `INSERT INTO ${deferred} (n) VALUES (1)` },
|
|
605
|
+
{ text: `INSERT INTO ${deferred} (n) VALUES (1)` }
|
|
606
|
+
]));
|
|
607
|
+
expect(atCommit.kind).toBe("unique");
|
|
608
|
+
expect(atCommit.batchIndex).toBe(null);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async function using2(open, fn) {
|
|
613
|
+
const pool = await open();
|
|
614
|
+
try {
|
|
615
|
+
await fn(pool);
|
|
616
|
+
} finally {
|
|
617
|
+
await pool.close();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function mapped(pending) {
|
|
621
|
+
try {
|
|
622
|
+
await pending;
|
|
623
|
+
} catch (error) {
|
|
624
|
+
return mapPostgresError2(error);
|
|
625
|
+
}
|
|
626
|
+
throw new OkmError("driver", "expected the call to fail", { kind: "driver" });
|
|
627
|
+
}
|
|
628
|
+
function expectKind(error, kind, status, retryable) {
|
|
629
|
+
expect(error).toBeInstanceOf(OkmError);
|
|
630
|
+
expect(error.kind).toBe(kind);
|
|
631
|
+
expect(error.retryable).toBe(retryable);
|
|
632
|
+
expect(error.toHttp().status).toBe(status);
|
|
633
|
+
expect(error.toHttp().body.reason).toBe(error.summary);
|
|
634
|
+
}
|
|
635
|
+
function expectNoValue(error, value) {
|
|
636
|
+
expect(error.message.includes(value)).toBe(false);
|
|
637
|
+
expect(JSON.stringify(error.log()).includes(value)).toBe(false);
|
|
638
|
+
expect(error.values()).toBeUndefined();
|
|
639
|
+
}
|
|
640
|
+
function raise(pool, sqlstate) {
|
|
641
|
+
return pool.execute(`DO $$ BEGIN RAISE EXCEPTION 'okm' USING ERRCODE = '${sqlstate}'; END $$`);
|
|
642
|
+
}
|
|
643
|
+
function insert2(table, email) {
|
|
644
|
+
return { text: `INSERT INTO ${table} (email) VALUES ('${email}')` };
|
|
645
|
+
}
|
|
646
|
+
var ids = 0;
|
|
647
|
+
function tableName2() {
|
|
648
|
+
ids += 1;
|
|
649
|
+
return `okm_e${ids.toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// src/contracts/catalog/build.ts
|
|
653
|
+
function catalog(objects) {
|
|
654
|
+
for (const object of objects) {
|
|
655
|
+
if (object.kind !== object.identity.kind) {
|
|
656
|
+
catalogError("OKM1020", `${identityLabel(object.identity)} has kind ${object.kind}.`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
const indexed = indexObjects(objects);
|
|
660
|
+
const keys = new Set;
|
|
661
|
+
for (const item of indexed) {
|
|
662
|
+
keys.add(item.key);
|
|
663
|
+
}
|
|
664
|
+
assertForeignKeys(objects, keys);
|
|
665
|
+
orderIndexed(indexed);
|
|
666
|
+
const sorted = [...indexed].sort((left, right) => compareText(left.key, right.key));
|
|
667
|
+
return { version: CATALOG_VERSION, objects: sorted.map((item) => item.object) };
|
|
668
|
+
}
|
|
669
|
+
function assertForeignKeys(objects, keys) {
|
|
670
|
+
for (const object of objects) {
|
|
671
|
+
if (object.kind !== "constraint" || object.definition.constraintKind !== "foreignKey") {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const references = object.definition.references;
|
|
675
|
+
if (references === undefined) {
|
|
676
|
+
catalogError("OKM1021", `Foreign key ${object.identity.name} is missing its target.`);
|
|
677
|
+
}
|
|
678
|
+
const tableKey = identityKey({
|
|
679
|
+
kind: "table",
|
|
680
|
+
namespace: references.parent.namespace,
|
|
681
|
+
name: references.parent.name
|
|
682
|
+
});
|
|
683
|
+
if (!keys.has(tableKey)) {
|
|
684
|
+
catalogError("OKM1021", `Foreign key ${object.identity.name} references missing table ${references.parent.name}.`);
|
|
685
|
+
}
|
|
686
|
+
for (const name of references.columns) {
|
|
687
|
+
const columnKey = identityKey({ kind: "column", parent: references.parent, name });
|
|
688
|
+
if (!keys.has(columnKey)) {
|
|
689
|
+
catalogError("OKM1021", `Foreign key ${object.identity.name} references missing column ${name}.`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// src/contracts/location.ts
|
|
696
|
+
function callerLocation(skip) {
|
|
697
|
+
const lines = new Error().stack?.split(`
|
|
698
|
+
`);
|
|
699
|
+
if (lines === undefined)
|
|
700
|
+
return;
|
|
701
|
+
return locationFromLines(lines, skip);
|
|
702
|
+
}
|
|
703
|
+
function locationFromLines(lines, skip) {
|
|
704
|
+
let left = skip;
|
|
705
|
+
for (const line of lines) {
|
|
706
|
+
const slash = Math.max(line.lastIndexOf("/"), line.lastIndexOf("\\"));
|
|
707
|
+
if (slash < 0)
|
|
708
|
+
continue;
|
|
709
|
+
const tail = line.slice(slash + 1);
|
|
710
|
+
const colon = tail.indexOf(":");
|
|
711
|
+
if (colon < 1 || tail.startsWith("location."))
|
|
712
|
+
continue;
|
|
713
|
+
if (left > 0) {
|
|
714
|
+
left -= 1;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const end = tail.indexOf(":", colon + 1);
|
|
718
|
+
if (end < 0)
|
|
719
|
+
return;
|
|
720
|
+
return `${tail.slice(0, colon)}:${tail.slice(colon + 1, end)}`;
|
|
721
|
+
}
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
function withLocation(message, source) {
|
|
725
|
+
return source === undefined ? message : `${message} (${source})`;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/dialects/pg/compile.ts
|
|
729
|
+
function compileColumn(builder, input) {
|
|
730
|
+
const state = builder.state;
|
|
731
|
+
const name = state.sqlName ?? input.name;
|
|
732
|
+
const dependencies = [];
|
|
733
|
+
if (state.extension !== undefined) {
|
|
734
|
+
assertIdentifier(state.extension, "extension");
|
|
735
|
+
dependencies.push({ kind: "extension", name: state.extension });
|
|
736
|
+
}
|
|
737
|
+
if (state.typeDependency !== undefined) {
|
|
738
|
+
assertIdentifier(state.typeDependency, "type");
|
|
739
|
+
dependencies.push({
|
|
740
|
+
kind: "type",
|
|
741
|
+
namespace: input.parent.namespace,
|
|
742
|
+
name: state.typeDependency
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (state.renamedFrom !== undefined) {
|
|
746
|
+
assertIdentifier(state.renamedFrom, "renamed column");
|
|
747
|
+
}
|
|
748
|
+
if (state.comment !== undefined) {
|
|
749
|
+
assertStoredText(state.comment, "column comment");
|
|
750
|
+
}
|
|
751
|
+
if (state.picklist !== undefined) {
|
|
752
|
+
for (const value of state.picklist.values) {
|
|
753
|
+
assertStoredText(value, "picklist value");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const built = column({
|
|
757
|
+
parent: input.parent,
|
|
758
|
+
name,
|
|
759
|
+
dataType: formatType(state.baseType, state.dims),
|
|
760
|
+
nullable: state.nullable,
|
|
761
|
+
provenance: input.provenance,
|
|
762
|
+
...input.owner !== undefined ? { owner: input.owner } : {},
|
|
763
|
+
...state.defaultSql !== undefined ? { defaultExpression: state.defaultSql } : {},
|
|
764
|
+
...state.collation !== undefined ? { collation: state.collation } : {},
|
|
765
|
+
...state.identity !== undefined ? { identity: state.identity } : {},
|
|
766
|
+
...state.generated !== undefined ? { generated: state.generated } : {},
|
|
767
|
+
...dependencies.length > 0 || (input.dependencies?.length ?? 0) > 0 ? { dependencies: [...dependencies, ...input.dependencies ?? []] } : {}
|
|
768
|
+
});
|
|
769
|
+
const check = state.picklist !== undefined && state.picklist.check ? constraint({
|
|
770
|
+
parent: input.parent,
|
|
771
|
+
constraintKind: "check",
|
|
772
|
+
columns: [name],
|
|
773
|
+
nameKey: name,
|
|
774
|
+
expression: picklistExpression(name, state.picklist.values),
|
|
775
|
+
provenance: input.provenance,
|
|
776
|
+
...input.owner !== undefined ? { owner: input.owner } : {}
|
|
777
|
+
}) : undefined;
|
|
778
|
+
const unique = state.unique !== undefined ? constraint({
|
|
779
|
+
parent: input.parent,
|
|
780
|
+
constraintKind: "unique",
|
|
781
|
+
columns: [name],
|
|
782
|
+
nameKey: name,
|
|
783
|
+
provenance: input.provenance,
|
|
784
|
+
...input.owner !== undefined ? { owner: input.owner } : {}
|
|
785
|
+
}) : undefined;
|
|
786
|
+
return {
|
|
787
|
+
column: built,
|
|
788
|
+
guarded: state.guarded,
|
|
789
|
+
hidden: state.hidden,
|
|
790
|
+
...check !== undefined ? { check } : {},
|
|
791
|
+
...unique !== undefined ? { unique } : {},
|
|
792
|
+
...state.renamedFrom !== undefined ? { renamedFrom: state.renamedFrom } : {},
|
|
793
|
+
...state.comment !== undefined ? { comment: state.comment } : {},
|
|
794
|
+
...state.unique?.reason !== undefined ? { uniqueReason: state.unique.reason } : {},
|
|
795
|
+
...state.unique?.global !== undefined ? { uniqueGlobal: state.unique.global } : {},
|
|
796
|
+
...state.domain !== undefined ? { domain: state.domain } : {}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function picklistExpression(name, values) {
|
|
800
|
+
const list = values.map((value) => quoteLiteral(value)).join(", ");
|
|
801
|
+
return `(${name} IN (${list}))`;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/dialects/pg/relations.ts
|
|
805
|
+
function isRelationCall(value) {
|
|
806
|
+
if (typeof value !== "object" || value === null)
|
|
807
|
+
return false;
|
|
808
|
+
if (!("kind" in value) || !("table" in value))
|
|
809
|
+
return false;
|
|
810
|
+
const kind = value.kind;
|
|
811
|
+
return (kind === "one" || kind === "many") && typeof value.table === "string";
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/dialects/pg/text.ts
|
|
815
|
+
function text() {
|
|
816
|
+
return required({ baseType: "text", encode: encodeText, decode: decodeText, sqlForm: "quote" });
|
|
817
|
+
}
|
|
818
|
+
function varchar(length) {
|
|
819
|
+
const limit = boundedLength(length, "varchar");
|
|
820
|
+
return required({
|
|
821
|
+
baseType: `varchar(${String(limit)})`,
|
|
822
|
+
encode: (value) => encodeLimited(value, limit, "varchar"),
|
|
823
|
+
decode: decodeText,
|
|
824
|
+
sqlForm: "quote"
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
function char(length) {
|
|
828
|
+
const limit = boundedLength(length, "char");
|
|
829
|
+
return required({
|
|
830
|
+
baseType: `char(${String(limit)})`,
|
|
831
|
+
encode: (value) => encodeLimited(value, limit, "char"),
|
|
832
|
+
decode: decodeText,
|
|
833
|
+
sqlForm: "quote"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function citext() {
|
|
837
|
+
return required({
|
|
838
|
+
baseType: "citext",
|
|
839
|
+
encode: encodeText,
|
|
840
|
+
decode: decodeText,
|
|
841
|
+
sqlForm: "quote",
|
|
842
|
+
extension: "citext"
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function encodeText(value) {
|
|
846
|
+
return value;
|
|
847
|
+
}
|
|
848
|
+
function decodeText(wire) {
|
|
849
|
+
return wire;
|
|
850
|
+
}
|
|
851
|
+
function encodeLimited(value, limit, role) {
|
|
852
|
+
if (characterLength(value) > limit) {
|
|
853
|
+
rejected(`${role} accepts at most ${String(limit)} characters.`);
|
|
854
|
+
}
|
|
855
|
+
return value;
|
|
856
|
+
}
|
|
857
|
+
function boundedLength(length, role) {
|
|
858
|
+
if (!Number.isInteger(length) || length < 1 || length > 10485760) {
|
|
859
|
+
definition(`${role} length ${String(length)} must be an integer from 1 to 10485760.`);
|
|
860
|
+
}
|
|
861
|
+
return length;
|
|
862
|
+
}
|
|
863
|
+
function characterLength(value) {
|
|
864
|
+
let count = 0;
|
|
865
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
866
|
+
const code = value.charCodeAt(index);
|
|
867
|
+
if (code >= 55296 && code <= 56319) {
|
|
868
|
+
index += 1;
|
|
869
|
+
}
|
|
870
|
+
count += 1;
|
|
871
|
+
}
|
|
872
|
+
return count;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// src/dialects/pg/tenancy.ts
|
|
876
|
+
function readTenancy(value) {
|
|
877
|
+
if (value === undefined)
|
|
878
|
+
return;
|
|
879
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
880
|
+
definition("schema() tenancy must be columnTenancy() from okmodel/tenancy.");
|
|
881
|
+
}
|
|
882
|
+
const record = value;
|
|
883
|
+
if (typeof record.rewrite !== "function") {
|
|
884
|
+
definition("schema() tenancy must be columnTenancy() from okmodel/tenancy.");
|
|
885
|
+
}
|
|
886
|
+
return value;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// src/dialects/pg/filters.ts
|
|
890
|
+
function openFilters(table, relations, spec, hiddenFields) {
|
|
891
|
+
const allow = spec.allow ?? {};
|
|
892
|
+
const sort = spec.sort ?? [];
|
|
893
|
+
for (const field of Object.keys(allow))
|
|
894
|
+
deny(hiddenFields, table, field);
|
|
895
|
+
for (const field of sort)
|
|
896
|
+
deny(hiddenFields, table, field);
|
|
897
|
+
const nested = spec.relations;
|
|
898
|
+
if (nested !== undefined) {
|
|
899
|
+
for (const name of Object.keys(nested)) {
|
|
900
|
+
const target = relationTable(relations, name);
|
|
901
|
+
for (const field of nested[name] ?? [])
|
|
902
|
+
deny(hiddenFields, target, field);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
parse(query) {
|
|
907
|
+
return import("./shared/zs9aymcp.js").then((mod) => mod.parseQuery(allow, sort, query, nested));
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
function deny(hiddenFields, table, field) {
|
|
912
|
+
if (!hiddenFields.has(`${table}.${field}`))
|
|
913
|
+
return;
|
|
914
|
+
throw new OkmError("OKM1123", `Field ${table}.${field} is hidden.`, {
|
|
915
|
+
fix: {
|
|
916
|
+
summary: "Remove this hidden field from the allowlist. Hidden fields stay out of caller filter or sort."
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function relationTable(relations, name) {
|
|
921
|
+
if (typeof relations !== "object" || relations === null)
|
|
922
|
+
return name;
|
|
923
|
+
const call = relations[name];
|
|
924
|
+
if (typeof call !== "object" || call === null || !("table" in call))
|
|
925
|
+
return name;
|
|
926
|
+
const table = call.table;
|
|
927
|
+
return typeof table === "string" ? table : name;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// src/dialects/pg/table.ts
|
|
931
|
+
var TABLE_KNOWN = new Set([
|
|
932
|
+
"checks",
|
|
933
|
+
"comment",
|
|
934
|
+
"indexes",
|
|
935
|
+
"omitDefaults",
|
|
936
|
+
"presets",
|
|
937
|
+
"primaryKey",
|
|
938
|
+
"reference",
|
|
939
|
+
"relations",
|
|
940
|
+
"renamedFrom",
|
|
941
|
+
"sqlName",
|
|
942
|
+
"tenancy",
|
|
943
|
+
"traits",
|
|
944
|
+
"unique",
|
|
945
|
+
"validate",
|
|
946
|
+
"validation"
|
|
947
|
+
]);
|
|
948
|
+
var TABLE_LATER = {
|
|
949
|
+
computed: "later",
|
|
950
|
+
policies: "later"
|
|
951
|
+
};
|
|
952
|
+
function table2(name, columns, options) {
|
|
953
|
+
const source = callerLocation(1);
|
|
954
|
+
if (name.length === 0) {
|
|
955
|
+
definition(withLocation("table() needs a name.", source));
|
|
956
|
+
}
|
|
957
|
+
if (options !== undefined) {
|
|
958
|
+
rejectLater(options, TABLE_KNOWN, TABLE_LATER, `Table ${name}`);
|
|
959
|
+
}
|
|
960
|
+
remember(name, columns);
|
|
961
|
+
return {
|
|
962
|
+
name,
|
|
963
|
+
columns,
|
|
964
|
+
filters(spec) {
|
|
965
|
+
return openFilters(name, options?.relations, spec, hiddenFields);
|
|
966
|
+
},
|
|
967
|
+
...options !== undefined ? { options } : {},
|
|
968
|
+
...source !== undefined ? { source } : {}
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
function snakeCase(name) {
|
|
972
|
+
let out = "";
|
|
973
|
+
for (let index = 0;index < name.length; index += 1) {
|
|
974
|
+
const char = name[index] ?? "";
|
|
975
|
+
const lower = char.toLowerCase();
|
|
976
|
+
if (lower !== char && char.toUpperCase() === char && out.length > 0 && !out.endsWith("_")) {
|
|
977
|
+
out += "_";
|
|
978
|
+
}
|
|
979
|
+
out += lower;
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
function emittedTypeName(name) {
|
|
984
|
+
let out = "";
|
|
985
|
+
let capitalize = true;
|
|
986
|
+
for (const char of name) {
|
|
987
|
+
const letter = char >= "a" && char <= "z" || char >= "A" && char <= "Z";
|
|
988
|
+
const digit = char >= "0" && char <= "9";
|
|
989
|
+
if (!letter && !digit) {
|
|
990
|
+
capitalize = true;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (capitalize && letter) {
|
|
994
|
+
out += char.toUpperCase();
|
|
995
|
+
capitalize = false;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
out += char;
|
|
999
|
+
capitalize = false;
|
|
1000
|
+
}
|
|
1001
|
+
if (out.length === 0) {
|
|
1002
|
+
return "Table";
|
|
1003
|
+
}
|
|
1004
|
+
const first = out[0] ?? "";
|
|
1005
|
+
if (first >= "0" && first <= "9") {
|
|
1006
|
+
return `T${out}`;
|
|
1007
|
+
}
|
|
1008
|
+
return out;
|
|
1009
|
+
}
|
|
1010
|
+
function rejectLater(options, known, later, where) {
|
|
1011
|
+
const record = options;
|
|
1012
|
+
for (const key of Object.keys(record)) {
|
|
1013
|
+
const prompt = later[key];
|
|
1014
|
+
if (prompt !== undefined) {
|
|
1015
|
+
if (record[key] !== undefined) {
|
|
1016
|
+
const when = prompt === "later" ? "later" : `in ${prompt}`;
|
|
1017
|
+
unavailable(`${where} option ${key} is not available yet. It arrives ${when}.`);
|
|
1018
|
+
}
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (!known.has(key)) {
|
|
1022
|
+
const accepted = [...known].sort().join(", ");
|
|
1023
|
+
const candidates = [...known, ...Object.keys(later)];
|
|
1024
|
+
throwNamed("OKM1060", key, candidates, `${where} option ${key} is not supported. Accepted options: ${accepted}.`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
var hiddenFields = new Set;
|
|
1029
|
+
function remember(name, columns) {
|
|
1030
|
+
for (const field of Object.keys(columns)) {
|
|
1031
|
+
const column = columns[field];
|
|
1032
|
+
if (column?.state?.hidden === true)
|
|
1033
|
+
hiddenFields.add(`${name}.${field}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/dialects/pg/schema.ts
|
|
1038
|
+
var SCHEMA_KNOWN = new Set([
|
|
1039
|
+
"casing",
|
|
1040
|
+
"codecs",
|
|
1041
|
+
"defaults",
|
|
1042
|
+
"extensions",
|
|
1043
|
+
"functions",
|
|
1044
|
+
"minPostgres",
|
|
1045
|
+
"requires",
|
|
1046
|
+
"tables",
|
|
1047
|
+
"tenancy",
|
|
1048
|
+
"traits",
|
|
1049
|
+
"triggers",
|
|
1050
|
+
"types",
|
|
1051
|
+
"validation",
|
|
1052
|
+
"views"
|
|
1053
|
+
]);
|
|
1054
|
+
var SCHEMA_LATER = {};
|
|
1055
|
+
var BIGINT_CODECS = new Set(["string", "number", "bigint"]);
|
|
1056
|
+
var NUMERIC_CODECS = new Set(["string", "number"]);
|
|
1057
|
+
function stage(jobs, build) {
|
|
1058
|
+
jobs.push((objects) => {
|
|
1059
|
+
objects.push(build());
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
function schema(config) {
|
|
1063
|
+
rejectLater(config, SCHEMA_KNOWN, SCHEMA_LATER, "schema()");
|
|
1064
|
+
const schemaTraits = openSchemaTraits(config.traits);
|
|
1065
|
+
const tenancy = readTenancy(config.tenancy);
|
|
1066
|
+
if (!Array.isArray(config.tables)) {
|
|
1067
|
+
definition("schema() needs a tables array.");
|
|
1068
|
+
}
|
|
1069
|
+
const casing = readCasing(config.casing);
|
|
1070
|
+
const tables = rewritePass(config.tables, tenancy, schemaTraits, casing);
|
|
1071
|
+
const hooks = collectHooks(tenancy, schemaTraits, config.tables, config.views);
|
|
1072
|
+
const types = readTypes(config.types);
|
|
1073
|
+
const codecs = readCodecs(config.codecs);
|
|
1074
|
+
const requires = readFloor(config.minPostgres, config.requires);
|
|
1075
|
+
const idDefault = readIdDefault(config.defaults);
|
|
1076
|
+
const namespace = staticNamespace("public");
|
|
1077
|
+
const accepted = acceptTables(config.tables);
|
|
1078
|
+
const jobs = [];
|
|
1079
|
+
const prepared = [];
|
|
1080
|
+
const sqlNames = new Set;
|
|
1081
|
+
const enums = new Map;
|
|
1082
|
+
for (const item of tables) {
|
|
1083
|
+
const built = compileTable(item, namespace, casing, codecs, requires, idDefault, jobs, enums, schemaTraits);
|
|
1084
|
+
if (sqlNames.has(built.sqlName)) {
|
|
1085
|
+
catalogError("OKM1023", `Table ${built.tsName} uses SQL name ${built.sqlName}, which another table already uses. Accepted names: ${list(accepted)}.`);
|
|
1086
|
+
}
|
|
1087
|
+
sqlNames.add(built.sqlName);
|
|
1088
|
+
prepared.push(built);
|
|
1089
|
+
}
|
|
1090
|
+
const byName = new Map;
|
|
1091
|
+
for (const item of prepared) {
|
|
1092
|
+
byName.set(item.tsName, item);
|
|
1093
|
+
}
|
|
1094
|
+
const edges = [];
|
|
1095
|
+
for (const item of prepared) {
|
|
1096
|
+
compileForeignKeys(item, byName, accepted, jobs, edges);
|
|
1097
|
+
}
|
|
1098
|
+
const model = {};
|
|
1099
|
+
const scopeFields = tenancy === undefined ? undefined : tenancy.keys !== undefined ? tenancy.keys : [tenancy.key];
|
|
1100
|
+
for (const item of prepared) {
|
|
1101
|
+
model[item.tsName] = tableModel(item, edges, byName, accepted, scopeFields);
|
|
1102
|
+
}
|
|
1103
|
+
bindViews(config.views, model, tenancy, tables, casing);
|
|
1104
|
+
let document;
|
|
1105
|
+
return {
|
|
1106
|
+
get catalog() {
|
|
1107
|
+
if (document === undefined) {
|
|
1108
|
+
const objects = [];
|
|
1109
|
+
for (const job of jobs)
|
|
1110
|
+
job(objects);
|
|
1111
|
+
for (const [name, note] of [...enums.entries()].sort((left, right) => compareText(left[0], right[0]))) {
|
|
1112
|
+
objects.push(enumType({
|
|
1113
|
+
namespace,
|
|
1114
|
+
name,
|
|
1115
|
+
labels: note.labels,
|
|
1116
|
+
provenance: note.provenance
|
|
1117
|
+
}));
|
|
1118
|
+
}
|
|
1119
|
+
contributeListed(config.extensions, objects);
|
|
1120
|
+
contributeListed(config.functions, objects);
|
|
1121
|
+
contributeListed(schemaTraits, objects);
|
|
1122
|
+
for (const item of config.tables) {
|
|
1123
|
+
contributeListed(item.options?.traits, objects);
|
|
1124
|
+
}
|
|
1125
|
+
contributeListed(config.triggers, objects);
|
|
1126
|
+
contributeListed(config.views, objects);
|
|
1127
|
+
document = catalog(objects);
|
|
1128
|
+
}
|
|
1129
|
+
return document;
|
|
1130
|
+
},
|
|
1131
|
+
types,
|
|
1132
|
+
casing,
|
|
1133
|
+
codecs,
|
|
1134
|
+
requires,
|
|
1135
|
+
tables: config.tables,
|
|
1136
|
+
model,
|
|
1137
|
+
...schemaTraits !== undefined && schemaTraits.length > 0 ? { traits: schemaTraits } : {},
|
|
1138
|
+
...tenancy !== undefined ? { tenancy } : {},
|
|
1139
|
+
...hooks !== undefined ? { hooks } : {},
|
|
1140
|
+
...config.validation !== undefined ? { validation: config.validation } : {}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function acceptTables(tables) {
|
|
1144
|
+
const names = [];
|
|
1145
|
+
const seen = new Set;
|
|
1146
|
+
const emitted = new Map;
|
|
1147
|
+
for (const item of tables) {
|
|
1148
|
+
if (typeof item.name !== "string" || item.name.length === 0) {
|
|
1149
|
+
definition("schema() tables need a name.");
|
|
1150
|
+
}
|
|
1151
|
+
if (seen.has(item.name)) {
|
|
1152
|
+
catalogError("OKM1023", withLocation(`Table ${item.name} is declared more than once. Accepted names: ${list([...names].sort())}.`, item.source));
|
|
1153
|
+
}
|
|
1154
|
+
seen.add(item.name);
|
|
1155
|
+
names.push(item.name);
|
|
1156
|
+
const typeName = emittedTypeName(item.name);
|
|
1157
|
+
const previous = emitted.get(typeName);
|
|
1158
|
+
if (previous !== undefined) {
|
|
1159
|
+
catalogError("OKM1023", `Table ${item.name} and ${previous} both emit ${typeName}. Accepted names: ${list([...names].sort())}.`);
|
|
1160
|
+
}
|
|
1161
|
+
emitted.set(typeName, item.name);
|
|
1162
|
+
}
|
|
1163
|
+
names.sort();
|
|
1164
|
+
return names;
|
|
1165
|
+
}
|
|
1166
|
+
function noteEnum(notes, labels, name, table, provenance) {
|
|
1167
|
+
const previous = notes.get(name);
|
|
1168
|
+
if (previous !== undefined && !sameEnumLabels(previous.labels, labels)) {
|
|
1169
|
+
catalogError("OKM1020", `Enum ${name} lists ${previous.labels.join(", ")} on ${previous.table} and ${labels.join(", ")} on ${table}. One enum has one label list.`);
|
|
1170
|
+
}
|
|
1171
|
+
if (previous === undefined || compareText(table, previous.table) < 0) {
|
|
1172
|
+
notes.set(name, { labels, table, provenance });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function rewritePass(tables, tenancy, schemaTraits, casing) {
|
|
1176
|
+
let next = tenancy === undefined ? bareTables(tables) : tenancy.rewrite(tables);
|
|
1177
|
+
const seen = new Set;
|
|
1178
|
+
const pending = [];
|
|
1179
|
+
const add = (value) => {
|
|
1180
|
+
if (seen.has(value))
|
|
1181
|
+
return;
|
|
1182
|
+
if (typeof value.rewrite !== "function")
|
|
1183
|
+
return;
|
|
1184
|
+
seen.add(value);
|
|
1185
|
+
pending.push(value);
|
|
1186
|
+
};
|
|
1187
|
+
if (schemaTraits !== undefined) {
|
|
1188
|
+
for (const trait of schemaTraits)
|
|
1189
|
+
add(trait);
|
|
1190
|
+
}
|
|
1191
|
+
for (const item of tables) {
|
|
1192
|
+
const own = item.options?.traits;
|
|
1193
|
+
if (!Array.isArray(own))
|
|
1194
|
+
continue;
|
|
1195
|
+
for (const trait of own) {
|
|
1196
|
+
if (typeof trait === "object" && trait !== null)
|
|
1197
|
+
add(trait);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
for (const trait of pending) {
|
|
1201
|
+
const rewrite = trait.rewrite;
|
|
1202
|
+
next = rewrite.call(trait, next, casing, schemaTraits);
|
|
1203
|
+
}
|
|
1204
|
+
return next;
|
|
1205
|
+
}
|
|
1206
|
+
var registeredHooks = [];
|
|
1207
|
+
function collectHooks(tenancy, schemaTraits, tables, views) {
|
|
1208
|
+
const found = [];
|
|
1209
|
+
const seen = new Set;
|
|
1210
|
+
const add = (value) => {
|
|
1211
|
+
if (seen.has(value))
|
|
1212
|
+
return;
|
|
1213
|
+
const hook = value.hook;
|
|
1214
|
+
if (typeof hook !== "function")
|
|
1215
|
+
return;
|
|
1216
|
+
seen.add(value);
|
|
1217
|
+
found.push(hook.bind(value));
|
|
1218
|
+
};
|
|
1219
|
+
if (Array.isArray(views)) {
|
|
1220
|
+
for (const item of views) {
|
|
1221
|
+
if (typeof item === "object" && item !== null)
|
|
1222
|
+
add(item);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (tenancy !== undefined)
|
|
1226
|
+
add(tenancy);
|
|
1227
|
+
if (schemaTraits !== undefined) {
|
|
1228
|
+
for (const trait of schemaTraits)
|
|
1229
|
+
add(trait);
|
|
1230
|
+
}
|
|
1231
|
+
for (const item of tables) {
|
|
1232
|
+
const own = item.options?.traits;
|
|
1233
|
+
if (!Array.isArray(own))
|
|
1234
|
+
continue;
|
|
1235
|
+
for (const trait of own) {
|
|
1236
|
+
if (typeof trait === "object" && trait !== null)
|
|
1237
|
+
add(trait);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
for (const hook of registeredHooks)
|
|
1241
|
+
found.push(hook);
|
|
1242
|
+
return found.length === 0 ? undefined : found;
|
|
1243
|
+
}
|
|
1244
|
+
function bindViews(views, model, tenancy, tables, casing) {
|
|
1245
|
+
if (!Array.isArray(views))
|
|
1246
|
+
return;
|
|
1247
|
+
const scoped = new Set;
|
|
1248
|
+
for (const item of views) {
|
|
1249
|
+
if (typeof item !== "object" || item === null)
|
|
1250
|
+
continue;
|
|
1251
|
+
const install = item.install;
|
|
1252
|
+
install?.call(item, model, tenancy, tables, casing, scoped);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
function archiveModel(item) {
|
|
1256
|
+
const archive = item.archiveModel;
|
|
1257
|
+
return archive === undefined ? undefined : { archive };
|
|
1258
|
+
}
|
|
1259
|
+
function bareTables(tables) {
|
|
1260
|
+
for (const item of tables) {
|
|
1261
|
+
const mark = item.options?.tenancy;
|
|
1262
|
+
if (mark !== undefined) {
|
|
1263
|
+
definition(`Table ${item.name} sets tenancy, and the schema does not.`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return tables;
|
|
1267
|
+
}
|
|
1268
|
+
function contributeListed(value, objects) {
|
|
1269
|
+
if (!Array.isArray(value))
|
|
1270
|
+
return;
|
|
1271
|
+
for (const item of value) {
|
|
1272
|
+
const produced = item.contribute(value, objects);
|
|
1273
|
+
if (Array.isArray(produced))
|
|
1274
|
+
for (const object of produced)
|
|
1275
|
+
objects.push(object);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
function openSchemaTraits(value) {
|
|
1279
|
+
if (value === undefined)
|
|
1280
|
+
return;
|
|
1281
|
+
if (!Array.isArray(value))
|
|
1282
|
+
definition("schema() traits must be a list of traits.");
|
|
1283
|
+
return value.length === 0 ? undefined : value;
|
|
1284
|
+
}
|
|
1285
|
+
function openTableTraits(table, columns, schemaTraits, options) {
|
|
1286
|
+
let shared = schemaTraits;
|
|
1287
|
+
const omit = options?.omitDefaults;
|
|
1288
|
+
if (omit !== undefined) {
|
|
1289
|
+
if (typeof omit !== "string" || omit.trim().length === 0) {
|
|
1290
|
+
definition(`Table ${table} omitDefaults needs a reason.`);
|
|
1291
|
+
}
|
|
1292
|
+
shared = undefined;
|
|
1293
|
+
}
|
|
1294
|
+
const own = options?.traits;
|
|
1295
|
+
if (own !== undefined && !Array.isArray(own)) {
|
|
1296
|
+
definition(`Table ${table} traits must be a list of traits.`);
|
|
1297
|
+
}
|
|
1298
|
+
const local = Array.isArray(own) && own.length > 0 ? own : undefined;
|
|
1299
|
+
if ((shared === undefined || shared.length === 0) && local === undefined)
|
|
1300
|
+
return;
|
|
1301
|
+
const traits = local === undefined ? shared : shared === undefined || shared.length === 0 ? local : [...shared, ...local];
|
|
1302
|
+
if (traits === undefined)
|
|
1303
|
+
return;
|
|
1304
|
+
const model = { columns: { ...columns }, presets: options?.presets };
|
|
1305
|
+
for (const trait of traits)
|
|
1306
|
+
trait.apply(model, { table });
|
|
1307
|
+
return { ...model, traits };
|
|
1308
|
+
}
|
|
1309
|
+
function compileTable(item, namespace, casing, codecs, requires, idDefault, jobs, enums, schemaTraits) {
|
|
1310
|
+
const options = item.options;
|
|
1311
|
+
const applied = openTableTraits(item.name, item.columns, schemaTraits, item.options);
|
|
1312
|
+
readTableNames(item.name, options);
|
|
1313
|
+
const sqlName = options?.sqlName ?? (casing === "snake" ? snakeCase(item.name) : item.name);
|
|
1314
|
+
assertIdentifier(sqlName, `table ${item.name}`);
|
|
1315
|
+
const provenance = item.source === undefined ? { origin: "file", name: item.name } : { origin: "file", name: item.name, source: item.source };
|
|
1316
|
+
const parent = { namespace, name: sqlName };
|
|
1317
|
+
stage(jobs, () => table({ namespace, name: sqlName, provenance }));
|
|
1318
|
+
const columns = [];
|
|
1319
|
+
const columnPrimary = [];
|
|
1320
|
+
const byField = new Map;
|
|
1321
|
+
const bySql = new Map;
|
|
1322
|
+
const handles = {};
|
|
1323
|
+
const sourceColumns = applied === undefined ? item.columns : applied.columns;
|
|
1324
|
+
for (const [field, builder] of Object.entries(sourceColumns)) {
|
|
1325
|
+
if (!(builder instanceof ColumnBuilder)) {
|
|
1326
|
+
definition(`Column ${item.name}.${field} must be a column builder.`);
|
|
1327
|
+
}
|
|
1328
|
+
const column = applySchemaId(builder, idDefault);
|
|
1329
|
+
const columnSql = column.state.sqlName ?? (casing === "snake" ? snakeCase(field) : field);
|
|
1330
|
+
assertCodec(item.name, field, column, codecs);
|
|
1331
|
+
const mark = applied !== undefined ? builder.trait : undefined;
|
|
1332
|
+
const traitName = typeof mark === "string" ? mark : undefined;
|
|
1333
|
+
const columnProvenance = traitName === undefined ? provenance : {
|
|
1334
|
+
origin: "trait",
|
|
1335
|
+
name: traitName,
|
|
1336
|
+
...provenance.source !== undefined ? { source: provenance.source } : {}
|
|
1337
|
+
};
|
|
1338
|
+
const identity = column.state.identity;
|
|
1339
|
+
if (identity !== undefined) {
|
|
1340
|
+
const seqName = fitIdentifier(`${sqlName}_${columnSql}_seq`);
|
|
1341
|
+
jobs.push((objects) => {
|
|
1342
|
+
const seq = sequence({
|
|
1343
|
+
namespace,
|
|
1344
|
+
name: seqName,
|
|
1345
|
+
dataType: "bigint",
|
|
1346
|
+
provenance,
|
|
1347
|
+
dependencies: [{ kind: "table", namespace, name: sqlName }]
|
|
1348
|
+
});
|
|
1349
|
+
objects.push(seq);
|
|
1350
|
+
pushCompiled(objects, column, {
|
|
1351
|
+
parent,
|
|
1352
|
+
name: columnSql,
|
|
1353
|
+
provenance: columnProvenance,
|
|
1354
|
+
dependencies: [seq.identity]
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
} else {
|
|
1358
|
+
jobs.push((objects) => {
|
|
1359
|
+
pushCompiled(objects, column, {
|
|
1360
|
+
parent,
|
|
1361
|
+
name: columnSql,
|
|
1362
|
+
provenance: columnProvenance
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
const decode = column.state.decode;
|
|
1367
|
+
const prepared = {
|
|
1368
|
+
field,
|
|
1369
|
+
sqlName: columnSql,
|
|
1370
|
+
dataType: formatType(column.state.baseType, column.state.dims),
|
|
1371
|
+
nullable: column.state.nullable,
|
|
1372
|
+
references: column.state.references,
|
|
1373
|
+
encode: column.state.encode,
|
|
1374
|
+
decode: decode === decodeText ? undefined : decode,
|
|
1375
|
+
hidden: column.state.filterable === true ? "filter" : column.state.hidden,
|
|
1376
|
+
sensitive: column.state.sensitive,
|
|
1377
|
+
guarded: column.state.guarded,
|
|
1378
|
+
writable: column.state.guarded !== true && column.state.omitWrite !== true && column.state.generated === undefined,
|
|
1379
|
+
unique: column.state.unique !== undefined,
|
|
1380
|
+
clientDefault: column.state.clientDefault === undefined ? undefined : "client",
|
|
1381
|
+
elementEncode: column.state.dims > 0 ? column.state.elementEncode : undefined,
|
|
1382
|
+
fill: column.state.clientDefault,
|
|
1383
|
+
accepts: column.state.accepts
|
|
1384
|
+
};
|
|
1385
|
+
columns.push(prepared);
|
|
1386
|
+
byField.set(field, prepared);
|
|
1387
|
+
bySql.set(prepared.sqlName, prepared);
|
|
1388
|
+
handles[field] = { name: prepared.sqlName };
|
|
1389
|
+
if (column.state.primaryKey) {
|
|
1390
|
+
if (column.state.nullable) {
|
|
1391
|
+
catalogError("OKM1020", `Primary key ${item.name}.${field} is nullable.`);
|
|
1392
|
+
}
|
|
1393
|
+
columnPrimary.push(prepared.sqlName);
|
|
1394
|
+
}
|
|
1395
|
+
if (column.state.defaultSql === "uuidv7()") {
|
|
1396
|
+
refuseUuidV7(requires);
|
|
1397
|
+
}
|
|
1398
|
+
const labels = column.state.enumLabels;
|
|
1399
|
+
if (labels !== undefined) {
|
|
1400
|
+
noteEnum(enums, labels, column.state.typeDependency ?? prepared.sqlName, sqlName, provenance);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const optionPrimary = readPrimaryKey(item.name, options, byField);
|
|
1404
|
+
if (columnPrimary.length > 1) {
|
|
1405
|
+
catalogError("OKM1020", `Table ${item.name} has more than one primary key (${columnPrimary.join(", ")}). One primary key is accepted.`);
|
|
1406
|
+
}
|
|
1407
|
+
if (columnPrimary.length > 0 && optionPrimary.length > 0) {
|
|
1408
|
+
catalogError("OKM1020", `Table ${item.name} has two primary keys. One primary key is accepted.`);
|
|
1409
|
+
}
|
|
1410
|
+
const primary = optionPrimary.length > 0 ? optionPrimary : columnPrimary;
|
|
1411
|
+
if (primary.length > 0) {
|
|
1412
|
+
stage(jobs, () => constraint({
|
|
1413
|
+
parent,
|
|
1414
|
+
constraintKind: "primaryKey",
|
|
1415
|
+
columns: primary,
|
|
1416
|
+
provenance
|
|
1417
|
+
}));
|
|
1418
|
+
}
|
|
1419
|
+
if (options?.unique !== undefined) {
|
|
1420
|
+
compileUniques(item.name, options, parent, provenance, byField, jobs);
|
|
1421
|
+
}
|
|
1422
|
+
if (options?.indexes !== undefined) {
|
|
1423
|
+
compileIndexes(item.name, options, handles, parent, provenance, jobs);
|
|
1424
|
+
}
|
|
1425
|
+
if (options?.checks !== undefined) {
|
|
1426
|
+
compileChecks(item.name, options, handles, parent, provenance, byField, jobs);
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
tsName: item.name,
|
|
1430
|
+
sqlName,
|
|
1431
|
+
parent,
|
|
1432
|
+
provenance,
|
|
1433
|
+
columns,
|
|
1434
|
+
primary,
|
|
1435
|
+
uniques: uniqueTargets(primary, columns, bySql, options, handles),
|
|
1436
|
+
byField,
|
|
1437
|
+
bySql,
|
|
1438
|
+
relationOptions: relationInput(item.name, options),
|
|
1439
|
+
...applied !== undefined ? { traits: applied.traits } : {},
|
|
1440
|
+
presets: applied?.presets ?? options?.presets,
|
|
1441
|
+
...archiveModel(item)
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
function uniqueTargets(primary, columns, bySql, options, handles) {
|
|
1445
|
+
const uniques = [];
|
|
1446
|
+
if (primary.length > 0) {
|
|
1447
|
+
const fields = [];
|
|
1448
|
+
for (const sqlName of primary) {
|
|
1449
|
+
const column = bySql.get(sqlName);
|
|
1450
|
+
if (column !== undefined)
|
|
1451
|
+
fields.push(column.field);
|
|
1452
|
+
}
|
|
1453
|
+
if (fields.length > 0)
|
|
1454
|
+
uniques.push(fields);
|
|
1455
|
+
}
|
|
1456
|
+
for (const column of columns) {
|
|
1457
|
+
if (column.unique)
|
|
1458
|
+
uniques.push([column.field]);
|
|
1459
|
+
}
|
|
1460
|
+
const named = options?.unique;
|
|
1461
|
+
if (named !== undefined) {
|
|
1462
|
+
for (const fields of Object.values(named))
|
|
1463
|
+
uniques.push([...fields]);
|
|
1464
|
+
}
|
|
1465
|
+
const build = options?.indexes;
|
|
1466
|
+
if (build !== undefined) {
|
|
1467
|
+
const calls = build(handles);
|
|
1468
|
+
if (Array.isArray(calls)) {
|
|
1469
|
+
for (const call of calls) {
|
|
1470
|
+
if (call.isUnique !== true || call.predicate === undefined)
|
|
1471
|
+
continue;
|
|
1472
|
+
const fields = [];
|
|
1473
|
+
let whole = true;
|
|
1474
|
+
for (const sqlName of call.columns) {
|
|
1475
|
+
const column = bySql.get(sqlName);
|
|
1476
|
+
if (column === undefined) {
|
|
1477
|
+
whole = false;
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
fields.push(column.field);
|
|
1481
|
+
}
|
|
1482
|
+
if (whole && fields.length > 0)
|
|
1483
|
+
uniques.push(fields);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
return uniques;
|
|
1488
|
+
}
|
|
1489
|
+
function relationInput(tableName, options) {
|
|
1490
|
+
const relations = options?.relations;
|
|
1491
|
+
if (relations === undefined)
|
|
1492
|
+
return;
|
|
1493
|
+
if (typeof relations !== "object" || relations === null || Array.isArray(relations)) {
|
|
1494
|
+
definition(`Table ${tableName} relations must be an object of one() and many() calls.`);
|
|
1495
|
+
}
|
|
1496
|
+
return relations;
|
|
1497
|
+
}
|
|
1498
|
+
function pushCompiled(objects, column, input) {
|
|
1499
|
+
const compiled = compileColumn(column, input);
|
|
1500
|
+
objects.push(compiled.column);
|
|
1501
|
+
if (compiled.check !== undefined)
|
|
1502
|
+
objects.push(compiled.check);
|
|
1503
|
+
if (compiled.unique !== undefined)
|
|
1504
|
+
objects.push(compiled.unique);
|
|
1505
|
+
column.state.domain?.emit?.(input.parent.namespace, input.provenance, objects);
|
|
1506
|
+
}
|
|
1507
|
+
function compileUniques(tableName, options, parent, provenance, byField, jobs) {
|
|
1508
|
+
const unique = options?.unique;
|
|
1509
|
+
if (unique === undefined) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const accepted = [...byField.keys()].sort();
|
|
1513
|
+
for (const [name, fields] of Object.entries(unique)) {
|
|
1514
|
+
if (fields.length === 0) {
|
|
1515
|
+
definition(`Unique ${tableName}.${name} needs a column.`);
|
|
1516
|
+
}
|
|
1517
|
+
const columns = [];
|
|
1518
|
+
for (const field of fields) {
|
|
1519
|
+
const column = byField.get(field);
|
|
1520
|
+
if (column === undefined) {
|
|
1521
|
+
throwNamed("OKM1020", field, accepted, `Unique ${tableName}.${name} names ${field}, which is not a column of ${tableName}. Accepted names: ${list(accepted)}.`);
|
|
1522
|
+
}
|
|
1523
|
+
columns.push(column.sqlName);
|
|
1524
|
+
}
|
|
1525
|
+
stage(jobs, () => constraint({
|
|
1526
|
+
parent,
|
|
1527
|
+
constraintKind: "unique",
|
|
1528
|
+
columns,
|
|
1529
|
+
nameKey: name,
|
|
1530
|
+
provenance
|
|
1531
|
+
}));
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
function compileIndexes(tableName, options, handles, parent, provenance, jobs) {
|
|
1535
|
+
const build = options?.indexes;
|
|
1536
|
+
if (build === undefined) {
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const calls = build(handles);
|
|
1540
|
+
if (!Array.isArray(calls)) {
|
|
1541
|
+
definition(`Table ${tableName} indexes must return an array.`);
|
|
1542
|
+
}
|
|
1543
|
+
for (const call of calls) {
|
|
1544
|
+
const built = readIndex(tableName, call);
|
|
1545
|
+
stage(jobs, () => index({
|
|
1546
|
+
parent,
|
|
1547
|
+
columns: built.columns,
|
|
1548
|
+
unique: built.isUnique === true,
|
|
1549
|
+
nameKey: built.columns.join("_"),
|
|
1550
|
+
...built.predicate !== undefined ? { predicate: built.predicate } : {},
|
|
1551
|
+
...built.expression !== undefined ? { expression: built.expression } : {},
|
|
1552
|
+
provenance
|
|
1553
|
+
}));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function compileChecks(tableName, options, handles, parent, provenance, byField, jobs) {
|
|
1557
|
+
const checks = options?.checks;
|
|
1558
|
+
if (checks === undefined) {
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
for (const [name, build] of Object.entries(checks)) {
|
|
1562
|
+
const used = [];
|
|
1563
|
+
const value = build(track(handles, used));
|
|
1564
|
+
const text = readCheck(tableName, name, value);
|
|
1565
|
+
const columns = [];
|
|
1566
|
+
for (const field of used) {
|
|
1567
|
+
const column = byField.get(field);
|
|
1568
|
+
if (column !== undefined) {
|
|
1569
|
+
columns.push(column.sqlName);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
stage(jobs, () => constraint({
|
|
1573
|
+
parent,
|
|
1574
|
+
constraintKind: "check",
|
|
1575
|
+
columns,
|
|
1576
|
+
nameKey: name,
|
|
1577
|
+
expression: text,
|
|
1578
|
+
provenance
|
|
1579
|
+
}));
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function compileForeignKeys(item, byName, accepted, jobs, edges) {
|
|
1583
|
+
for (const column of item.columns) {
|
|
1584
|
+
const reference = column.references;
|
|
1585
|
+
if (reference === undefined) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const target = byName.get(reference.table);
|
|
1589
|
+
if (target === undefined) {
|
|
1590
|
+
throwNamed("OKM1020", reference.table, accepted, `Table ${reference.table} is not in the schema. Accepted names: ${list(accepted)}.`);
|
|
1591
|
+
}
|
|
1592
|
+
const localColumns = localKey(item, column, reference);
|
|
1593
|
+
const targetColumns = resolveTarget(item, column, target, reference);
|
|
1594
|
+
if (localColumns.length !== targetColumns.length) {
|
|
1595
|
+
catalogError("OKM1021", `Foreign key ${item.tsName}.${column.field} on ${target.tsName} has ${localColumns.length} local columns and ${targetColumns.length} target columns.`);
|
|
1596
|
+
}
|
|
1597
|
+
if (reference.onDelete === "set null" || reference.onUpdate === "set null") {
|
|
1598
|
+
for (const local of localColumns) {
|
|
1599
|
+
if (!local.nullable) {
|
|
1600
|
+
definition(`Foreign key ${item.tsName}.${column.field} uses set null. ${item.tsName}.${local.field} must be nullable.`);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const localNames = [];
|
|
1605
|
+
const remoteNames = [];
|
|
1606
|
+
for (let index = 0;index < localColumns.length; index += 1) {
|
|
1607
|
+
const local = localColumns[index];
|
|
1608
|
+
const remote = targetColumns[index];
|
|
1609
|
+
if (local === undefined || remote === undefined)
|
|
1610
|
+
continue;
|
|
1611
|
+
if (local.dataType !== remote.dataType) {
|
|
1612
|
+
catalogError("OKM1022", `Foreign key ${item.tsName}.${local.field} is ${local.dataType} and ${target.tsName}.${remote.field} is ${remote.dataType}. Accepted type: ${remote.dataType}.`);
|
|
1613
|
+
}
|
|
1614
|
+
localNames.push(local.sqlName);
|
|
1615
|
+
remoteNames.push(remote.sqlName);
|
|
1616
|
+
}
|
|
1617
|
+
stage(jobs, () => constraint({
|
|
1618
|
+
parent: item.parent,
|
|
1619
|
+
constraintKind: "foreignKey",
|
|
1620
|
+
columns: localNames,
|
|
1621
|
+
nameKey: column.sqlName,
|
|
1622
|
+
provenance: item.provenance,
|
|
1623
|
+
references: {
|
|
1624
|
+
parent: target.parent,
|
|
1625
|
+
columns: remoteNames,
|
|
1626
|
+
...reference.onDelete !== undefined ? { onDelete: reference.onDelete } : {},
|
|
1627
|
+
...reference.onUpdate !== undefined ? { onUpdate: reference.onUpdate } : {}
|
|
1628
|
+
}
|
|
1629
|
+
}));
|
|
1630
|
+
edges.push({
|
|
1631
|
+
fromTable: item.tsName,
|
|
1632
|
+
fromField: column.field,
|
|
1633
|
+
toTable: target.tsName,
|
|
1634
|
+
local: localNames,
|
|
1635
|
+
remote: remoteNames
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function localKey(item, column, reference) {
|
|
1640
|
+
const along = reference.along;
|
|
1641
|
+
if (along === undefined || along.length === 0)
|
|
1642
|
+
return [column];
|
|
1643
|
+
const accepted = [...item.byField.keys()].sort();
|
|
1644
|
+
const columns = [column];
|
|
1645
|
+
for (const name of along) {
|
|
1646
|
+
const extra = item.byField.get(name) ?? item.bySql.get(name);
|
|
1647
|
+
if (extra === undefined) {
|
|
1648
|
+
throwNamed("OKM1021", name, accepted, `Foreign key ${item.tsName}.${column.field} includes ${name}, which is not a column. Accepted columns: ${list(accepted)}.`);
|
|
1649
|
+
}
|
|
1650
|
+
columns.push(extra);
|
|
1651
|
+
}
|
|
1652
|
+
return columns;
|
|
1653
|
+
}
|
|
1654
|
+
function resolveTarget(item, column, target, reference) {
|
|
1655
|
+
const accepted = [...target.byField.keys()].sort();
|
|
1656
|
+
const named = reference.columns;
|
|
1657
|
+
if (named !== undefined) {
|
|
1658
|
+
const found = [];
|
|
1659
|
+
for (const name of named) {
|
|
1660
|
+
const match = target.byField.get(name) ?? target.bySql.get(name);
|
|
1661
|
+
if (match === undefined) {
|
|
1662
|
+
throwNamed("OKM1021", name, accepted, `Foreign key ${item.tsName}.${column.field} references ${name} on ${target.tsName}, which is not a column. Accepted columns: ${list(accepted)}.`);
|
|
1663
|
+
}
|
|
1664
|
+
found.push(match);
|
|
1665
|
+
}
|
|
1666
|
+
if (found.length !== 1 && reference.along === undefined) {
|
|
1667
|
+
catalogError("OKM1021", `Foreign key ${item.tsName}.${column.field} on ${target.tsName} is ambiguous. Accepted columns: ${list(accepted)}.`);
|
|
1668
|
+
}
|
|
1669
|
+
return found;
|
|
1670
|
+
}
|
|
1671
|
+
if (target.primary.length === 0) {
|
|
1672
|
+
catalogError("OKM1021", `Foreign key ${item.tsName}.${column.field} on ${target.tsName} has no primary key. Accepted columns: ${list(accepted)}.`);
|
|
1673
|
+
}
|
|
1674
|
+
if (target.primary.length !== 1) {
|
|
1675
|
+
catalogError("OKM1021", `Foreign key ${item.tsName}.${column.field} on ${target.tsName} is ambiguous. Accepted columns: ${list(target.primary)}.`);
|
|
1676
|
+
}
|
|
1677
|
+
const sqlName = target.primary[0] ?? "";
|
|
1678
|
+
const match = target.bySql.get(sqlName);
|
|
1679
|
+
if (match === undefined) {
|
|
1680
|
+
catalogError("OKM1021", `Foreign key ${item.tsName}.${column.field} on ${target.tsName} is missing its target. Accepted columns: ${list(accepted)}.`);
|
|
1681
|
+
}
|
|
1682
|
+
return [match];
|
|
1683
|
+
}
|
|
1684
|
+
function assertCodec(tableName, field, builder, codecs) {
|
|
1685
|
+
const base = builder.state.baseType;
|
|
1686
|
+
if (base === "bigint" && codecs.bigint !== "string") {
|
|
1687
|
+
const label = builder.state.typeLabel ?? "string";
|
|
1688
|
+
if (label !== codecs.bigint) {
|
|
1689
|
+
definition(`Schema codecs.bigint is ${codecs.bigint}, and ${tableName}.${field} was built as ${label}. Set as: "${codecs.bigint}" on that column.`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (base.startsWith("numeric") && codecs.numeric !== "string") {
|
|
1693
|
+
const label = builder.state.typeLabel ?? "string";
|
|
1694
|
+
if (label !== codecs.numeric) {
|
|
1695
|
+
definition(`Schema codecs.numeric is ${codecs.numeric}, and ${tableName}.${field} was built as ${label}. Set as: "${codecs.numeric}" on that column.`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function readTableNames(tableName, options) {
|
|
1700
|
+
if (options?.sqlName !== undefined) {
|
|
1701
|
+
if (options.sqlName.length === 0) {
|
|
1702
|
+
definition(`Table ${tableName} sqlName must be a non-empty SQL identifier.`);
|
|
1703
|
+
}
|
|
1704
|
+
assertIdentifier(options.sqlName, `table ${tableName} sqlName`);
|
|
1705
|
+
}
|
|
1706
|
+
if (options?.renamedFrom !== undefined) {
|
|
1707
|
+
if (options.renamedFrom.length === 0) {
|
|
1708
|
+
definition(`Table ${tableName} renamedFrom must be a non-empty previous name.`);
|
|
1709
|
+
}
|
|
1710
|
+
assertIdentifier(options.renamedFrom, `table ${tableName} renamedFrom`);
|
|
1711
|
+
}
|
|
1712
|
+
if (options?.comment !== undefined) {
|
|
1713
|
+
assertStoredText(options.comment, `table ${tableName} comment`);
|
|
1714
|
+
if (options.comment.length === 0) {
|
|
1715
|
+
definition(`Table ${tableName} comment must be non-empty.`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
function readTypes(types) {
|
|
1720
|
+
if (types === undefined) {
|
|
1721
|
+
return "emitted";
|
|
1722
|
+
}
|
|
1723
|
+
if (types !== "emitted" && types !== "inferred") {
|
|
1724
|
+
definition(`schema() types ${String(types)} must be emitted or inferred.`);
|
|
1725
|
+
}
|
|
1726
|
+
return types;
|
|
1727
|
+
}
|
|
1728
|
+
function readCasing(casing) {
|
|
1729
|
+
if (casing === undefined) {
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
if (casing !== "snake") {
|
|
1733
|
+
definition(`schema() casing ${String(casing)} must be snake.`);
|
|
1734
|
+
}
|
|
1735
|
+
return casing;
|
|
1736
|
+
}
|
|
1737
|
+
function readCodecs(codecs) {
|
|
1738
|
+
const bigint = codecs?.bigint ?? "string";
|
|
1739
|
+
const numeric = codecs?.numeric ?? "string";
|
|
1740
|
+
const timestamps = codecs?.timestamps ?? "temporal";
|
|
1741
|
+
if (!BIGINT_CODECS.has(bigint)) {
|
|
1742
|
+
definition(`schema() codecs.bigint ${String(bigint)} must be string, number, or bigint.`);
|
|
1743
|
+
}
|
|
1744
|
+
if (!NUMERIC_CODECS.has(numeric)) {
|
|
1745
|
+
definition(`schema() codecs.numeric ${String(numeric)} must be string or number.`);
|
|
1746
|
+
}
|
|
1747
|
+
if (timestamps !== "temporal") {
|
|
1748
|
+
definition(`schema() codecs.timestamps ${String(timestamps)} must be temporal.`);
|
|
1749
|
+
}
|
|
1750
|
+
return { bigint, numeric, timestamps };
|
|
1751
|
+
}
|
|
1752
|
+
function tableModel(item, edges, byName, accepted, scopeFields) {
|
|
1753
|
+
const primary = [];
|
|
1754
|
+
for (const sqlName of item.primary) {
|
|
1755
|
+
const column = item.bySql.get(sqlName);
|
|
1756
|
+
if (column !== undefined)
|
|
1757
|
+
primary.push(column.field);
|
|
1758
|
+
}
|
|
1759
|
+
let conceal = false;
|
|
1760
|
+
const columns = item.columns.map((column) => {
|
|
1761
|
+
if (column.hidden || column.sensitive)
|
|
1762
|
+
conceal = true;
|
|
1763
|
+
return {
|
|
1764
|
+
field: column.field,
|
|
1765
|
+
sql: column.sqlName,
|
|
1766
|
+
dataType: column.dataType,
|
|
1767
|
+
encode: column.encode,
|
|
1768
|
+
decode: column.decode,
|
|
1769
|
+
hidden: column.hidden,
|
|
1770
|
+
sensitive: column.sensitive,
|
|
1771
|
+
guarded: column.guarded,
|
|
1772
|
+
writable: column.writable,
|
|
1773
|
+
guardUpdate: column.writable && primary.includes(column.field),
|
|
1774
|
+
...column.fill !== undefined ? { fill: column.fill, clientDefault: "client" } : {},
|
|
1775
|
+
...column.elementEncode !== undefined ? { elementEncode: column.elementEncode } : {},
|
|
1776
|
+
accepts: column.accepts
|
|
1777
|
+
};
|
|
1778
|
+
});
|
|
1779
|
+
return {
|
|
1780
|
+
name: item.tsName,
|
|
1781
|
+
sql: item.sqlName,
|
|
1782
|
+
primary,
|
|
1783
|
+
uniques: item.uniques,
|
|
1784
|
+
columns,
|
|
1785
|
+
relations: resolveRelations(item, edges, byName, accepted, scopeFields),
|
|
1786
|
+
...conceal ? { conceal: true } : {},
|
|
1787
|
+
...item.traits !== undefined ? { traits: item.traits } : {},
|
|
1788
|
+
...item.provenance.source !== undefined ? { source: item.provenance.source } : {},
|
|
1789
|
+
...item.archive !== undefined ? { archive: item.archive } : {},
|
|
1790
|
+
...item.presets !== undefined ? { presets: item.presets } : {}
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function resolveRelations(item, edges, byName, accepted, scopeFields) {
|
|
1794
|
+
const raw = item.relationOptions;
|
|
1795
|
+
if (raw === undefined)
|
|
1796
|
+
return [];
|
|
1797
|
+
const describe = (table) => byName.get(table);
|
|
1798
|
+
const record = raw;
|
|
1799
|
+
const names = Object.keys(record);
|
|
1800
|
+
const resolved = [];
|
|
1801
|
+
const seen = new Set;
|
|
1802
|
+
for (const name of names) {
|
|
1803
|
+
if (seen.has(name))
|
|
1804
|
+
continue;
|
|
1805
|
+
seen.add(name);
|
|
1806
|
+
const call = record[name];
|
|
1807
|
+
if (!isRelationCall(call)) {
|
|
1808
|
+
unavailable(`Table ${item.tsName} relation ${name} is not a relation. one(), many(), manyThrough(), morph(), and morphMany() are accepted.`);
|
|
1809
|
+
}
|
|
1810
|
+
const own = call.resolve;
|
|
1811
|
+
if (own !== undefined) {
|
|
1812
|
+
resolved.push(own({
|
|
1813
|
+
owner: item.tsName,
|
|
1814
|
+
name,
|
|
1815
|
+
edges,
|
|
1816
|
+
tables: accepted,
|
|
1817
|
+
describe,
|
|
1818
|
+
...scopeFields === undefined ? {} : { scopeFields }
|
|
1819
|
+
}));
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
if (!byName.has(call.table)) {
|
|
1823
|
+
throwNamed("OKM1020", call.table, accepted, `Relation ${item.tsName}.${name} names ${call.table}, which is not in the schema. Accepted names: ${list(accepted)}.`);
|
|
1824
|
+
}
|
|
1825
|
+
const field = call.field;
|
|
1826
|
+
const matches = call.kind === "one" ? edges.filter((edge) => edge.fromTable === item.tsName && edge.toTable === call.table && (field === undefined || edge.fromField === field)) : edges.filter((edge) => edge.toTable === item.tsName && edge.fromTable === call.table && (field === undefined || edge.fromField === field));
|
|
1827
|
+
if (matches.length !== 1) {
|
|
1828
|
+
const hint = matches.map((edge) => edge.fromField).sort();
|
|
1829
|
+
const which = hint.length === 0 ? "none" : hint.join(", ");
|
|
1830
|
+
catalogError("OKM1021", `Relation ${item.tsName}.${name} on ${call.table} is ambiguous. Accepted columns: ${which}.`);
|
|
1831
|
+
}
|
|
1832
|
+
const edge = matches[0];
|
|
1833
|
+
if (edge === undefined) {
|
|
1834
|
+
catalogError("OKM1021", `Relation ${item.tsName}.${name} on ${call.table} is ambiguous. Accepted columns: (none).`);
|
|
1835
|
+
}
|
|
1836
|
+
resolved.push(call.kind === "one" ? { name, kind: "one", table: call.table, local: edge.local, remote: edge.remote } : { name, kind: "many", table: call.table, local: edge.remote, remote: edge.local });
|
|
1837
|
+
}
|
|
1838
|
+
return resolved;
|
|
1839
|
+
}
|
|
1840
|
+
function readPrimaryKey(tableName, options, byField) {
|
|
1841
|
+
const listed = options?.primaryKey;
|
|
1842
|
+
if (listed === undefined)
|
|
1843
|
+
return [];
|
|
1844
|
+
if (!Array.isArray(listed) || listed.length === 0) {
|
|
1845
|
+
definition(`Table ${tableName} primaryKey needs a column.`);
|
|
1846
|
+
}
|
|
1847
|
+
const columns = [];
|
|
1848
|
+
const seen = new Set;
|
|
1849
|
+
for (const field of listed) {
|
|
1850
|
+
if (typeof field !== "string" || field.length === 0 || seen.has(field)) {
|
|
1851
|
+
definition(`Table ${tableName} primaryKey repeats a column.`);
|
|
1852
|
+
}
|
|
1853
|
+
seen.add(field);
|
|
1854
|
+
const column = byField.get(field);
|
|
1855
|
+
if (column === undefined || column.nullable) {
|
|
1856
|
+
catalogError("OKM1020", `Primary key ${tableName}.${field} is missing or nullable.`);
|
|
1857
|
+
}
|
|
1858
|
+
columns.push(column.sqlName);
|
|
1859
|
+
}
|
|
1860
|
+
return columns;
|
|
1861
|
+
}
|
|
1862
|
+
function applySchemaId(column, idDefault) {
|
|
1863
|
+
if (idDefault === undefined || column.state.idSource !== "implicit")
|
|
1864
|
+
return column;
|
|
1865
|
+
const fill = idDefault.kind === "client" ? idDefault.fill : undefined;
|
|
1866
|
+
const okid = fill?.name === "okid";
|
|
1867
|
+
return new ColumnBuilder({
|
|
1868
|
+
...column.state,
|
|
1869
|
+
defaultSql: idDefault.kind === "database" ? idDefault.sql : undefined,
|
|
1870
|
+
clientDefault: fill,
|
|
1871
|
+
idSource: "column",
|
|
1872
|
+
...okid ? {
|
|
1873
|
+
baseType: "text",
|
|
1874
|
+
collation: "C",
|
|
1875
|
+
encode: encodeText,
|
|
1876
|
+
decode: decodeText,
|
|
1877
|
+
sqlForm: "quote"
|
|
1878
|
+
} : {}
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
function readIdDefault(defaults) {
|
|
1882
|
+
if (defaults === undefined)
|
|
1883
|
+
return;
|
|
1884
|
+
if (typeof defaults !== "object" || defaults === null) {
|
|
1885
|
+
definition("schema() defaults must be an object.");
|
|
1886
|
+
}
|
|
1887
|
+
for (const key of Object.keys(defaults)) {
|
|
1888
|
+
if (key !== "id") {
|
|
1889
|
+
definition(`schema() defaults.${key} is not a schema default. Accepted names: id.`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const id = defaults.id;
|
|
1893
|
+
if (id === undefined)
|
|
1894
|
+
return;
|
|
1895
|
+
if (id === "uuidv7")
|
|
1896
|
+
return { kind: "database", sql: "uuidv7()" };
|
|
1897
|
+
if (id === "uuidv4")
|
|
1898
|
+
return { kind: "database", sql: "gen_random_uuid()" };
|
|
1899
|
+
const fill = readClientGenerator(id);
|
|
1900
|
+
if (fill !== undefined)
|
|
1901
|
+
return { kind: "client", fill };
|
|
1902
|
+
definition('schema() defaults.id must be "uuidv7", "uuidv4", or a client generator.');
|
|
1903
|
+
}
|
|
1904
|
+
function refuseUuidV7(requires) {
|
|
1905
|
+
const declared = parsePostgresFloor(requires?.postgres);
|
|
1906
|
+
if (!(declared > 0) || declared >= UUIDV7_POSTGRES)
|
|
1907
|
+
return;
|
|
1908
|
+
const sentence = uuidv7NeedsPostgres();
|
|
1909
|
+
throw new OkmError("OKM1812", sentence, { fix: { summary: sentence } });
|
|
1910
|
+
}
|
|
1911
|
+
function readFloor(minPostgres, requires) {
|
|
1912
|
+
if (minPostgres !== undefined && requires !== undefined) {
|
|
1913
|
+
definition("minPostgres or requires.postgres, not both.");
|
|
1914
|
+
}
|
|
1915
|
+
if (minPostgres === undefined && requires === undefined)
|
|
1916
|
+
return;
|
|
1917
|
+
let major = minPostgres;
|
|
1918
|
+
if (minPostgres === undefined) {
|
|
1919
|
+
if (typeof requires !== "object" || requires === null || Array.isArray(requires)) {
|
|
1920
|
+
definition(minPostgresDefinition());
|
|
1921
|
+
}
|
|
1922
|
+
const postgres = requires.postgres;
|
|
1923
|
+
if (postgres === undefined)
|
|
1924
|
+
return {};
|
|
1925
|
+
major = typeof postgres === "string" ? parsePostgresFloor(postgres) : NaN;
|
|
1926
|
+
}
|
|
1927
|
+
if (!isPostgresMajor(major))
|
|
1928
|
+
definition(minPostgresDefinition());
|
|
1929
|
+
return { postgres: formatPostgresFloor(major) };
|
|
1930
|
+
}
|
|
1931
|
+
function readIndex(tableName, call) {
|
|
1932
|
+
if (call.columns.length === 0) {
|
|
1933
|
+
definition(`Index on ${tableName} needs a column.`);
|
|
1934
|
+
}
|
|
1935
|
+
for (const name of call.columns) {
|
|
1936
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1937
|
+
definition(`Index on ${tableName} must be built from column handles.`);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return call;
|
|
1941
|
+
}
|
|
1942
|
+
function readCheck(tableName, name, value) {
|
|
1943
|
+
if (value === undefined || typeof value.text !== "string" || value.text.length === 0) {
|
|
1944
|
+
definition(`Check ${tableName}.${name} must return sql text.`);
|
|
1945
|
+
}
|
|
1946
|
+
assertStoredText(value.text, `check ${tableName}.${name}`);
|
|
1947
|
+
return value.text;
|
|
1948
|
+
}
|
|
1949
|
+
function track(handles, used) {
|
|
1950
|
+
return new Proxy(handles, {
|
|
1951
|
+
get(target, key) {
|
|
1952
|
+
if (typeof key === "string" && Object.hasOwn(target, key)) {
|
|
1953
|
+
used.push(key);
|
|
1954
|
+
return target[key];
|
|
1955
|
+
}
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
function list(names) {
|
|
1961
|
+
return names.length === 0 ? "(none)" : names.join(", ");
|
|
1962
|
+
}
|
|
1963
|
+
// src/dialects/pg/keys.ts
|
|
1964
|
+
function id(options) {
|
|
1965
|
+
const chosen = options?.default;
|
|
1966
|
+
const client = typeof chosen === "function" ? readClientGenerator(chosen) : undefined;
|
|
1967
|
+
const okid = client?.name === "okid";
|
|
1968
|
+
const supplied = chosen === "none";
|
|
1969
|
+
if (client === undefined && chosen !== undefined && chosen !== "uuidv7" && chosen !== "uuidv4" && chosen !== "none") {
|
|
1970
|
+
definition(`id() default ${String(chosen)} must be uuidv7, uuidv4, none, or a client generator.`);
|
|
1971
|
+
}
|
|
1972
|
+
return openColumn({
|
|
1973
|
+
baseType: okid ? "text" : "uuid",
|
|
1974
|
+
nullable: false,
|
|
1975
|
+
hasDefault: client !== undefined || !supplied,
|
|
1976
|
+
generated: false,
|
|
1977
|
+
guarded: client !== undefined || !supplied,
|
|
1978
|
+
hidden: false,
|
|
1979
|
+
omitWrite: client !== undefined || !supplied,
|
|
1980
|
+
omitUpdate: true,
|
|
1981
|
+
...client !== undefined ? { clientDefault: client } : {},
|
|
1982
|
+
...okid ? { collation: "C" } : {},
|
|
1983
|
+
idSource: chosen === undefined ? "implicit" : "column",
|
|
1984
|
+
...client === undefined && !supplied ? { defaultSql: chosen === "uuidv7" ? "uuidv7()" : "gen_random_uuid()" } : {},
|
|
1985
|
+
primaryKey: true,
|
|
1986
|
+
encode: okid ? encodeText : encodeUuid,
|
|
1987
|
+
decode: okid ? decodeText : decodeUuid,
|
|
1988
|
+
sqlForm: "quote"
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
function uuid() {
|
|
1992
|
+
return required({
|
|
1993
|
+
baseType: "uuid",
|
|
1994
|
+
encode: encodeUuid,
|
|
1995
|
+
decode: decodeUuid,
|
|
1996
|
+
sqlForm: "quote"
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
function encodeUuid(value) {
|
|
2000
|
+
const text = value.toLowerCase();
|
|
2001
|
+
if (!uuidText().test(text)) {
|
|
2002
|
+
rejected(`uuid ${value} must be 8-4-4-4-12 hexadecimal digits, for example 01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c8d.`);
|
|
2003
|
+
}
|
|
2004
|
+
return text;
|
|
2005
|
+
}
|
|
2006
|
+
function decodeUuid(wire) {
|
|
2007
|
+
return encodeUuid(wire);
|
|
2008
|
+
}
|
|
2009
|
+
function uuidText() {
|
|
2010
|
+
uuidPattern ??= /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
2011
|
+
return uuidPattern;
|
|
2012
|
+
}
|
|
2013
|
+
var uuidPattern;
|
|
2014
|
+
// src/dialects/pg/bool.ts
|
|
2015
|
+
function boolean() {
|
|
2016
|
+
return required({
|
|
2017
|
+
baseType: "boolean",
|
|
2018
|
+
encode: encodeBoolean,
|
|
2019
|
+
decode: decodeBoolean,
|
|
2020
|
+
sqlForm: "raw"
|
|
2021
|
+
});
|
|
2022
|
+
}
|
|
2023
|
+
function bytea() {
|
|
2024
|
+
return required({
|
|
2025
|
+
baseType: "bytea",
|
|
2026
|
+
encode: encodeBytea,
|
|
2027
|
+
decode: decodeBytea,
|
|
2028
|
+
accepts: ["Uint8Array"],
|
|
2029
|
+
sqlForm: "quote"
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
function encodeBoolean(value) {
|
|
2033
|
+
return value ? "true" : "false";
|
|
2034
|
+
}
|
|
2035
|
+
function decodeBoolean(wire) {
|
|
2036
|
+
if (wire === "true" || wire === "t" || wire === "1") {
|
|
2037
|
+
return true;
|
|
2038
|
+
}
|
|
2039
|
+
if (wire === "false" || wire === "f" || wire === "0") {
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
2042
|
+
rejected(`boolean ${wire} must be true, false, t, f, 1, or 0.`);
|
|
2043
|
+
}
|
|
2044
|
+
function encodeBytea(value) {
|
|
2045
|
+
let hex = "";
|
|
2046
|
+
for (const byte of value) {
|
|
2047
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
2048
|
+
}
|
|
2049
|
+
return `\\x${hex}`;
|
|
2050
|
+
}
|
|
2051
|
+
function decodeBytea(wire) {
|
|
2052
|
+
if (!wire.startsWith("\\x") || (wire.length - 2) % 2 !== 0) {
|
|
2053
|
+
rejected(`bytea ${wire} must be \\x followed by an even number of hex digits.`);
|
|
2054
|
+
}
|
|
2055
|
+
const bytes = new Uint8Array((wire.length - 2) / 2);
|
|
2056
|
+
for (let index = 0;index < bytes.length; index += 1) {
|
|
2057
|
+
const hex = wire.slice(2 + index * 2, 4 + index * 2);
|
|
2058
|
+
const byte = Number.parseInt(hex, 16);
|
|
2059
|
+
if (!hexText().test(hex) || Number.isNaN(byte)) {
|
|
2060
|
+
rejected(`bytea ${wire} must be \\x followed by an even number of hex digits.`);
|
|
2061
|
+
}
|
|
2062
|
+
bytes[index] = byte;
|
|
2063
|
+
}
|
|
2064
|
+
return bytes;
|
|
2065
|
+
}
|
|
2066
|
+
function hexText() {
|
|
2067
|
+
hexPattern ??= /^[0-9a-f]{2}$/i;
|
|
2068
|
+
return hexPattern;
|
|
2069
|
+
}
|
|
2070
|
+
var hexPattern;
|
|
2071
|
+
|
|
2072
|
+
// src/dialects/pg/custom.ts
|
|
2073
|
+
function custom(spec) {
|
|
2074
|
+
if (spec.sqlType.length === 0) {
|
|
2075
|
+
definition("custom sqlType must be a non-empty SQL type.");
|
|
2076
|
+
}
|
|
2077
|
+
return required({
|
|
2078
|
+
baseType: spec.sqlType,
|
|
2079
|
+
encode: spec.encode,
|
|
2080
|
+
decode: spec.decode,
|
|
2081
|
+
...spec.accepts !== undefined ? { accepts: spec.accepts } : {},
|
|
2082
|
+
sqlForm: spec.sqlForm ?? "quote",
|
|
2083
|
+
...spec.extension !== undefined ? { extension: spec.extension } : {}
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/dialects/pg/finite.ts
|
|
2088
|
+
function decimalText(value, role) {
|
|
2089
|
+
if (!Number.isFinite(value)) {
|
|
2090
|
+
rejected(`${role} ${String(value)} must be a finite number. Infinity and NaN are not accepted.`);
|
|
2091
|
+
}
|
|
2092
|
+
return Object.is(value, -0) ? "0" : JSON.stringify(value);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
// src/dialects/pg/decimal.ts
|
|
2096
|
+
function numeric(precision, scale, options) {
|
|
2097
|
+
const baseType = numericType(precision, scale);
|
|
2098
|
+
if (options?.as === "number") {
|
|
2099
|
+
return required({
|
|
2100
|
+
baseType,
|
|
2101
|
+
encode: encodeNumericNumber,
|
|
2102
|
+
decode: decodeNumericNumber,
|
|
2103
|
+
sqlForm: "raw",
|
|
2104
|
+
typeLabel: "number"
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
if (options !== undefined && options.as !== undefined && options.as !== "string") {
|
|
2108
|
+
definition(`numeric as ${String(options.as)} must be string or number.`);
|
|
2109
|
+
}
|
|
2110
|
+
return required({
|
|
2111
|
+
baseType,
|
|
2112
|
+
encode: encodeNumericString,
|
|
2113
|
+
decode: decodeNumericString,
|
|
2114
|
+
sqlForm: "raw"
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
function real() {
|
|
2118
|
+
return required({
|
|
2119
|
+
baseType: "real",
|
|
2120
|
+
encode: (value) => decimalText(value, "real"),
|
|
2121
|
+
decode: (wire) => decodeFinite(wire, "real"),
|
|
2122
|
+
sqlForm: "raw"
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
function double() {
|
|
2126
|
+
return required({
|
|
2127
|
+
baseType: "double precision",
|
|
2128
|
+
encode: (value) => decimalText(value, "double"),
|
|
2129
|
+
decode: (wire) => decodeFinite(wire, "double"),
|
|
2130
|
+
sqlForm: "raw"
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
function encodeNumericString(value) {
|
|
2134
|
+
if (!numericText().test(value)) {
|
|
2135
|
+
rejected(`numeric ${value} must be a decimal literal, for example 1, -2.5, or 1e-3.`);
|
|
2136
|
+
}
|
|
2137
|
+
return value;
|
|
2138
|
+
}
|
|
2139
|
+
function decodeNumericString(wire) {
|
|
2140
|
+
return encodeNumericString(wire);
|
|
2141
|
+
}
|
|
2142
|
+
function encodeNumericNumber(value) {
|
|
2143
|
+
return decimalText(value, "numeric");
|
|
2144
|
+
}
|
|
2145
|
+
function decodeNumericNumber(wire) {
|
|
2146
|
+
encodeNumericString(wire);
|
|
2147
|
+
return Number(wire);
|
|
2148
|
+
}
|
|
2149
|
+
function numericType(precision, scale) {
|
|
2150
|
+
if (precision === undefined && scale === undefined) {
|
|
2151
|
+
return "numeric";
|
|
2152
|
+
}
|
|
2153
|
+
if (precision === undefined || !Number.isInteger(precision) || precision < 1 || precision > 1000) {
|
|
2154
|
+
definition(`numeric precision ${String(precision)} must be an integer from 1 to 1000.`);
|
|
2155
|
+
}
|
|
2156
|
+
if (scale === undefined) {
|
|
2157
|
+
return `numeric(${String(precision)})`;
|
|
2158
|
+
}
|
|
2159
|
+
if (!Number.isInteger(scale) || scale < 0 || scale > precision) {
|
|
2160
|
+
definition(`numeric scale ${String(scale)} must be an integer from 0 to ${String(precision)}.`);
|
|
2161
|
+
}
|
|
2162
|
+
return `numeric(${String(precision)},${String(scale)})`;
|
|
2163
|
+
}
|
|
2164
|
+
function decodeFinite(wire, role) {
|
|
2165
|
+
const value = Number(wire);
|
|
2166
|
+
if (!Number.isFinite(value)) {
|
|
2167
|
+
rejected(`${role} ${wire} must be a finite number.`);
|
|
2168
|
+
}
|
|
2169
|
+
return Object.is(value, -0) ? 0 : value;
|
|
2170
|
+
}
|
|
2171
|
+
function numericText() {
|
|
2172
|
+
numericPattern ??= /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
2173
|
+
return numericPattern;
|
|
2174
|
+
}
|
|
2175
|
+
var numericPattern;
|
|
2176
|
+
|
|
2177
|
+
// src/dialects/pg/enum.ts
|
|
2178
|
+
function enumColumn(name, values) {
|
|
2179
|
+
if (name.length === 0) {
|
|
2180
|
+
definition("enum name must be a non-empty type name.");
|
|
2181
|
+
}
|
|
2182
|
+
if (values.length === 0) {
|
|
2183
|
+
definition("enum must list at least one label.");
|
|
2184
|
+
}
|
|
2185
|
+
const allowed = new Set;
|
|
2186
|
+
for (const value of values) {
|
|
2187
|
+
if (value.length === 0) {
|
|
2188
|
+
definition("enum labels must be non-empty strings.");
|
|
2189
|
+
}
|
|
2190
|
+
if (allowed.has(value)) {
|
|
2191
|
+
definition(`enum label ${value} is repeated. Each label is accepted once.`);
|
|
2192
|
+
}
|
|
2193
|
+
allowed.add(value);
|
|
2194
|
+
}
|
|
2195
|
+
return required({
|
|
2196
|
+
baseType: name,
|
|
2197
|
+
typeDependency: name,
|
|
2198
|
+
enumLabels: [...values],
|
|
2199
|
+
typeLabel: values.map((value) => JSON.stringify(value)).join(" | "),
|
|
2200
|
+
encode: (value) => {
|
|
2201
|
+
if (!allowed.has(value)) {
|
|
2202
|
+
rejected(`enum rejected ${value}. Accepted labels: ${[...allowed].join(", ")}.`);
|
|
2203
|
+
}
|
|
2204
|
+
return value;
|
|
2205
|
+
},
|
|
2206
|
+
decode: (wire) => {
|
|
2207
|
+
if (!allowed.has(wire)) {
|
|
2208
|
+
rejected(`enum rejected ${wire}. Accepted labels: ${[...allowed].join(", ")}.`);
|
|
2209
|
+
}
|
|
2210
|
+
return wire;
|
|
2211
|
+
},
|
|
2212
|
+
sqlForm: "quote"
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
function domain(name, base, check) {
|
|
2216
|
+
if (name.length === 0)
|
|
2217
|
+
definition("domain name must be a non-empty type name.");
|
|
2218
|
+
if (check.length === 0)
|
|
2219
|
+
definition("domain check must be a non-empty expression.");
|
|
2220
|
+
if (base.state.enumLabels !== undefined) {
|
|
2221
|
+
definition("A domain cannot use an enum as its base in this version. Use a scalar column type.");
|
|
2222
|
+
}
|
|
2223
|
+
const baseSql = canonicalBase(formatType(base.state.baseType, base.state.dims));
|
|
2224
|
+
const extension = base.state.extension;
|
|
2225
|
+
const inner = base.state.baseType;
|
|
2226
|
+
const nested = base.state.domain !== undefined;
|
|
2227
|
+
return new ColumnBuilder({
|
|
2228
|
+
...base.state,
|
|
2229
|
+
baseType: name,
|
|
2230
|
+
dims: 0,
|
|
2231
|
+
typeDependency: name,
|
|
2232
|
+
enumLabels: undefined,
|
|
2233
|
+
domain: {
|
|
2234
|
+
base: baseSql,
|
|
2235
|
+
check,
|
|
2236
|
+
emit(namespace, provenance, objects) {
|
|
2237
|
+
base.state.domain?.emit?.(namespace, provenance, objects);
|
|
2238
|
+
const dependencies = [];
|
|
2239
|
+
if (extension !== undefined)
|
|
2240
|
+
dependencies.push({ kind: "extension", name: extension });
|
|
2241
|
+
if (nested)
|
|
2242
|
+
dependencies.push({ kind: "type", namespace, name: inner });
|
|
2243
|
+
const built = domainType({
|
|
2244
|
+
namespace,
|
|
2245
|
+
name,
|
|
2246
|
+
base: baseSql,
|
|
2247
|
+
check,
|
|
2248
|
+
provenance,
|
|
2249
|
+
...dependencies.length > 0 ? { dependencies } : {}
|
|
2250
|
+
});
|
|
2251
|
+
const index = objects.findIndex((object) => object.kind === "type" && object.identity.name === name && sameNamespace(object.identity.namespace, namespace));
|
|
2252
|
+
const previous = index >= 0 ? objects[index] : undefined;
|
|
2253
|
+
if (previous !== undefined && previous.kind === "type") {
|
|
2254
|
+
const definition = previous.definition;
|
|
2255
|
+
if (!("base" in definition) || definition.base !== baseSql || definition.check !== check) {
|
|
2256
|
+
catalogError("OKM1020", `Domain ${name} is ${"base" in definition ? definition.base : "an enum"} on ${previous.provenance.name} and ${baseSql} on ${provenance.name}. One domain has one base and one check.`);
|
|
2257
|
+
}
|
|
2258
|
+
if (compareText(provenance.name, previous.provenance.name) < 0)
|
|
2259
|
+
objects[index] = built;
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
objects.push(built);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
function canonicalBase(typeName) {
|
|
2268
|
+
const bracket = typeName.indexOf("[");
|
|
2269
|
+
const scalar = bracket === -1 ? typeName : typeName.slice(0, bracket);
|
|
2270
|
+
const suffix = bracket === -1 ? "" : typeName.slice(bracket);
|
|
2271
|
+
return canonicalScalar(scalar) + suffix;
|
|
2272
|
+
}
|
|
2273
|
+
function canonicalScalar(typeName) {
|
|
2274
|
+
const varchar = /^varchar\((\d+)\)$/.exec(typeName);
|
|
2275
|
+
if (varchar !== null)
|
|
2276
|
+
return `character varying(${varchar[1] ?? ""})`;
|
|
2277
|
+
const fixed = /^char\((\d+)\)$/.exec(typeName);
|
|
2278
|
+
if (fixed !== null)
|
|
2279
|
+
return `character(${fixed[1] ?? ""})`;
|
|
2280
|
+
const timed = /^(timestamptz|timestamp|timetz|time)(\(\d+\))?$/.exec(typeName);
|
|
2281
|
+
if (timed === null)
|
|
2282
|
+
return typeName;
|
|
2283
|
+
const name = timed[1];
|
|
2284
|
+
const precision = timed[2] ?? "";
|
|
2285
|
+
if (name === "timestamptz")
|
|
2286
|
+
return `timestamp${precision} with time zone`;
|
|
2287
|
+
if (name === "timestamp")
|
|
2288
|
+
return `timestamp${precision} without time zone`;
|
|
2289
|
+
if (name === "timetz")
|
|
2290
|
+
return `time${precision} with time zone`;
|
|
2291
|
+
return `time${precision} without time zone`;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// src/dialects/pg/geometry.ts
|
|
2295
|
+
function point() {
|
|
2296
|
+
return required({
|
|
2297
|
+
baseType: "point",
|
|
2298
|
+
encode: encodePoint,
|
|
2299
|
+
decode: decodePoint,
|
|
2300
|
+
accepts: ["Object"],
|
|
2301
|
+
sqlForm: "quote"
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
function line() {
|
|
2305
|
+
return required({
|
|
2306
|
+
baseType: "line",
|
|
2307
|
+
encode: encodeLine,
|
|
2308
|
+
decode: decodeLine,
|
|
2309
|
+
accepts: ["Object"],
|
|
2310
|
+
sqlForm: "quote"
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2313
|
+
function encodePoint(value) {
|
|
2314
|
+
return `(${decimalText(value.x, "point")},${decimalText(value.y, "point")})`;
|
|
2315
|
+
}
|
|
2316
|
+
function decodePoint(wire) {
|
|
2317
|
+
const match = pointText().exec(wire);
|
|
2318
|
+
const x = match?.[1];
|
|
2319
|
+
const y = match?.[2];
|
|
2320
|
+
if (x === undefined || y === undefined) {
|
|
2321
|
+
rejected(`point ${wire} must be (x,y), for example (1,2).`);
|
|
2322
|
+
}
|
|
2323
|
+
return { x: finite(Number(x), "point"), y: finite(Number(y), "point") };
|
|
2324
|
+
}
|
|
2325
|
+
function encodeLine(value) {
|
|
2326
|
+
return `{${decimalText(value.a, "line")},${decimalText(value.b, "line")},${decimalText(value.c, "line")}}`;
|
|
2327
|
+
}
|
|
2328
|
+
function decodeLine(wire) {
|
|
2329
|
+
const match = lineText().exec(wire);
|
|
2330
|
+
const a = match?.[1];
|
|
2331
|
+
const b = match?.[2];
|
|
2332
|
+
const c = match?.[3];
|
|
2333
|
+
if (a === undefined || b === undefined || c === undefined) {
|
|
2334
|
+
rejected(`line ${wire} must be {a,b,c}, for example {1,0,-1}.`);
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
a: finite(Number(a), "line"),
|
|
2338
|
+
b: finite(Number(b), "line"),
|
|
2339
|
+
c: finite(Number(c), "line")
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
function finite(value, role) {
|
|
2343
|
+
if (!Number.isFinite(value)) {
|
|
2344
|
+
rejected(`${role} ${String(value)} must be a finite number. Infinity and NaN are not accepted.`);
|
|
2345
|
+
}
|
|
2346
|
+
return Object.is(value, -0) ? 0 : value;
|
|
2347
|
+
}
|
|
2348
|
+
function pointText() {
|
|
2349
|
+
pointPattern ??= /^\(([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?),([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\)$/;
|
|
2350
|
+
return pointPattern;
|
|
2351
|
+
}
|
|
2352
|
+
function lineText() {
|
|
2353
|
+
linePattern ??= /^\{([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?),([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?),([-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?)\}$/;
|
|
2354
|
+
return linePattern;
|
|
2355
|
+
}
|
|
2356
|
+
var pointPattern;
|
|
2357
|
+
var linePattern;
|
|
2358
|
+
|
|
2359
|
+
// src/dialects/pg/integer.ts
|
|
2360
|
+
function smallint() {
|
|
2361
|
+
return required({
|
|
2362
|
+
baseType: "smallint",
|
|
2363
|
+
encode: (value) => integerToken(value, -32768, 32767, "smallint"),
|
|
2364
|
+
decode: (wire) => decodeBounded(wire, -32768, 32767, "smallint"),
|
|
2365
|
+
sqlForm: "raw"
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
function integer() {
|
|
2369
|
+
return required({
|
|
2370
|
+
baseType: "integer",
|
|
2371
|
+
encode: (value) => integerToken(value, -2147483648, 2147483647, "integer"),
|
|
2372
|
+
decode: (wire) => decodeBounded(wire, -2147483648, 2147483647, "integer"),
|
|
2373
|
+
sqlForm: "raw"
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
function bigint(options) {
|
|
2377
|
+
const mode = options?.as ?? "string";
|
|
2378
|
+
if (mode === "number") {
|
|
2379
|
+
return required({
|
|
2380
|
+
baseType: "bigint",
|
|
2381
|
+
encode: encodeBigintNumber,
|
|
2382
|
+
decode: decodeBigintNumber,
|
|
2383
|
+
sqlForm: "raw",
|
|
2384
|
+
typeLabel: "number"
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
if (mode === "bigint") {
|
|
2388
|
+
return required({
|
|
2389
|
+
baseType: "bigint",
|
|
2390
|
+
encode: encodeBigintValue,
|
|
2391
|
+
decode: decodeBigintValue,
|
|
2392
|
+
sqlForm: "raw",
|
|
2393
|
+
typeLabel: "bigint"
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
if (mode !== "string") {
|
|
2397
|
+
definition(`bigint as ${String(mode)} must be string, number, or bigint.`);
|
|
2398
|
+
}
|
|
2399
|
+
return required({
|
|
2400
|
+
baseType: "bigint",
|
|
2401
|
+
encode: encodeBigintString,
|
|
2402
|
+
decode: decodeBigintString,
|
|
2403
|
+
sqlForm: "raw"
|
|
2404
|
+
});
|
|
2405
|
+
}
|
|
2406
|
+
function identity(options) {
|
|
2407
|
+
const mode = options?.as ?? "string";
|
|
2408
|
+
if (mode === "number") {
|
|
2409
|
+
return openColumn({
|
|
2410
|
+
...identityFlags(),
|
|
2411
|
+
encode: encodeBigintNumber,
|
|
2412
|
+
decode: decodeBigintNumber,
|
|
2413
|
+
typeLabel: "number"
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
if (mode === "bigint") {
|
|
2417
|
+
return openColumn({
|
|
2418
|
+
...identityFlags(),
|
|
2419
|
+
encode: encodeBigintValue,
|
|
2420
|
+
decode: decodeBigintValue,
|
|
2421
|
+
typeLabel: "bigint"
|
|
2422
|
+
});
|
|
2423
|
+
}
|
|
2424
|
+
if (mode !== "string") {
|
|
2425
|
+
definition(`identity as ${String(mode)} must be string, number, or bigint.`);
|
|
2426
|
+
}
|
|
2427
|
+
return openColumn({
|
|
2428
|
+
...identityFlags(),
|
|
2429
|
+
encode: encodeBigintString,
|
|
2430
|
+
decode: decodeBigintString
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
function encodeBigintString(value) {
|
|
2434
|
+
parseBigint(value);
|
|
2435
|
+
return value;
|
|
2436
|
+
}
|
|
2437
|
+
function decodeBigintString(wire) {
|
|
2438
|
+
return encodeBigintString(wire);
|
|
2439
|
+
}
|
|
2440
|
+
function encodeBigintNumber(value) {
|
|
2441
|
+
if (!Number.isSafeInteger(value)) {
|
|
2442
|
+
rejected(`bigint number ${String(value)} must be an integer from ${String(Number.MIN_SAFE_INTEGER)} to ${String(Number.MAX_SAFE_INTEGER)}.`);
|
|
2443
|
+
}
|
|
2444
|
+
return String(value);
|
|
2445
|
+
}
|
|
2446
|
+
function decodeBigintNumber(wire) {
|
|
2447
|
+
const parsed = parseBigint(wire);
|
|
2448
|
+
const value = Number(parsed);
|
|
2449
|
+
if (!Number.isSafeInteger(value)) {
|
|
2450
|
+
rejected(`bigint ${wire} must be an integer from ${String(Number.MIN_SAFE_INTEGER)} to ${String(Number.MAX_SAFE_INTEGER)} when as is number.`);
|
|
2451
|
+
}
|
|
2452
|
+
return value;
|
|
2453
|
+
}
|
|
2454
|
+
function encodeBigintValue(value) {
|
|
2455
|
+
return value.toString();
|
|
2456
|
+
}
|
|
2457
|
+
function decodeBigintValue(wire) {
|
|
2458
|
+
return parseBigint(wire);
|
|
2459
|
+
}
|
|
2460
|
+
function integerToken(value, min, max, role) {
|
|
2461
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
2462
|
+
rejected(`${role} ${String(value)} must be an integer from ${String(min)} to ${String(max)}.`);
|
|
2463
|
+
}
|
|
2464
|
+
return String(value);
|
|
2465
|
+
}
|
|
2466
|
+
function decodeBounded(wire, min, max, role) {
|
|
2467
|
+
if (!integerText().test(wire)) {
|
|
2468
|
+
rejected(`${role} ${wire} must be an integer, for example 0 or -12.`);
|
|
2469
|
+
}
|
|
2470
|
+
const value = Number(wire);
|
|
2471
|
+
integerToken(value, min, max, role);
|
|
2472
|
+
return value;
|
|
2473
|
+
}
|
|
2474
|
+
function identityFlags() {
|
|
2475
|
+
return {
|
|
2476
|
+
baseType: "bigint",
|
|
2477
|
+
nullable: false,
|
|
2478
|
+
hasDefault: true,
|
|
2479
|
+
generated: false,
|
|
2480
|
+
guarded: false,
|
|
2481
|
+
hidden: false,
|
|
2482
|
+
omitWrite: true,
|
|
2483
|
+
sqlForm: "raw",
|
|
2484
|
+
identity: { always: true },
|
|
2485
|
+
primaryKey: true
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
function parseBigint(wire) {
|
|
2489
|
+
if (!integerText().test(wire)) {
|
|
2490
|
+
rejected(`bigint ${wire} must be an integer with no leading zeros, for example 0 or -12.`);
|
|
2491
|
+
}
|
|
2492
|
+
return BigInt(wire);
|
|
2493
|
+
}
|
|
2494
|
+
function integerText() {
|
|
2495
|
+
integerPattern ??= /^-?(?:0|[1-9]\d*)$/;
|
|
2496
|
+
return integerPattern;
|
|
2497
|
+
}
|
|
2498
|
+
var integerPattern;
|
|
2499
|
+
|
|
2500
|
+
// src/dialects/pg/network.ts
|
|
2501
|
+
function inet() {
|
|
2502
|
+
return network("inet", false);
|
|
2503
|
+
}
|
|
2504
|
+
function cidr() {
|
|
2505
|
+
return network("cidr", true);
|
|
2506
|
+
}
|
|
2507
|
+
function macaddr() {
|
|
2508
|
+
return mac(6, "macaddr");
|
|
2509
|
+
}
|
|
2510
|
+
function macaddr8() {
|
|
2511
|
+
return mac(8, "macaddr8");
|
|
2512
|
+
}
|
|
2513
|
+
function network(baseType, prefixRequired) {
|
|
2514
|
+
return required({
|
|
2515
|
+
baseType,
|
|
2516
|
+
encode: (value) => encodeNetwork(value, baseType, prefixRequired),
|
|
2517
|
+
decode: (wire) => encodeNetwork(wire, baseType, prefixRequired),
|
|
2518
|
+
sqlForm: "quote"
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
function mac(groups, baseType) {
|
|
2522
|
+
return required({
|
|
2523
|
+
baseType,
|
|
2524
|
+
encode: (value) => encodeMac(value, groups, baseType),
|
|
2525
|
+
decode: (wire) => encodeMac(wire, groups, baseType),
|
|
2526
|
+
sqlForm: "quote"
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
function encodeNetwork(value, role, prefixRequired) {
|
|
2530
|
+
const text = value.toLowerCase();
|
|
2531
|
+
const slash = text.lastIndexOf("/");
|
|
2532
|
+
const host = slash === -1 ? text : text.slice(0, slash);
|
|
2533
|
+
const prefix = slash === -1 ? undefined : text.slice(slash + 1);
|
|
2534
|
+
if (prefixRequired && prefix === undefined) {
|
|
2535
|
+
rejected(`${role} ${value} needs a prefix length.`);
|
|
2536
|
+
}
|
|
2537
|
+
if (prefix !== undefined && (!/^\d{1,3}$/.test(prefix) || Number(prefix) > 128)) {
|
|
2538
|
+
rejected(`${role} prefix ${prefix} is not a length.`);
|
|
2539
|
+
}
|
|
2540
|
+
if (host.includes(":")) {
|
|
2541
|
+
if (!/^[0-9a-f:]+$/.test(host) || host.length > 39) {
|
|
2542
|
+
rejected(`${role} ${value} is not an address.`);
|
|
2543
|
+
}
|
|
2544
|
+
return prefix === undefined ? host : `${host}/${prefix}`;
|
|
2545
|
+
}
|
|
2546
|
+
const octets = host.split(".");
|
|
2547
|
+
if (octets.length !== 4 || octets.some((octet) => !octetOk(octet))) {
|
|
2548
|
+
rejected(`${role} ${value} is not an address.`);
|
|
2549
|
+
}
|
|
2550
|
+
if (prefix !== undefined && Number(prefix) > 32) {
|
|
2551
|
+
rejected(`${role} prefix ${prefix} is not a length.`);
|
|
2552
|
+
}
|
|
2553
|
+
return prefix === undefined ? host : `${host}/${prefix}`;
|
|
2554
|
+
}
|
|
2555
|
+
function octetOk(octet) {
|
|
2556
|
+
if (!/^\d{1,3}$/.test(octet)) {
|
|
2557
|
+
return false;
|
|
2558
|
+
}
|
|
2559
|
+
const value = Number(octet);
|
|
2560
|
+
return value <= 255 && octet === String(value);
|
|
2561
|
+
}
|
|
2562
|
+
function encodeMac(value, groups, role) {
|
|
2563
|
+
const text = value.toLowerCase();
|
|
2564
|
+
const parts = text.split(":");
|
|
2565
|
+
if (parts.length !== groups || parts.some((part) => !/^[0-9a-f]{2}$/.test(part))) {
|
|
2566
|
+
rejected(`${role} ${value} is not a MAC address.`);
|
|
2567
|
+
}
|
|
2568
|
+
return text;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
// src/dialects/pg/temporal.ts
|
|
2572
|
+
var TEMPORAL_MISSING = "Date and time codecs need the Temporal global. okmodel does not ship a polyfill. Use a runtime that provides Temporal, or assign a polyfill to globalThis.Temporal before using these columns.";
|
|
2573
|
+
function requireTemporal() {
|
|
2574
|
+
const temporal = globalThis.Temporal;
|
|
2575
|
+
if (typeof temporal !== "object" || temporal === null || !("Instant" in temporal)) {
|
|
2576
|
+
rejected(TEMPORAL_MISSING);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function timePrecision(precision, role) {
|
|
2580
|
+
if (precision === undefined) {
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
if (!Number.isInteger(precision) || precision < 0 || precision > 6) {
|
|
2584
|
+
definition(`${role} precision ${String(precision)} must be an integer from 0 to 6.`);
|
|
2585
|
+
}
|
|
2586
|
+
return precision;
|
|
2587
|
+
}
|
|
2588
|
+
function encodeInstant(value, precision) {
|
|
2589
|
+
requireTemporal();
|
|
2590
|
+
if (precision === undefined) {
|
|
2591
|
+
return value.toString();
|
|
2592
|
+
}
|
|
2593
|
+
return value.toString({ fractionalSecondDigits: precision });
|
|
2594
|
+
}
|
|
2595
|
+
function decodeInstant(wire) {
|
|
2596
|
+
requireTemporal();
|
|
2597
|
+
try {
|
|
2598
|
+
return Temporal.Instant.from(wire);
|
|
2599
|
+
} catch {
|
|
2600
|
+
rejected(`timestamptz ${wire} must be an ISO-8601 instant, for example 2020-01-01T00:00:00Z.`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function encodeDateTime(value, precision) {
|
|
2604
|
+
requireTemporal();
|
|
2605
|
+
if (precision === undefined) {
|
|
2606
|
+
return value.toString();
|
|
2607
|
+
}
|
|
2608
|
+
return value.toString({ fractionalSecondDigits: precision });
|
|
2609
|
+
}
|
|
2610
|
+
function decodeDateTime(wire) {
|
|
2611
|
+
requireTemporal();
|
|
2612
|
+
try {
|
|
2613
|
+
return Temporal.PlainDateTime.from(wire);
|
|
2614
|
+
} catch {
|
|
2615
|
+
rejected(`timestamp ${wire} must be an ISO-8601 date-time without a zone, for example 2020-01-01T00:00:00.`);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
function encodeDate(value) {
|
|
2619
|
+
requireTemporal();
|
|
2620
|
+
return value.toString();
|
|
2621
|
+
}
|
|
2622
|
+
function decodeDate(wire) {
|
|
2623
|
+
requireTemporal();
|
|
2624
|
+
try {
|
|
2625
|
+
return Temporal.PlainDate.from(wire);
|
|
2626
|
+
} catch {
|
|
2627
|
+
rejected(`date ${wire} must be YYYY-MM-DD.`);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function encodeTime(value, precision) {
|
|
2631
|
+
requireTemporal();
|
|
2632
|
+
if (precision === undefined) {
|
|
2633
|
+
return value.toString();
|
|
2634
|
+
}
|
|
2635
|
+
return value.toString({ fractionalSecondDigits: precision });
|
|
2636
|
+
}
|
|
2637
|
+
function decodeTime(wire) {
|
|
2638
|
+
requireTemporal();
|
|
2639
|
+
try {
|
|
2640
|
+
return Temporal.PlainTime.from(wire);
|
|
2641
|
+
} catch {
|
|
2642
|
+
rejected(`time ${wire} must be a time of day, for example 00:00:00.`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function encodeTimeZone(value, precision) {
|
|
2646
|
+
requireTemporal();
|
|
2647
|
+
if (!offsetText().test(value.offset)) {
|
|
2648
|
+
rejected(`timetz offset ${value.offset} must be ±HH:MM.`);
|
|
2649
|
+
}
|
|
2650
|
+
return `${encodeTime(value.time, precision)}${value.offset}`;
|
|
2651
|
+
}
|
|
2652
|
+
function decodeTimeZone(wire) {
|
|
2653
|
+
const match = timeZoneText().exec(wire);
|
|
2654
|
+
const time = match?.[1];
|
|
2655
|
+
const hours = match?.[2];
|
|
2656
|
+
if (time === undefined || hours === undefined) {
|
|
2657
|
+
rejected(`timetz ${wire} must be a time of day plus ±HH or ±HH:MM, for example 00:00:00+00.`);
|
|
2658
|
+
}
|
|
2659
|
+
return { time: decodeTime(time), offset: `${hours}:${match?.[3] ?? "00"}` };
|
|
2660
|
+
}
|
|
2661
|
+
function encodeDuration(value) {
|
|
2662
|
+
requireTemporal();
|
|
2663
|
+
const text = value.toString();
|
|
2664
|
+
return text.startsWith("-") ? text.slice(1).replace(/\d+(?:\.\d+)?(?=[A-Z])/g, "-$&") : text;
|
|
2665
|
+
}
|
|
2666
|
+
function decodeDuration(wire) {
|
|
2667
|
+
requireTemporal();
|
|
2668
|
+
const fields = {};
|
|
2669
|
+
const rest = wire.replace(/([+-]?\d+) (year|mon|day)s?|([+-]?)(\d+):(\d{2}):(\d{2})(?:\.(\d{1,6}))?/g, (_part, count, unit, sign, h = "", m = "", s = "", f = "") => {
|
|
2670
|
+
if (count !== undefined && unit !== undefined) {
|
|
2671
|
+
fields[`${unit === "mon" ? "month" : unit}s`] = Number(count);
|
|
2672
|
+
return "";
|
|
2673
|
+
}
|
|
2674
|
+
const micros = Number(f.padEnd(6, "0"));
|
|
2675
|
+
const flip = sign === "-" ? -1 : 1;
|
|
2676
|
+
fields.hours = flip * Number(h);
|
|
2677
|
+
fields.minutes = flip * Number(m);
|
|
2678
|
+
fields.seconds = flip * Number(s);
|
|
2679
|
+
fields.milliseconds = flip * Math.trunc(micros / 1000);
|
|
2680
|
+
fields.microseconds = flip * (micros % 1000);
|
|
2681
|
+
return "";
|
|
2682
|
+
});
|
|
2683
|
+
try {
|
|
2684
|
+
return Temporal.Duration.from(rest.trim() === "" && wire !== "" ? fields : wire);
|
|
2685
|
+
} catch {
|
|
2686
|
+
rejected(`interval ${wire} must be what Postgres sends (for example 01:30:00 or 1 day 02:03:04) or an ISO-8601 duration (PT1H). Parts of different signs are not one Temporal.Duration.`);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
function intervalFields(fields) {
|
|
2690
|
+
switch (fields) {
|
|
2691
|
+
case "year":
|
|
2692
|
+
case "month":
|
|
2693
|
+
case "day":
|
|
2694
|
+
case "hour":
|
|
2695
|
+
case "minute":
|
|
2696
|
+
case "second":
|
|
2697
|
+
case "year to month":
|
|
2698
|
+
case "day to hour":
|
|
2699
|
+
case "day to minute":
|
|
2700
|
+
case "day to second":
|
|
2701
|
+
case "hour to minute":
|
|
2702
|
+
case "hour to second":
|
|
2703
|
+
case "minute to second":
|
|
2704
|
+
return fields;
|
|
2705
|
+
default:
|
|
2706
|
+
definition(`interval fields ${fields} must be one of: year, month, day, hour, minute, second, year to month, day to hour, day to minute, day to second, hour to minute, hour to second, minute to second.`);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
function offsetText() {
|
|
2710
|
+
offsetPattern ??= /^[+-]\d{2}:\d{2}$/;
|
|
2711
|
+
return offsetPattern;
|
|
2712
|
+
}
|
|
2713
|
+
function timeZoneText() {
|
|
2714
|
+
timeZonePattern ??= /^(.+?)([+-]\d{2})(?::(\d{2}))?$/;
|
|
2715
|
+
return timeZonePattern;
|
|
2716
|
+
}
|
|
2717
|
+
var offsetPattern;
|
|
2718
|
+
var timeZonePattern;
|
|
2719
|
+
|
|
2720
|
+
// src/dialects/pg/range.ts
|
|
2721
|
+
function tstzrange() {
|
|
2722
|
+
return rangeColumn("tstzrange", encodeInstant, decodeInstant, true);
|
|
2723
|
+
}
|
|
2724
|
+
function daterange() {
|
|
2725
|
+
return rangeColumn("daterange", encodeDate, decodeDate, true);
|
|
2726
|
+
}
|
|
2727
|
+
function int4range() {
|
|
2728
|
+
return rangeColumn("int4range", (value) => integerToken(value, -2147483648, 2147483647, "int4range"), (wire) => decodeBoundNumber(wire, "int4range"), false);
|
|
2729
|
+
}
|
|
2730
|
+
function int8range() {
|
|
2731
|
+
return rangeColumn("int8range", encodeBigintString, decodeBigintString, false);
|
|
2732
|
+
}
|
|
2733
|
+
function numrange() {
|
|
2734
|
+
return rangeColumn("numrange", encodeNumericString, decodeNumericString, false);
|
|
2735
|
+
}
|
|
2736
|
+
function rangeColumn(baseType, encodeBound, decodeBound, quote) {
|
|
2737
|
+
return required({
|
|
2738
|
+
baseType,
|
|
2739
|
+
encode: (value) => writeRange(value, encodeBound, quote),
|
|
2740
|
+
decode: (wire) => readRange(wire, decodeBound, quote),
|
|
2741
|
+
accepts: ["Object"],
|
|
2742
|
+
sqlForm: "cast"
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
function writeRange(value, encodeBound, quote) {
|
|
2746
|
+
if (value.empty) {
|
|
2747
|
+
return "empty";
|
|
2748
|
+
}
|
|
2749
|
+
if (value.empty !== false) {
|
|
2750
|
+
rejected("range needs empty: true, or empty: false with lower and upper.");
|
|
2751
|
+
}
|
|
2752
|
+
const lower = value.lower === null ? "" : boundText(encodeBound(value.lower), quote);
|
|
2753
|
+
const upper = value.upper === null ? "" : boundText(encodeBound(value.upper), quote);
|
|
2754
|
+
const open = value.lowerInclusive ? "[" : "(";
|
|
2755
|
+
const close = value.upperInclusive ? "]" : ")";
|
|
2756
|
+
return `${open}${lower},${upper}${close}`;
|
|
2757
|
+
}
|
|
2758
|
+
function boundText(encoded, quote) {
|
|
2759
|
+
if (!quote) {
|
|
2760
|
+
return encoded;
|
|
2761
|
+
}
|
|
2762
|
+
return `"${encoded.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
2763
|
+
}
|
|
2764
|
+
function readRange(wire, decodeBound, quote) {
|
|
2765
|
+
if (wire === "empty") {
|
|
2766
|
+
return { empty: true };
|
|
2767
|
+
}
|
|
2768
|
+
const open = wire[0];
|
|
2769
|
+
const close = wire[wire.length - 1];
|
|
2770
|
+
if (open !== "[" && open !== "(" || close !== "]" && close !== ")") {
|
|
2771
|
+
rejected(`range ${wire} is not a range literal.`);
|
|
2772
|
+
}
|
|
2773
|
+
const body = wire.slice(1, -1);
|
|
2774
|
+
const split = splitBound(body);
|
|
2775
|
+
if (split === undefined) {
|
|
2776
|
+
rejected(`range ${wire} is not a range literal.`);
|
|
2777
|
+
}
|
|
2778
|
+
return {
|
|
2779
|
+
empty: false,
|
|
2780
|
+
lowerInclusive: open === "[",
|
|
2781
|
+
upperInclusive: close === "]",
|
|
2782
|
+
lower: split.lower.length === 0 ? null : decodeBound(unquote(split.lower, quote)),
|
|
2783
|
+
upper: split.upper.length === 0 ? null : decodeBound(unquote(split.upper, quote))
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
function splitBound(body) {
|
|
2787
|
+
let quoted = false;
|
|
2788
|
+
for (let index = 0;index < body.length; index += 1) {
|
|
2789
|
+
const char = body[index];
|
|
2790
|
+
if (char === "\\") {
|
|
2791
|
+
index += 1;
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (char === '"') {
|
|
2795
|
+
quoted = !quoted;
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2798
|
+
if (char === "," && !quoted) {
|
|
2799
|
+
return { lower: body.slice(0, index), upper: body.slice(index + 1) };
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
function unquote(token, quote) {
|
|
2805
|
+
if (!quote) {
|
|
2806
|
+
return token;
|
|
2807
|
+
}
|
|
2808
|
+
if (!token.startsWith('"') || !token.endsWith('"')) {
|
|
2809
|
+
rejected(`range bound ${token} is not quoted.`);
|
|
2810
|
+
}
|
|
2811
|
+
return token.slice(1, -1).replaceAll("\\\"", '"').replaceAll("\\\\", "\\");
|
|
2812
|
+
}
|
|
2813
|
+
function decodeBoundNumber(wire, role) {
|
|
2814
|
+
const value = Number(wire);
|
|
2815
|
+
integerToken(value, -2147483648, 2147483647, role);
|
|
2816
|
+
return value;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// src/dialects/pg/search.ts
|
|
2820
|
+
function tsvector() {
|
|
2821
|
+
return required({
|
|
2822
|
+
baseType: "tsvector",
|
|
2823
|
+
encode: encodeText,
|
|
2824
|
+
decode: decodeText,
|
|
2825
|
+
sqlForm: "quote"
|
|
2826
|
+
});
|
|
2827
|
+
}
|
|
2828
|
+
function ltree() {
|
|
2829
|
+
return required({
|
|
2830
|
+
baseType: "ltree",
|
|
2831
|
+
encode: encodeLtree,
|
|
2832
|
+
decode: encodeLtree,
|
|
2833
|
+
sqlForm: "quote",
|
|
2834
|
+
extension: "ltree"
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
function encodeLtree(value) {
|
|
2838
|
+
if (!ltreeText().test(value)) {
|
|
2839
|
+
rejected(`ltree ${value} must be labels of letters, digits, or underscores separated by dots, for example top.science.`);
|
|
2840
|
+
}
|
|
2841
|
+
return value;
|
|
2842
|
+
}
|
|
2843
|
+
function ltreeText() {
|
|
2844
|
+
ltreePattern ??= /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)*$/;
|
|
2845
|
+
return ltreePattern;
|
|
2846
|
+
}
|
|
2847
|
+
var ltreePattern;
|
|
2848
|
+
|
|
2849
|
+
// src/dialects/pg/time.ts
|
|
2850
|
+
function timestamptz(precision) {
|
|
2851
|
+
const digits = timePrecision(precision, "timestamptz");
|
|
2852
|
+
return required({
|
|
2853
|
+
baseType: timed("timestamptz", digits),
|
|
2854
|
+
encode: (value) => encodeInstant(value, digits),
|
|
2855
|
+
decode: decodeInstant,
|
|
2856
|
+
accepts: ["Temporal.Instant"],
|
|
2857
|
+
sqlForm: "quote"
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
function timestamp(precision) {
|
|
2861
|
+
const digits = timePrecision(precision, "timestamp");
|
|
2862
|
+
return required({
|
|
2863
|
+
baseType: timed("timestamp", digits),
|
|
2864
|
+
encode: (value) => encodeDateTime(value, digits),
|
|
2865
|
+
decode: decodeDateTime,
|
|
2866
|
+
accepts: ["Temporal.PlainDateTime"],
|
|
2867
|
+
sqlForm: "quote"
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
function date() {
|
|
2871
|
+
return required({
|
|
2872
|
+
baseType: "date",
|
|
2873
|
+
encode: encodeDate,
|
|
2874
|
+
decode: decodeDate,
|
|
2875
|
+
accepts: ["Temporal.PlainDate"],
|
|
2876
|
+
sqlForm: "quote"
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
function time(precision) {
|
|
2880
|
+
const digits = timePrecision(precision, "time");
|
|
2881
|
+
return required({
|
|
2882
|
+
baseType: timed("time", digits),
|
|
2883
|
+
encode: (value) => encodeTime(value, digits),
|
|
2884
|
+
decode: decodeTime,
|
|
2885
|
+
accepts: ["Temporal.PlainTime"],
|
|
2886
|
+
sqlForm: "quote"
|
|
2887
|
+
});
|
|
2888
|
+
}
|
|
2889
|
+
function timetz(precision) {
|
|
2890
|
+
const digits = timePrecision(precision, "timetz");
|
|
2891
|
+
return required({
|
|
2892
|
+
baseType: timed("timetz", digits),
|
|
2893
|
+
encode: (value) => encodeTimeZone(value, digits),
|
|
2894
|
+
decode: decodeTimeZone,
|
|
2895
|
+
accepts: ["Object"],
|
|
2896
|
+
sqlForm: "quote"
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
function interval(fields) {
|
|
2900
|
+
const qualifier = fields === undefined ? undefined : intervalFields(fields);
|
|
2901
|
+
return required({
|
|
2902
|
+
baseType: qualifier === undefined ? "interval" : `interval ${qualifier}`,
|
|
2903
|
+
encode: encodeDuration,
|
|
2904
|
+
decode: decodeDuration,
|
|
2905
|
+
accepts: ["Temporal.Duration"],
|
|
2906
|
+
sqlForm: "quote"
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
function timed(name, precision) {
|
|
2910
|
+
if (precision === undefined) {
|
|
2911
|
+
return name;
|
|
2912
|
+
}
|
|
2913
|
+
return `${name}(${String(precision)})`;
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// src/dialects/pg/namespace.ts
|
|
2917
|
+
var t = {
|
|
2918
|
+
id,
|
|
2919
|
+
identity,
|
|
2920
|
+
uuid,
|
|
2921
|
+
smallint,
|
|
2922
|
+
integer,
|
|
2923
|
+
bigint,
|
|
2924
|
+
numeric,
|
|
2925
|
+
real,
|
|
2926
|
+
double,
|
|
2927
|
+
text,
|
|
2928
|
+
varchar,
|
|
2929
|
+
char,
|
|
2930
|
+
citext,
|
|
2931
|
+
boolean,
|
|
2932
|
+
bytea,
|
|
2933
|
+
json,
|
|
2934
|
+
jsonb,
|
|
2935
|
+
timestamptz,
|
|
2936
|
+
timestamp,
|
|
2937
|
+
date,
|
|
2938
|
+
time,
|
|
2939
|
+
timetz,
|
|
2940
|
+
interval,
|
|
2941
|
+
tstzrange,
|
|
2942
|
+
daterange,
|
|
2943
|
+
int4range,
|
|
2944
|
+
int8range,
|
|
2945
|
+
numrange,
|
|
2946
|
+
inet,
|
|
2947
|
+
cidr,
|
|
2948
|
+
macaddr,
|
|
2949
|
+
macaddr8,
|
|
2950
|
+
point,
|
|
2951
|
+
line,
|
|
2952
|
+
tsvector,
|
|
2953
|
+
ltree,
|
|
2954
|
+
enum: enumColumn,
|
|
2955
|
+
domain,
|
|
2956
|
+
custom
|
|
2957
|
+
};
|
|
2958
|
+
// src/runtime/ids/uuid.ts
|
|
2959
|
+
var HEX = "0123456789abcdef";
|
|
2960
|
+
var uuidv4 = clientGenerator(() => crypto.randomUUID(), "uuidv4");
|
|
2961
|
+
var uuidv7 = clientGenerator(mintUuidV7, "uuidv7");
|
|
2962
|
+
function mintUuidV7() {
|
|
2963
|
+
const bytes = new Uint8Array(16);
|
|
2964
|
+
crypto.getRandomValues(bytes);
|
|
2965
|
+
const now = Date.now();
|
|
2966
|
+
bytes[0] = Math.floor(now / 2 ** 40) & 255;
|
|
2967
|
+
bytes[1] = Math.floor(now / 2 ** 32) & 255;
|
|
2968
|
+
bytes[2] = Math.floor(now / 2 ** 24) & 255;
|
|
2969
|
+
bytes[3] = Math.floor(now / 2 ** 16) & 255;
|
|
2970
|
+
bytes[4] = Math.floor(now / 2 ** 8) & 255;
|
|
2971
|
+
bytes[5] = now & 255;
|
|
2972
|
+
const sixth = bytes[6] ?? 0;
|
|
2973
|
+
const eighth = bytes[8] ?? 0;
|
|
2974
|
+
bytes[6] = sixth & 15 | 112;
|
|
2975
|
+
bytes[8] = eighth & 63 | 128;
|
|
2976
|
+
return formatUuid(bytes);
|
|
2977
|
+
}
|
|
2978
|
+
function formatUuid(bytes) {
|
|
2979
|
+
let out = "";
|
|
2980
|
+
for (let index = 0;index < 16; index += 1) {
|
|
2981
|
+
if (index === 4 || index === 6 || index === 8 || index === 10)
|
|
2982
|
+
out += "-";
|
|
2983
|
+
const byte = bytes[index] ?? 0;
|
|
2984
|
+
out += HEX[byte >> 4];
|
|
2985
|
+
out += HEX[byte & 15];
|
|
2986
|
+
}
|
|
2987
|
+
return out;
|
|
2988
|
+
}
|
|
2989
|
+
// src/dialects/pg/path-endpoint.ts
|
|
2990
|
+
var pathEndpoint = Symbol("okmodel.pathEndpoint");
|
|
2991
|
+
function markPathEndpoint(table) {
|
|
2992
|
+
return Object.assign(table, { [pathEndpoint]: true });
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
// src/runtime/tenancy/index.ts
|
|
2996
|
+
function columnTenancy(input) {
|
|
2997
|
+
if (input === undefined || typeof input !== "object") {
|
|
2998
|
+
definition("columnTenancy() needs { key, type }.");
|
|
2999
|
+
}
|
|
3000
|
+
if (input.type !== "uuid") {
|
|
3001
|
+
definition(`columnTenancy() type ${String(input.type)} must be uuid.`);
|
|
3002
|
+
}
|
|
3003
|
+
if (typeof input.key !== "string" || input.key.length === 0) {
|
|
3004
|
+
definition("columnTenancy() key must be a field name.");
|
|
3005
|
+
}
|
|
3006
|
+
assertIdentifier(input.key, "tenancy key");
|
|
3007
|
+
const key = input.key;
|
|
3008
|
+
const column = uuid().guarded();
|
|
3009
|
+
const encode = column.state.encode;
|
|
3010
|
+
const tenants = new Set;
|
|
3011
|
+
const globals = new Map;
|
|
3012
|
+
const exemptions = new Map;
|
|
3013
|
+
const api = {
|
|
3014
|
+
key,
|
|
3015
|
+
type: "uuid",
|
|
3016
|
+
strategy: "column",
|
|
3017
|
+
rewrite(tables) {
|
|
3018
|
+
tenants.clear();
|
|
3019
|
+
globals.clear();
|
|
3020
|
+
exemptions.clear();
|
|
3021
|
+
return rewriteTables(key, column, encode, api, tables, tenants, globals, exemptions);
|
|
3022
|
+
},
|
|
3023
|
+
column: () => column,
|
|
3024
|
+
predicate(spec) {
|
|
3025
|
+
return writePredicate(key, tenants, spec);
|
|
3026
|
+
},
|
|
3027
|
+
guard(table, field, kind) {
|
|
3028
|
+
if (!tenants.has(table) || field !== key)
|
|
3029
|
+
return;
|
|
3030
|
+
refuseKey(kind, table, field);
|
|
3031
|
+
},
|
|
3032
|
+
stamp(table, rows, scope) {
|
|
3033
|
+
if (!tenants.has(table))
|
|
3034
|
+
return;
|
|
3035
|
+
stampRows(key, table, rows, scope);
|
|
3036
|
+
},
|
|
3037
|
+
client(spec) {
|
|
3038
|
+
const names = spec.scoped ? spec.names : spec.names.filter((name) => !tenants.has(name));
|
|
3039
|
+
if (spec.scoped)
|
|
3040
|
+
return { names };
|
|
3041
|
+
return { names, ...bindClient(key, encode, spec.open) };
|
|
3042
|
+
},
|
|
3043
|
+
rules(table, source, scope) {
|
|
3044
|
+
return tenancyRules(table, source, scope, tenants, globals, exemptions);
|
|
3045
|
+
},
|
|
3046
|
+
scopeView(name) {
|
|
3047
|
+
tenants.add(name);
|
|
3048
|
+
},
|
|
3049
|
+
hook(target, ctx) {
|
|
3050
|
+
if (ctx.tables === undefined || ctx.names === undefined || ctx.open === undefined || ctx.scoped === undefined) {
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
const view = api.client({ names: ctx.names, scoped: ctx.scoped, open: ctx.open });
|
|
3054
|
+
if (!ctx.scoped) {
|
|
3055
|
+
for (const name of ctx.names) {
|
|
3056
|
+
if (view.names.includes(name))
|
|
3057
|
+
continue;
|
|
3058
|
+
delete ctx.tables[name];
|
|
3059
|
+
delete target[name];
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
if (view.for !== undefined)
|
|
3063
|
+
target.for = view.for;
|
|
3064
|
+
if (view.unscoped !== undefined)
|
|
3065
|
+
target.unscoped = view.unscoped;
|
|
3066
|
+
},
|
|
3067
|
+
missing(table) {
|
|
3068
|
+
throw new OkmError("OKM1701", `Table ${table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
return api;
|
|
3072
|
+
}
|
|
3073
|
+
function rewriteTables(key, column, encode, api, tables, tenants, globals, exemptions) {
|
|
3074
|
+
const kind = new Map;
|
|
3075
|
+
const swapped = new Map;
|
|
3076
|
+
const extras = new Map;
|
|
3077
|
+
for (const item of tables) {
|
|
3078
|
+
const options = item.options;
|
|
3079
|
+
const mark = options?.tenancy;
|
|
3080
|
+
if (mark === undefined) {
|
|
3081
|
+
kind.set(item.name, "tenant");
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
if (isGlobal(mark)) {
|
|
3085
|
+
kind.set(item.name, "global");
|
|
3086
|
+
globals.set(item.name, mark.reason);
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
if (isRecord(mark) && typeof mark.rewrite === "function") {
|
|
3090
|
+
swapped.set(item.name, mark.rewrite(item, {
|
|
3091
|
+
tables,
|
|
3092
|
+
keys: [key],
|
|
3093
|
+
encodes: [encode],
|
|
3094
|
+
api,
|
|
3095
|
+
extras
|
|
3096
|
+
}));
|
|
3097
|
+
kind.set(item.name, "path");
|
|
3098
|
+
continue;
|
|
3099
|
+
}
|
|
3100
|
+
definition(`Table ${item.name} tenancy must be global("reason") or via().`);
|
|
3101
|
+
}
|
|
3102
|
+
return tables.map((item) => {
|
|
3103
|
+
const next = swapped.get(item.name);
|
|
3104
|
+
if (next !== undefined)
|
|
3105
|
+
return next;
|
|
3106
|
+
if (kind.get(item.name) !== "tenant") {
|
|
3107
|
+
if (kind.get(item.name) === "global")
|
|
3108
|
+
refuseGlobalReference(item, kind);
|
|
3109
|
+
return item;
|
|
3110
|
+
}
|
|
3111
|
+
return rewriteTenant(key, column, item, tables, kind, tenants, exemptions, extras);
|
|
3112
|
+
});
|
|
3113
|
+
}
|
|
3114
|
+
function refuseGlobalReference(item, kind) {
|
|
3115
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
3116
|
+
if (!(builder instanceof ColumnBuilder))
|
|
3117
|
+
continue;
|
|
3118
|
+
const reference = builder.state.references;
|
|
3119
|
+
if (reference !== undefined && kind.get(reference.table) === "tenant") {
|
|
3120
|
+
throw new OkmError("OKM1705", `Table ${item.name}.${field} is global and references tenant table ${reference.table}. Drop the reference, or make ${item.name} a tenant table.`);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function rewriteTenant(key, column, item, tables, kind, tenants, exemptions, extras) {
|
|
3125
|
+
if (Object.hasOwn(item.columns, key)) {
|
|
3126
|
+
throw new OkmError("OKM1012", `Table ${item.name} already declares ${key}, which tenancy adds. Rename the field or drop it from the table.`);
|
|
3127
|
+
}
|
|
3128
|
+
const options = item.options;
|
|
3129
|
+
const columns = {};
|
|
3130
|
+
const unique = {};
|
|
3131
|
+
const named = options?.unique;
|
|
3132
|
+
if (named !== undefined) {
|
|
3133
|
+
for (const [name, fields] of Object.entries(named)) {
|
|
3134
|
+
unique[name] = fields.includes(key) ? fields : [key, ...fields];
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
const notes = [];
|
|
3138
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
3139
|
+
if (!(builder instanceof ColumnBuilder)) {
|
|
3140
|
+
columns[field] = builder;
|
|
3141
|
+
continue;
|
|
3142
|
+
}
|
|
3143
|
+
const patch = {};
|
|
3144
|
+
if (builder.state.primaryKey === true)
|
|
3145
|
+
patch.dropPrimary = true;
|
|
3146
|
+
const flag = builder.state.unique;
|
|
3147
|
+
if (flag !== undefined) {
|
|
3148
|
+
if (flag.global === true) {
|
|
3149
|
+
const reason = flag.reason?.trim() ?? "";
|
|
3150
|
+
if (reason.length === 0) {
|
|
3151
|
+
definition(`Unique ${item.name}.${field} sets global and needs a reason.`);
|
|
3152
|
+
}
|
|
3153
|
+
notes.push({ name: field, reason });
|
|
3154
|
+
} else {
|
|
3155
|
+
patch.dropUnique = true;
|
|
3156
|
+
unique[field] = [key, field];
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const reference = builder.state.references;
|
|
3160
|
+
if (reference !== undefined) {
|
|
3161
|
+
const widened = widenReference(key, item.name, field, reference, tables, kind);
|
|
3162
|
+
if (widened !== undefined)
|
|
3163
|
+
patch.references = widened;
|
|
3164
|
+
}
|
|
3165
|
+
const changed = patch.dropUnique === true || patch.dropPrimary === true || patch.references !== undefined;
|
|
3166
|
+
columns[field] = changed ? retarget(builder, patch) : builder;
|
|
3167
|
+
}
|
|
3168
|
+
const primary = primaryFields(item);
|
|
3169
|
+
const primaryKey = primary.length > 0 && !primary.includes(key) ? [...primary, key] : undefined;
|
|
3170
|
+
if (primaryKey !== undefined)
|
|
3171
|
+
unique[`${primary.join("_")}_${key}`] = primaryKey;
|
|
3172
|
+
const noted = extras.get(item.name);
|
|
3173
|
+
if (noted !== undefined && noted.length > 0 && unique[noted.join("_")] === undefined) {
|
|
3174
|
+
unique[noted.join("_")] = noted;
|
|
3175
|
+
}
|
|
3176
|
+
columns[key] = column;
|
|
3177
|
+
tenants.add(item.name);
|
|
3178
|
+
if (notes.length > 0)
|
|
3179
|
+
exemptions.set(item.name, notes);
|
|
3180
|
+
const indexes = wrapIndexes(key, item.name, options?.indexes);
|
|
3181
|
+
const rewritten = {
|
|
3182
|
+
...item,
|
|
3183
|
+
columns,
|
|
3184
|
+
options: {
|
|
3185
|
+
...options,
|
|
3186
|
+
...primaryKey !== undefined ? { primaryKey } : {},
|
|
3187
|
+
...Object.keys(unique).length > 0 ? { unique } : {},
|
|
3188
|
+
...indexes !== undefined ? { indexes } : {},
|
|
3189
|
+
tenantKeys: [key]
|
|
3190
|
+
}
|
|
3191
|
+
};
|
|
3192
|
+
return noted !== undefined && noted.length > 0 ? markPathEndpoint(rewritten) : rewritten;
|
|
3193
|
+
}
|
|
3194
|
+
function widenReference(key, table, field, reference, tables, kind) {
|
|
3195
|
+
const targetKind = kind.get(reference.table);
|
|
3196
|
+
if (targetKind !== "tenant")
|
|
3197
|
+
return;
|
|
3198
|
+
const action = [reference.onDelete, reference.onUpdate].find((item) => item === "set null" || item === "set default");
|
|
3199
|
+
if (action !== undefined) {
|
|
3200
|
+
definition(`Foreign key ${table}.${field} includes ${key}, so ${action} cannot clear it. Accepted actions: cascade, restrict, no action.`);
|
|
3201
|
+
}
|
|
3202
|
+
const target = tables.find((item) => item.name === reference.table);
|
|
3203
|
+
const remote = reference.columns ?? (target === undefined ? undefined : primaryFields(target));
|
|
3204
|
+
if (remote === undefined || remote.length === 0)
|
|
3205
|
+
return;
|
|
3206
|
+
if (remote.includes(key)) {
|
|
3207
|
+
if (reference.along?.includes(key))
|
|
3208
|
+
return;
|
|
3209
|
+
return { ...reference, along: [...reference.along ?? [], key] };
|
|
3210
|
+
}
|
|
3211
|
+
return {
|
|
3212
|
+
...reference,
|
|
3213
|
+
columns: [...remote, key],
|
|
3214
|
+
along: [...reference.along ?? [], key]
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
function primaryFields(item) {
|
|
3218
|
+
const listed = item.options?.primaryKey;
|
|
3219
|
+
if (listed !== undefined && listed.length > 0)
|
|
3220
|
+
return [...listed];
|
|
3221
|
+
const fields = [];
|
|
3222
|
+
for (const [field, builder] of Object.entries(item.columns)) {
|
|
3223
|
+
if (builder instanceof ColumnBuilder && builder.state.primaryKey === true)
|
|
3224
|
+
fields.push(field);
|
|
3225
|
+
}
|
|
3226
|
+
return fields;
|
|
3227
|
+
}
|
|
3228
|
+
function wrapIndexes(key, table, build) {
|
|
3229
|
+
if (build === undefined)
|
|
3230
|
+
return;
|
|
3231
|
+
return (columns) => {
|
|
3232
|
+
const calls = build(columns);
|
|
3233
|
+
const tenantSql = columns[key]?.name;
|
|
3234
|
+
if (!Array.isArray(calls) || tenantSql === undefined)
|
|
3235
|
+
return calls;
|
|
3236
|
+
for (const call of calls) {
|
|
3237
|
+
if (call.columns[0] === tenantSql)
|
|
3238
|
+
continue;
|
|
3239
|
+
throw new OkmError("OKM1706", `Index on ${table} starts with ${call.columns[0] ?? "no column"}. Put ${tenantSql} first.`);
|
|
3240
|
+
}
|
|
3241
|
+
return calls;
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
function writePredicate(key, tenants, input) {
|
|
3245
|
+
if (!tenants.has(input.table))
|
|
3246
|
+
return false;
|
|
3247
|
+
const scope = input.scope;
|
|
3248
|
+
if (scope === undefined) {
|
|
3249
|
+
throw new OkmError("OKM1701", `Table ${input.table} is tenant-scoped. Call for({ ${key} }) or unscoped("reason").`);
|
|
3250
|
+
}
|
|
3251
|
+
if ("unscoped" in scope) {
|
|
3252
|
+
input.sink.mark("unscoped|");
|
|
3253
|
+
return false;
|
|
3254
|
+
}
|
|
3255
|
+
const sql = input.fieldSql(key);
|
|
3256
|
+
if (sql === undefined) {
|
|
3257
|
+
throw new OkmError("OKM1701", `Table ${input.table} is missing its tenant key ${key}.`);
|
|
3258
|
+
}
|
|
3259
|
+
input.sink.text(input.appended ? " and " : " where ");
|
|
3260
|
+
input.sink.text(input.alias);
|
|
3261
|
+
input.sink.text(".");
|
|
3262
|
+
input.sink.text(quote(sql));
|
|
3263
|
+
input.sink.text(" = ");
|
|
3264
|
+
input.sink.param(input.encode === undefined ? scope.value : input.encode(scope.value));
|
|
3265
|
+
input.sink.mark("tenant|");
|
|
3266
|
+
return true;
|
|
3267
|
+
}
|
|
3268
|
+
function refuseKey(kind, table, field) {
|
|
3269
|
+
if (kind === "insert") {
|
|
3270
|
+
throw new OkmError("OKM1190", `Field ${table}.${field} is the tenant key. The scope sets it. Input cannot.`);
|
|
3271
|
+
}
|
|
3272
|
+
if (kind === "update") {
|
|
3273
|
+
throw new OkmError("OKM1704", `Field ${table}.${field} is the tenant key. An update cannot change it.`);
|
|
3274
|
+
}
|
|
3275
|
+
throw new OkmError("OKM1704", `Field ${table}.${field} is the tenant key. A filter cannot set it.`);
|
|
3276
|
+
}
|
|
3277
|
+
function stampRows(key, table, rows, scope) {
|
|
3278
|
+
if (scope === undefined || "unscoped" in scope) {
|
|
3279
|
+
throw new OkmError("OKM1701", `insert on ${table} has no tenant. Call for({ ${key} }) so the scope can set it.`);
|
|
3280
|
+
}
|
|
3281
|
+
for (const row of rows)
|
|
3282
|
+
row[key] = scope.value;
|
|
3283
|
+
}
|
|
3284
|
+
function bindClient(key, encode, openScope) {
|
|
3285
|
+
return {
|
|
3286
|
+
for: (input) => {
|
|
3287
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
3288
|
+
throw new OkmError("OKM1701", `for() needs { ${key} }.`);
|
|
3289
|
+
}
|
|
3290
|
+
const record = input;
|
|
3291
|
+
const value = record[key];
|
|
3292
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3293
|
+
throw new OkmError("OKM1701", `for() needs { ${key} } as a uuid.`);
|
|
3294
|
+
}
|
|
3295
|
+
for (const name of Object.keys(record)) {
|
|
3296
|
+
if (name !== key) {
|
|
3297
|
+
throw new OkmError("OKM1701", `for() accepts ${key}. ${name} is not the tenant key.`);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
encode(value);
|
|
3301
|
+
return openScope({ value });
|
|
3302
|
+
},
|
|
3303
|
+
unscoped: (reason) => {
|
|
3304
|
+
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
3305
|
+
throw new OkmError("OKM1701", "unscoped() needs a reason.");
|
|
3306
|
+
}
|
|
3307
|
+
return openScope({ unscoped: reason.trim() });
|
|
3308
|
+
}
|
|
3309
|
+
};
|
|
3310
|
+
}
|
|
3311
|
+
function tenancyRules(table, source, scope, tenants, globals, exemptions) {
|
|
3312
|
+
const rules = [];
|
|
3313
|
+
const reason = globals.get(table);
|
|
3314
|
+
if (reason !== undefined) {
|
|
3315
|
+
rules.push({
|
|
3316
|
+
rule: "tenancy",
|
|
3317
|
+
contribution: `global ${reason}`,
|
|
3318
|
+
provenance: "catalog",
|
|
3319
|
+
...source !== undefined ? { source } : {}
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
const notes = exemptions.get(table);
|
|
3323
|
+
if (notes !== undefined) {
|
|
3324
|
+
for (const item of notes) {
|
|
3325
|
+
rules.push({
|
|
3326
|
+
rule: "tenancy",
|
|
3327
|
+
contribution: `unique ${item.name} global ${item.reason}`,
|
|
3328
|
+
provenance: "catalog",
|
|
3329
|
+
...source !== undefined ? { source } : {}
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (tenants.has(table) && scope !== undefined && "value" in scope) {
|
|
3334
|
+
rules.push({ rule: "tenancy", contribution: "scoped", provenance: "planner" });
|
|
3335
|
+
}
|
|
3336
|
+
if (scope !== undefined && "unscoped" in scope) {
|
|
3337
|
+
rules.push({
|
|
3338
|
+
rule: "tenancy",
|
|
3339
|
+
contribution: `unscoped ${scope.unscoped}`,
|
|
3340
|
+
provenance: "caller"
|
|
3341
|
+
});
|
|
3342
|
+
}
|
|
3343
|
+
return rules;
|
|
3344
|
+
}
|
|
3345
|
+
function isGlobal(value) {
|
|
3346
|
+
return isRecord(value) && value.kind === "global" && typeof value.reason === "string";
|
|
3347
|
+
}
|
|
3348
|
+
function isRecord(value) {
|
|
3349
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// packages/harness/src/proxy.ts
|
|
3353
|
+
import { createServer, connect } from "node:net";
|
|
3354
|
+
async function startCutProxy(target) {
|
|
3355
|
+
const upstream = new URL(target);
|
|
3356
|
+
let armed = false;
|
|
3357
|
+
const sockets = new Set;
|
|
3358
|
+
const server = createServer((client) => {
|
|
3359
|
+
const remote = connect({ host: upstream.hostname, port: Number(upstream.port) });
|
|
3360
|
+
let cut = false;
|
|
3361
|
+
sockets.add(client).add(remote);
|
|
3362
|
+
client.on("data", (chunk) => {
|
|
3363
|
+
if (armed && chunk.includes("COMMIT")) {
|
|
3364
|
+
armed = false;
|
|
3365
|
+
cut = true;
|
|
3366
|
+
}
|
|
3367
|
+
remote.write(chunk);
|
|
3368
|
+
});
|
|
3369
|
+
remote.on("data", (chunk) => {
|
|
3370
|
+
if (cut) {
|
|
3371
|
+
cut = false;
|
|
3372
|
+
client.destroy();
|
|
3373
|
+
return;
|
|
3374
|
+
}
|
|
3375
|
+
if (!client.destroyed)
|
|
3376
|
+
client.write(chunk);
|
|
3377
|
+
});
|
|
3378
|
+
for (const socket of [client, remote]) {
|
|
3379
|
+
socket.on("error", () => {
|
|
3380
|
+
client.destroy();
|
|
3381
|
+
remote.destroy();
|
|
3382
|
+
});
|
|
3383
|
+
socket.on("close", () => sockets.delete(socket));
|
|
3384
|
+
}
|
|
3385
|
+
client.on("close", () => remote.end());
|
|
3386
|
+
remote.on("close", () => client.destroy());
|
|
3387
|
+
});
|
|
3388
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
3389
|
+
const address = server.address();
|
|
3390
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
3391
|
+
const url = new URL(target);
|
|
3392
|
+
url.hostname = "127.0.0.1";
|
|
3393
|
+
url.port = String(port);
|
|
3394
|
+
return {
|
|
3395
|
+
url: url.toString(),
|
|
3396
|
+
cutNextCommit() {
|
|
3397
|
+
armed = true;
|
|
3398
|
+
},
|
|
3399
|
+
close() {
|
|
3400
|
+
for (const socket of sockets)
|
|
3401
|
+
socket.destroy();
|
|
3402
|
+
return new Promise((resolve) => server.close(() => resolve()));
|
|
3403
|
+
}
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
// packages/harness/src/schema-name.ts
|
|
3408
|
+
function isolatedSchemaName() {
|
|
3409
|
+
return `s_${crypto.randomUUID().replaceAll("-", "")}`;
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
// packages/harness/src/tx-suite.ts
|
|
3413
|
+
var accounts = table2("accounts", { id: t.text().primaryKey(), balance: t.integer() });
|
|
3414
|
+
var events = table2("events", { id: t.text().primaryKey(), note: t.text() });
|
|
3415
|
+
var app = schema({ casing: "snake", tables: [accounts, events] });
|
|
3416
|
+
var TENANT_A = "01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c8d";
|
|
3417
|
+
var TENANT_B = "01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c8e";
|
|
3418
|
+
var notes = table2("notes", { id: id({ default: "none" }), body: text() });
|
|
3419
|
+
var tenantApp = schema({
|
|
3420
|
+
casing: "snake",
|
|
3421
|
+
tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
|
|
3422
|
+
tables: [notes]
|
|
3423
|
+
});
|
|
3424
|
+
var noteRows = table2("notes", { id: t.text().primaryKey(), body: t.text() });
|
|
3425
|
+
var outbox = table2("outbox", { id: t.text().primaryKey(), kind: t.text() });
|
|
3426
|
+
var slots = table2("slots", { id: t.text().primaryKey(), label: t.text() });
|
|
3427
|
+
var issued = table2("issued", { id: id({ default: uuidv4 }), body: t.text() });
|
|
3428
|
+
var ledger = table2("ledger", { id: t.text().primaryKey(), balance: t.integer() });
|
|
3429
|
+
var rowsApp = schema({
|
|
3430
|
+
casing: "snake",
|
|
3431
|
+
tables: [noteRows, outbox, slots, issued, ledger]
|
|
3432
|
+
});
|
|
3433
|
+
function registerTxSuite(suite) {
|
|
3434
|
+
const { name } = suite;
|
|
3435
|
+
const test = suite.test;
|
|
3436
|
+
const needsSecond = suite.memory ? skipped(test, "one connection") : test;
|
|
3437
|
+
const cancels = suite.capabilities.cancel && !suite.memory ? test : skipped(test, "no cancel");
|
|
3438
|
+
const race = needsSecond;
|
|
3439
|
+
const proxied = suite.memory || suite.cut === false ? skipped(test, "no socket to cut") : test;
|
|
3440
|
+
async function withEnv(built, options, fn) {
|
|
3441
|
+
const schemaName = suite.memory ? "public" : isolatedSchemaName();
|
|
3442
|
+
const admin = suite.memory ? undefined : await openSuiteAdmin(suite.url);
|
|
3443
|
+
const pools = [];
|
|
3444
|
+
try {
|
|
3445
|
+
if (admin !== undefined)
|
|
3446
|
+
await admin.unsafe(`create schema ${schemaName}`);
|
|
3447
|
+
const open = async (config) => {
|
|
3448
|
+
const url = config.url ?? suite.url;
|
|
3449
|
+
const pool = await suite.open({
|
|
3450
|
+
schema: schemaName,
|
|
3451
|
+
max: config.max,
|
|
3452
|
+
...options.timeouts !== undefined ? { timeouts: options.timeouts } : {},
|
|
3453
|
+
...url !== undefined ? { url } : {}
|
|
3454
|
+
});
|
|
3455
|
+
pools.push(pool);
|
|
3456
|
+
return pool;
|
|
3457
|
+
};
|
|
3458
|
+
const setup = options.url === undefined || suite.memory ? undefined : await open({ max: 1 });
|
|
3459
|
+
const pool = await open({
|
|
3460
|
+
max: options.max ?? 4,
|
|
3461
|
+
...options.url ? { url: options.url } : {}
|
|
3462
|
+
});
|
|
3463
|
+
const ddl = [...renderCatalog2(built.catalog, schemaName), ...options.sql ?? []];
|
|
3464
|
+
for (const statement of ddl)
|
|
3465
|
+
await (setup ?? pool).execute(statement);
|
|
3466
|
+
const db = await suite.connect(pool, {
|
|
3467
|
+
schema: built,
|
|
3468
|
+
...options.timeouts !== undefined ? { timeouts: options.timeouts } : {},
|
|
3469
|
+
...options.hookm !== undefined ? { hookm: options.hookm } : {},
|
|
3470
|
+
...options.generators !== undefined ? { generators: options.generators } : {}
|
|
3471
|
+
});
|
|
3472
|
+
try {
|
|
3473
|
+
await fn({
|
|
3474
|
+
db,
|
|
3475
|
+
pool,
|
|
3476
|
+
schemaName,
|
|
3477
|
+
direct: () => open({ max: 1 })
|
|
3478
|
+
});
|
|
3479
|
+
} finally {
|
|
3480
|
+
await db.close();
|
|
3481
|
+
}
|
|
3482
|
+
} finally {
|
|
3483
|
+
for (const pool of pools)
|
|
3484
|
+
await pool.close().catch(() => {
|
|
3485
|
+
return;
|
|
3486
|
+
});
|
|
3487
|
+
if (admin !== undefined) {
|
|
3488
|
+
await admin.unsafe(`drop schema if exists ${schemaName} cascade`).catch(() => {
|
|
3489
|
+
return;
|
|
3490
|
+
});
|
|
3491
|
+
await admin.end({ timeout: 5 });
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
const withApp = (options, fn) => withEnv(app, options, fn);
|
|
3496
|
+
async function seed(db) {
|
|
3497
|
+
await db.accounts.insert({ id: "a", balance: 100 });
|
|
3498
|
+
await db.accounts.insert({ id: "b", balance: 100 });
|
|
3499
|
+
}
|
|
3500
|
+
const balances = async (db) => {
|
|
3501
|
+
const rows = await db.accounts.find({ limit: 100 });
|
|
3502
|
+
return Object.fromEntries(rows.map((row) => [row.id, row.balance]));
|
|
3503
|
+
};
|
|
3504
|
+
test(`${name} tx commits its writes and returns the callback's value`, async () => {
|
|
3505
|
+
await withApp({}, async ({ db }) => {
|
|
3506
|
+
const value = await db.tx(async (tx) => {
|
|
3507
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3508
|
+
await tx.accounts.insert({ id: "b", balance: 2 });
|
|
3509
|
+
const inside = await tx.accounts.find({ limit: 10 });
|
|
3510
|
+
return inside.length;
|
|
3511
|
+
});
|
|
3512
|
+
expect(value).toBe(2);
|
|
3513
|
+
expect(await balances(db)).toEqual({ a: 1, b: 2 });
|
|
3514
|
+
});
|
|
3515
|
+
});
|
|
3516
|
+
test(`${name} tx rolls back on a throw and the caller's own error comes back as thrown`, async () => {
|
|
3517
|
+
await withApp({}, async ({ db }) => {
|
|
3518
|
+
const boom = new TypeError("mine");
|
|
3519
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3520
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3521
|
+
throw boom;
|
|
3522
|
+
}));
|
|
3523
|
+
expect(error).toBe(boom);
|
|
3524
|
+
expect(await balances(db)).toEqual({});
|
|
3525
|
+
});
|
|
3526
|
+
});
|
|
3527
|
+
test(`${name} tx refuses to commit after a handled statement error`, async () => {
|
|
3528
|
+
await withApp({}, async ({ db }) => {
|
|
3529
|
+
await seed(db);
|
|
3530
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3531
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 5 } });
|
|
3532
|
+
await tx.accounts.insert({ id: "b", balance: 9 }).catch(() => {
|
|
3533
|
+
return;
|
|
3534
|
+
});
|
|
3535
|
+
}));
|
|
3536
|
+
expect(error).toBeInstanceOf(OkmError);
|
|
3537
|
+
expect(error.kind).toBe("unique");
|
|
3538
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3539
|
+
});
|
|
3540
|
+
});
|
|
3541
|
+
test(`${name} a nested tx is a savepoint: the inner failure rolls back only the inner writes`, async () => {
|
|
3542
|
+
await withApp({}, async ({ db }) => {
|
|
3543
|
+
await db.tx(async (tx) => {
|
|
3544
|
+
await tx.accounts.insert({ id: "x", balance: 1 });
|
|
3545
|
+
const inner = new Error("inner");
|
|
3546
|
+
const error = await failure2(tx.tx(async (nested) => {
|
|
3547
|
+
await nested.accounts.insert({ id: "y", balance: 2 });
|
|
3548
|
+
throw inner;
|
|
3549
|
+
}));
|
|
3550
|
+
expect(error).toBe(inner);
|
|
3551
|
+
await tx.accounts.insert({ id: "z", balance: 3 });
|
|
3552
|
+
});
|
|
3553
|
+
expect(await balances(db)).toEqual({ x: 1, z: 3 });
|
|
3554
|
+
});
|
|
3555
|
+
});
|
|
3556
|
+
test(`${name} a nested tx that swallows its own failed statement rolls back and throws it`, async () => {
|
|
3557
|
+
await withApp({}, async ({ db }) => {
|
|
3558
|
+
await seed(db);
|
|
3559
|
+
await db.tx(async (tx) => {
|
|
3560
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 7 } });
|
|
3561
|
+
const error = await failure2(tx.tx(async (nested) => {
|
|
3562
|
+
await nested.accounts.insert({ id: "b", balance: 1 }).catch(() => {
|
|
3563
|
+
return;
|
|
3564
|
+
});
|
|
3565
|
+
}));
|
|
3566
|
+
expect(error.kind).toBe("unique");
|
|
3567
|
+
await tx.accounts.update({ where: { id: "b" }, set: { balance: 8 } });
|
|
3568
|
+
});
|
|
3569
|
+
expect(await balances(db)).toEqual({ a: 7, b: 8 });
|
|
3570
|
+
});
|
|
3571
|
+
});
|
|
3572
|
+
test(`${name} nested tx refuses isolation, retry, timeout, and signal, which belong to the outermost tx`, async () => {
|
|
3573
|
+
await withApp({}, async ({ db }) => {
|
|
3574
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3575
|
+
await tx.tx({ isolation: "serializable" }, async () => {
|
|
3576
|
+
return;
|
|
3577
|
+
});
|
|
3578
|
+
}));
|
|
3579
|
+
expect(error.code).toBe("OKM1121");
|
|
3580
|
+
});
|
|
3581
|
+
});
|
|
3582
|
+
test(`${name} a client the callback leaked cannot touch the connection after the tx ends`, async () => {
|
|
3583
|
+
await withApp({ max: 1 }, async ({ db }) => {
|
|
3584
|
+
let leaked;
|
|
3585
|
+
await db.tx(async (tx) => {
|
|
3586
|
+
leaked = tx;
|
|
3587
|
+
});
|
|
3588
|
+
const error = await failure2(leaked.accounts.find({ limit: 1 }));
|
|
3589
|
+
expect(error.message).toContain("transaction has finished");
|
|
3590
|
+
expect(await balances(db)).toEqual({});
|
|
3591
|
+
});
|
|
3592
|
+
});
|
|
3593
|
+
test(`${name} batch is atomic and returns results in order`, async () => {
|
|
3594
|
+
await withApp({}, async ({ db }) => {
|
|
3595
|
+
const results = await db.batch([
|
|
3596
|
+
db.accounts.insert({ id: "a", balance: 1 }),
|
|
3597
|
+
db.accounts.insert({ id: "b", balance: 2 }),
|
|
3598
|
+
db.accounts.update({ where: { id: "a" }, set: { balance: 10 } })
|
|
3599
|
+
]);
|
|
3600
|
+
expect(results).toHaveLength(3);
|
|
3601
|
+
expect(results[0]).toMatchObject({ id: "a", balance: 1 });
|
|
3602
|
+
expect(results[1]).toMatchObject({ id: "b", balance: 2 });
|
|
3603
|
+
expect(results[2]).toMatchObject({ count: 1 });
|
|
3604
|
+
expect(await balances(db)).toEqual({ a: 10, b: 2 });
|
|
3605
|
+
});
|
|
3606
|
+
});
|
|
3607
|
+
test(`${name} a failing batch rolls everything back and names the operation`, async () => {
|
|
3608
|
+
await withApp({}, async ({ db }) => {
|
|
3609
|
+
await seed(db);
|
|
3610
|
+
const error = await failure2(db.batch([
|
|
3611
|
+
db.accounts.update({ where: { id: "a" }, set: { balance: 1 } }),
|
|
3612
|
+
db.accounts.insert({ id: "b", balance: 2 }),
|
|
3613
|
+
db.accounts.update({ where: { id: "b" }, set: { balance: 3 } })
|
|
3614
|
+
]));
|
|
3615
|
+
expect(error.kind).toBe("unique");
|
|
3616
|
+
expect(error.batchIndex).toBe(1);
|
|
3617
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3618
|
+
});
|
|
3619
|
+
});
|
|
3620
|
+
test(`${name} batch inside tx is a savepoint: a failing batch can be handled and the tx survives`, async () => {
|
|
3621
|
+
await withApp({}, async ({ db }) => {
|
|
3622
|
+
await seed(db);
|
|
3623
|
+
await db.tx(async (tx) => {
|
|
3624
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 1 } });
|
|
3625
|
+
const error = await failure2(tx.batch([
|
|
3626
|
+
tx.accounts.update({ where: { id: "a" }, set: { balance: 2 } }),
|
|
3627
|
+
tx.accounts.insert({ id: "b", balance: 3 })
|
|
3628
|
+
]));
|
|
3629
|
+
expect(error.batchIndex).toBe(1);
|
|
3630
|
+
const ok = await tx.batch([
|
|
3631
|
+
tx.accounts.update({ where: { id: "b" }, set: { balance: 4 } })
|
|
3632
|
+
]);
|
|
3633
|
+
expect(ok).toHaveLength(1);
|
|
3634
|
+
});
|
|
3635
|
+
expect(await balances(db)).toEqual({ a: 1, b: 4 });
|
|
3636
|
+
});
|
|
3637
|
+
});
|
|
3638
|
+
test(`${name} batch refuses .replica() (OKM1840) and anything that is not a write`, async () => {
|
|
3639
|
+
await withApp({}, async ({ db }) => {
|
|
3640
|
+
const replica = await failure2(db.batch([db.accounts.insert({ id: "a", balance: 1 })]).replica());
|
|
3641
|
+
expect(replica.code).toBe("OKM1840");
|
|
3642
|
+
const read = await failure2(db.batch([db.accounts.find({ limit: 1 })]));
|
|
3643
|
+
expect(read.code).toBe("OKM1121");
|
|
3644
|
+
expect(await balances(db)).toEqual({});
|
|
3645
|
+
});
|
|
3646
|
+
});
|
|
3647
|
+
test(`${name} afterCommit runs in order after the commit, never after a rollback`, async () => {
|
|
3648
|
+
await withApp({}, async ({ db, pool }) => {
|
|
3649
|
+
const calls = [];
|
|
3650
|
+
await db.tx(async (tx) => {
|
|
3651
|
+
tx.afterCommit(async () => {
|
|
3652
|
+
const rows = await pool.execute("select count(*) from accounts");
|
|
3653
|
+
calls.push(`first:${String(rows.rows[0]?.[0])}`);
|
|
3654
|
+
});
|
|
3655
|
+
tx.afterCommit(() => {
|
|
3656
|
+
calls.push("second");
|
|
3657
|
+
});
|
|
3658
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3659
|
+
await tx.tx(async (nested) => {
|
|
3660
|
+
nested.afterCommit(() => {
|
|
3661
|
+
calls.push("nested");
|
|
3662
|
+
});
|
|
3663
|
+
});
|
|
3664
|
+
const failed = await failure2(tx.tx(async (nested) => {
|
|
3665
|
+
nested.afterCommit(() => {
|
|
3666
|
+
calls.push("rolled-back nested");
|
|
3667
|
+
});
|
|
3668
|
+
throw new Error("no");
|
|
3669
|
+
}));
|
|
3670
|
+
expect(failed).toBeInstanceOf(Error);
|
|
3671
|
+
expect(calls).toEqual([]);
|
|
3672
|
+
});
|
|
3673
|
+
expect(calls).toEqual(["first:1", "second", "nested"]);
|
|
3674
|
+
calls.length = 0;
|
|
3675
|
+
await failure2(db.tx(async (tx) => {
|
|
3676
|
+
tx.afterCommit(() => {
|
|
3677
|
+
calls.push("never");
|
|
3678
|
+
});
|
|
3679
|
+
throw new Error("rollback");
|
|
3680
|
+
}));
|
|
3681
|
+
expect(calls).toEqual([]);
|
|
3682
|
+
});
|
|
3683
|
+
});
|
|
3684
|
+
test(`${name} an afterCommit error reaches hookm.onError and never changes the commit`, async () => {
|
|
3685
|
+
const errors = [];
|
|
3686
|
+
const calls = [];
|
|
3687
|
+
await withApp({ hookm: [{ onError: (error) => errors.push(error) }] }, async ({ db }) => {
|
|
3688
|
+
const value = await db.tx(async (tx) => {
|
|
3689
|
+
tx.afterCommit(() => {
|
|
3690
|
+
throw new Error("callback broke");
|
|
3691
|
+
});
|
|
3692
|
+
tx.afterCommit(() => {
|
|
3693
|
+
calls.push("after the broken one");
|
|
3694
|
+
});
|
|
3695
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3696
|
+
return "done";
|
|
3697
|
+
});
|
|
3698
|
+
expect(value).toBe("done");
|
|
3699
|
+
expect(calls).toEqual(["after the broken one"]);
|
|
3700
|
+
expect(errors).toHaveLength(1);
|
|
3701
|
+
expect(errors[0].message).toBe("callback broke");
|
|
3702
|
+
expect(await balances(db)).toEqual({ a: 1 });
|
|
3703
|
+
});
|
|
3704
|
+
});
|
|
3705
|
+
test(`${name} a row lock outside tx is OKM1830, and a bad lock or wait is refused`, async () => {
|
|
3706
|
+
await withApp({}, async ({ db }) => {
|
|
3707
|
+
const outside = await failure2(db.accounts.find({ limit: 1, lock: "update" }));
|
|
3708
|
+
expect(outside.code).toBe("OKM1830");
|
|
3709
|
+
await db.tx(async (tx) => {
|
|
3710
|
+
const bad = await failure2(tx.accounts.find({ limit: 1, lock: "exclusive" }));
|
|
3711
|
+
expect(bad.code).toBe("OKM1120");
|
|
3712
|
+
});
|
|
3713
|
+
});
|
|
3714
|
+
});
|
|
3715
|
+
test(`${name} find takes update and share locks with nowait and skip inside tx`, async () => {
|
|
3716
|
+
await withApp({}, async ({ db }) => {
|
|
3717
|
+
await seed(db);
|
|
3718
|
+
await db.tx(async (tx) => {
|
|
3719
|
+
for (const lock of ["update", "share"]) {
|
|
3720
|
+
for (const wait of [undefined, "nowait", "skip"]) {
|
|
3721
|
+
const rows = await tx.accounts.find({
|
|
3722
|
+
where: { id: "a" },
|
|
3723
|
+
limit: 1,
|
|
3724
|
+
lock,
|
|
3725
|
+
...wait !== undefined ? { wait } : {}
|
|
3726
|
+
});
|
|
3727
|
+
expect(rows).toHaveLength(1);
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
});
|
|
3731
|
+
});
|
|
3732
|
+
});
|
|
3733
|
+
race(`${name} nowait fails at once and skip leaves the locked row out`, async () => {
|
|
3734
|
+
await withApp({}, async ({ db }) => {
|
|
3735
|
+
await seed(db);
|
|
3736
|
+
const hold = await holdRow(db, "a");
|
|
3737
|
+
try {
|
|
3738
|
+
const refused = await failure2(db.tx((tx) => tx.accounts.find({ where: { id: "a" }, limit: 1, lock: "update", wait: "nowait" })));
|
|
3739
|
+
expect(refused.kind).toBe("lock_timeout");
|
|
3740
|
+
const skipped = await db.tx((tx) => tx.accounts.find({ limit: 10, lock: "update", wait: "skip" }));
|
|
3741
|
+
expect(skipped.map((row) => row.id)).toEqual(["b"]);
|
|
3742
|
+
const shared = await db.tx((tx) => tx.accounts.find({ where: { id: "b" }, limit: 1, lock: "share", wait: "nowait" }));
|
|
3743
|
+
expect(shared).toHaveLength(1);
|
|
3744
|
+
} finally {
|
|
3745
|
+
await hold.release();
|
|
3746
|
+
}
|
|
3747
|
+
});
|
|
3748
|
+
});
|
|
3749
|
+
race(`${name} two serializable transactions that conflict: the loser is retried on a fresh transaction`, async () => {
|
|
3750
|
+
await withApp({}, async ({ db }) => {
|
|
3751
|
+
await seed(db);
|
|
3752
|
+
const attempts = { a: 0, b: 0 };
|
|
3753
|
+
const read = barrier(2);
|
|
3754
|
+
let releaseB = () => {};
|
|
3755
|
+
const aCommitted = new Promise((resolve) => {
|
|
3756
|
+
releaseB = resolve;
|
|
3757
|
+
});
|
|
3758
|
+
const run = (me) => db.tx({ isolation: "serializable", retry: 3 }, async (tx) => {
|
|
3759
|
+
attempts[me] += 1;
|
|
3760
|
+
const rows = await tx.accounts.find({ limit: 10 });
|
|
3761
|
+
const total = rows.reduce((sum, row) => sum + row.balance, 0);
|
|
3762
|
+
if (attempts[me] === 1)
|
|
3763
|
+
await read();
|
|
3764
|
+
if (me === "b" && attempts.b === 1)
|
|
3765
|
+
await aCommitted;
|
|
3766
|
+
await tx.accounts.update({ where: { id: me }, set: { balance: total } });
|
|
3767
|
+
});
|
|
3768
|
+
const a = run("a").finally(() => {
|
|
3769
|
+
releaseB();
|
|
3770
|
+
});
|
|
3771
|
+
await Promise.all([a, run("b")]);
|
|
3772
|
+
expect(attempts.a + attempts.b).toBe(3);
|
|
3773
|
+
expect(Object.values(await balances(db)).toSorted((a, b) => a - b)).toEqual([200, 300]);
|
|
3774
|
+
});
|
|
3775
|
+
}, 20000);
|
|
3776
|
+
race(`${name} without retry the serialization failure surfaces as a retryable error`, async () => {
|
|
3777
|
+
await withApp({}, async ({ db }) => {
|
|
3778
|
+
await seed(db);
|
|
3779
|
+
const read = barrier(2);
|
|
3780
|
+
let releaseB = () => {};
|
|
3781
|
+
const aCommitted = new Promise((resolve) => {
|
|
3782
|
+
releaseB = resolve;
|
|
3783
|
+
});
|
|
3784
|
+
const run = (me) => db.tx({ isolation: "serializable" }, async (tx) => {
|
|
3785
|
+
const rows = await tx.accounts.find({ limit: 10 });
|
|
3786
|
+
await read();
|
|
3787
|
+
if (me === "b")
|
|
3788
|
+
await aCommitted;
|
|
3789
|
+
await tx.accounts.update({
|
|
3790
|
+
where: { id: me },
|
|
3791
|
+
set: { balance: rows.reduce((sum, row) => sum + row.balance, 0) }
|
|
3792
|
+
});
|
|
3793
|
+
});
|
|
3794
|
+
const a = run("a").finally(() => {
|
|
3795
|
+
releaseB();
|
|
3796
|
+
});
|
|
3797
|
+
const settled = await Promise.allSettled([a, run("b")]);
|
|
3798
|
+
const rejected = settled.filter((item) => item.status === "rejected");
|
|
3799
|
+
expect(rejected).toHaveLength(1);
|
|
3800
|
+
const error = rejected[0].reason;
|
|
3801
|
+
expect(error.kind).toBe("serialization");
|
|
3802
|
+
expect(error.retryable).toBe(true);
|
|
3803
|
+
});
|
|
3804
|
+
}, 20000);
|
|
3805
|
+
race(`${name} a real deadlock: the victim is retried and both finish`, async () => {
|
|
3806
|
+
await withApp({}, async ({ db }) => {
|
|
3807
|
+
await seed(db);
|
|
3808
|
+
const attempts = { a: 0, b: 0 };
|
|
3809
|
+
const ready = barrier(2);
|
|
3810
|
+
const run = (me, first, second) => db.tx({ retry: 2 }, async (tx) => {
|
|
3811
|
+
attempts[me] += 1;
|
|
3812
|
+
await tx.accounts.find({ where: { id: first }, limit: 1, lock: "update" });
|
|
3813
|
+
if (attempts[me] === 1)
|
|
3814
|
+
await ready();
|
|
3815
|
+
await tx.accounts.find({ where: { id: second }, limit: 1, lock: "update" });
|
|
3816
|
+
await tx.accounts.update({ where: { id: first }, set: { balance: attempts[me] } });
|
|
3817
|
+
});
|
|
3818
|
+
await Promise.all([run("a", "a", "b"), run("b", "b", "a")]);
|
|
3819
|
+
expect(attempts.a + attempts.b).toBe(3);
|
|
3820
|
+
});
|
|
3821
|
+
}, 20000);
|
|
3822
|
+
race(`${name} a deadlock with retry 0 fails as kind deadlock`, async () => {
|
|
3823
|
+
await withApp({}, async ({ db }) => {
|
|
3824
|
+
await seed(db);
|
|
3825
|
+
const ready = barrier(2);
|
|
3826
|
+
const run = (first, second) => db.tx(async (tx) => {
|
|
3827
|
+
await tx.accounts.find({ where: { id: first }, limit: 1, lock: "update" });
|
|
3828
|
+
await ready();
|
|
3829
|
+
await tx.accounts.find({ where: { id: second }, limit: 1, lock: "update" });
|
|
3830
|
+
});
|
|
3831
|
+
const settled = await Promise.allSettled([run("a", "b"), run("b", "a")]);
|
|
3832
|
+
const rejected = settled.filter((item) => item.status === "rejected");
|
|
3833
|
+
expect(rejected).toHaveLength(1);
|
|
3834
|
+
expect(rejected[0].reason.kind).toBe("deadlock");
|
|
3835
|
+
});
|
|
3836
|
+
}, 20000);
|
|
3837
|
+
race(`${name} repeatable read keeps one snapshot, read committed does not`, async () => {
|
|
3838
|
+
await withApp({}, async ({ db }) => {
|
|
3839
|
+
await seed(db);
|
|
3840
|
+
const snapshot = async (isolation) => db.tx({ isolation }, async (tx) => {
|
|
3841
|
+
const before = await tx.accounts.find({ where: { id: "a" }, limit: 1 });
|
|
3842
|
+
await db.accounts.update({
|
|
3843
|
+
where: { id: "a" },
|
|
3844
|
+
set: { balance: before[0].balance + 1 }
|
|
3845
|
+
});
|
|
3846
|
+
const after = await tx.accounts.find({ where: { id: "a" }, limit: 1 });
|
|
3847
|
+
return [before[0].balance, after[0].balance];
|
|
3848
|
+
});
|
|
3849
|
+
expect(await snapshot("repeatable read")).toEqual([100, 100]);
|
|
3850
|
+
expect(await snapshot("read committed")).toEqual([101, 102]);
|
|
3851
|
+
});
|
|
3852
|
+
});
|
|
3853
|
+
cancels(`${name} a statement killed by its timeout fails as kind timeout and the pool is fine`, async () => {
|
|
3854
|
+
await withApp({}, async ({ db }) => {
|
|
3855
|
+
await seed(db);
|
|
3856
|
+
const hold = await holdRow(db, "a");
|
|
3857
|
+
try {
|
|
3858
|
+
const started = Date.now();
|
|
3859
|
+
const error = await failure2(db.accounts.update({ where: { id: "a" }, set: { balance: 7 } }, { timeout: 200 }));
|
|
3860
|
+
expect(error.kind).toBe("timeout");
|
|
3861
|
+
expect(Date.now() - started).toBeLessThan(3000);
|
|
3862
|
+
} finally {
|
|
3863
|
+
await hold.release();
|
|
3864
|
+
}
|
|
3865
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3866
|
+
await db.accounts.update({ where: { id: "a" }, set: { balance: 8 } });
|
|
3867
|
+
expect((await balances(db)).a).toBe(8);
|
|
3868
|
+
});
|
|
3869
|
+
});
|
|
3870
|
+
cancels(`${name} a statement killed by abort fails as kind cancelled and is never retried`, async () => {
|
|
3871
|
+
await withApp({}, async ({ db }) => {
|
|
3872
|
+
await seed(db);
|
|
3873
|
+
const hold = await holdRow(db, "a");
|
|
3874
|
+
try {
|
|
3875
|
+
const controller = new AbortController;
|
|
3876
|
+
setTimeout(() => controller.abort(), 150);
|
|
3877
|
+
let runs = 0;
|
|
3878
|
+
const error = await failure2(db.tx({ retry: 3 }, async (tx) => {
|
|
3879
|
+
runs += 1;
|
|
3880
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 7 } }, { signal: controller.signal });
|
|
3881
|
+
}));
|
|
3882
|
+
expect(error.kind).toBe("cancelled");
|
|
3883
|
+
expect(error.retryable).toBe(false);
|
|
3884
|
+
expect(runs).toBe(1);
|
|
3885
|
+
} finally {
|
|
3886
|
+
await hold.release();
|
|
3887
|
+
}
|
|
3888
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3889
|
+
});
|
|
3890
|
+
});
|
|
3891
|
+
cancels(`${name} a timeout on a statement inside tx rolls the whole tx back`, async () => {
|
|
3892
|
+
await withApp({}, async ({ db }) => {
|
|
3893
|
+
await seed(db);
|
|
3894
|
+
const hold = await holdRow(db, "a");
|
|
3895
|
+
try {
|
|
3896
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3897
|
+
await tx.accounts.insert({ id: "c", balance: 1 });
|
|
3898
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 9 } }, { timeout: 200 });
|
|
3899
|
+
}));
|
|
3900
|
+
expect(error.kind).toBe("timeout");
|
|
3901
|
+
} finally {
|
|
3902
|
+
await hold.release();
|
|
3903
|
+
}
|
|
3904
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3905
|
+
});
|
|
3906
|
+
});
|
|
3907
|
+
cancels(`${name} connect timeouts.statement is the default for a call with no timeout`, async () => {
|
|
3908
|
+
await withApp({ timeouts: { statement: 200 } }, async ({ db }) => {
|
|
3909
|
+
await seed(db);
|
|
3910
|
+
const hold = await holdRow(db, "a");
|
|
3911
|
+
try {
|
|
3912
|
+
const error = await failure2(db.accounts.update({ where: { id: "a" }, set: { balance: 7 } }));
|
|
3913
|
+
expect(error.kind).toBe("timeout");
|
|
3914
|
+
} finally {
|
|
3915
|
+
await hold.release();
|
|
3916
|
+
}
|
|
3917
|
+
});
|
|
3918
|
+
});
|
|
3919
|
+
cancels(`${name} tx({ timeout }) stops a blocked transaction and rolls it back`, async () => {
|
|
3920
|
+
await withApp({}, async ({ db }) => {
|
|
3921
|
+
await seed(db);
|
|
3922
|
+
const hold = await holdRow(db, "a");
|
|
3923
|
+
try {
|
|
3924
|
+
const started = Date.now();
|
|
3925
|
+
const error = await failure2(db.tx({ timeout: 300 }, async (tx) => {
|
|
3926
|
+
await tx.accounts.insert({ id: "c", balance: 1 });
|
|
3927
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 9 } });
|
|
3928
|
+
}));
|
|
3929
|
+
expect(error.kind).toBe("timeout");
|
|
3930
|
+
expect(Date.now() - started).toBeLessThan(3000);
|
|
3931
|
+
} finally {
|
|
3932
|
+
await hold.release();
|
|
3933
|
+
}
|
|
3934
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3935
|
+
});
|
|
3936
|
+
});
|
|
3937
|
+
cancels(`${name} tx({ signal }) cancels the transaction and rolls it back`, async () => {
|
|
3938
|
+
await withApp({}, async ({ db }) => {
|
|
3939
|
+
await seed(db);
|
|
3940
|
+
const hold = await holdRow(db, "a");
|
|
3941
|
+
try {
|
|
3942
|
+
const controller = new AbortController;
|
|
3943
|
+
setTimeout(() => controller.abort(), 150);
|
|
3944
|
+
const error = await failure2(db.tx({ signal: controller.signal }, async (tx) => {
|
|
3945
|
+
await tx.accounts.insert({ id: "c", balance: 1 });
|
|
3946
|
+
await tx.accounts.update({ where: { id: "a" }, set: { balance: 9 } });
|
|
3947
|
+
}));
|
|
3948
|
+
expect(error.kind).toBe("cancelled");
|
|
3949
|
+
} finally {
|
|
3950
|
+
await hold.release();
|
|
3951
|
+
}
|
|
3952
|
+
expect(await balances(db)).toEqual({ a: 100, b: 100 });
|
|
3953
|
+
});
|
|
3954
|
+
});
|
|
3955
|
+
test(`${name} an abort after the transaction finished has no effect`, async () => {
|
|
3956
|
+
await withApp({}, async ({ db }) => {
|
|
3957
|
+
const controller = new AbortController;
|
|
3958
|
+
const value = await db.tx({ signal: controller.signal }, async (tx) => {
|
|
3959
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3960
|
+
return 42;
|
|
3961
|
+
});
|
|
3962
|
+
controller.abort();
|
|
3963
|
+
await sleep(30);
|
|
3964
|
+
expect(value).toBe(42);
|
|
3965
|
+
expect(await balances(db)).toEqual({ a: 1 });
|
|
3966
|
+
});
|
|
3967
|
+
});
|
|
3968
|
+
test(`${name} an already-aborted signal fails before a connection is taken`, async () => {
|
|
3969
|
+
await withApp({ max: 1 }, async ({ db }) => {
|
|
3970
|
+
const controller = new AbortController;
|
|
3971
|
+
controller.abort();
|
|
3972
|
+
const error = await failure2(db.tx({ signal: controller.signal }, async (tx) => {
|
|
3973
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3974
|
+
}));
|
|
3975
|
+
expect(error.kind).toBe("cancelled");
|
|
3976
|
+
expect(await balances(db)).toEqual({});
|
|
3977
|
+
});
|
|
3978
|
+
});
|
|
3979
|
+
test(`${name} timeouts.idleInTransaction stops a transaction that waits between statements`, async () => {
|
|
3980
|
+
await withApp({ timeouts: { idleInTransaction: 150 } }, async ({ db }) => {
|
|
3981
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3982
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3983
|
+
await sleep(500);
|
|
3984
|
+
await tx.accounts.insert({ id: "b", balance: 2 });
|
|
3985
|
+
}));
|
|
3986
|
+
expect(error.kind).toBe("timeout");
|
|
3987
|
+
expect(await balances(db)).toEqual({});
|
|
3988
|
+
});
|
|
3989
|
+
});
|
|
3990
|
+
test(`${name} timeouts.transaction bounds a callback that never settles and frees the connection`, async () => {
|
|
3991
|
+
await withApp({ max: 1, timeouts: { transaction: 250 } }, async ({ db }) => {
|
|
3992
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
3993
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
3994
|
+
await new Promise(() => {
|
|
3995
|
+
return;
|
|
3996
|
+
});
|
|
3997
|
+
}));
|
|
3998
|
+
expect(error.kind).toBe("timeout");
|
|
3999
|
+
expect(await balances(db)).toEqual({});
|
|
4000
|
+
});
|
|
4001
|
+
});
|
|
4002
|
+
proxied(`${name} a commit cut on the wire is outcome_unknown (OKM1401) and is never retried`, async () => {
|
|
4003
|
+
const proxy = await startCutProxy(suite.url ?? primaryUrl());
|
|
4004
|
+
try {
|
|
4005
|
+
await withApp({ url: proxy.url, max: 2 }, async ({ db, direct }) => {
|
|
4006
|
+
let runs = 0;
|
|
4007
|
+
proxy.cutNextCommit();
|
|
4008
|
+
const error = await failure2(db.tx({ retry: 3 }, async (tx) => {
|
|
4009
|
+
runs += 1;
|
|
4010
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
4011
|
+
}));
|
|
4012
|
+
expect(error.code).toBe("OKM1401");
|
|
4013
|
+
expect(error.kind).toBe("outcome_unknown");
|
|
4014
|
+
expect(error.retryable).toBe(false);
|
|
4015
|
+
expect(runs).toBe(1);
|
|
4016
|
+
const check = await (await direct()).execute("select id from accounts");
|
|
4017
|
+
expect(check.rows).toEqual([["a"]]);
|
|
4018
|
+
await db.accounts.insert({ id: "b", balance: 2 });
|
|
4019
|
+
expect(Object.keys(await balances(db)).toSorted()).toEqual(["a", "b"]);
|
|
4020
|
+
});
|
|
4021
|
+
} finally {
|
|
4022
|
+
await proxy.close();
|
|
4023
|
+
}
|
|
4024
|
+
});
|
|
4025
|
+
proxied(`${name} a batch whose commit is cut is outcome_unknown with a null batchIndex`, async () => {
|
|
4026
|
+
const proxy = await startCutProxy(suite.url ?? primaryUrl());
|
|
4027
|
+
try {
|
|
4028
|
+
await withApp({ url: proxy.url, max: 2 }, async ({ db }) => {
|
|
4029
|
+
proxy.cutNextCommit();
|
|
4030
|
+
const error = await failure2(db.batch([
|
|
4031
|
+
db.accounts.insert({ id: "a", balance: 1 }),
|
|
4032
|
+
db.accounts.insert({ id: "b", balance: 2 })
|
|
4033
|
+
]));
|
|
4034
|
+
expect(error.code).toBe("OKM1401");
|
|
4035
|
+
expect(error.batchIndex).toBeNull();
|
|
4036
|
+
});
|
|
4037
|
+
} finally {
|
|
4038
|
+
await proxy.close();
|
|
4039
|
+
}
|
|
4040
|
+
});
|
|
4041
|
+
test(`${name} after a rollback the pooled connection has no transaction, lock, or isolation left`, async () => {
|
|
4042
|
+
await withApp({ max: 1 }, async ({ db, pool }) => {
|
|
4043
|
+
const probe = async () => (await pool.execute(`select pg_backend_pid()::text,
|
|
4044
|
+
current_setting('transaction_isolation'),
|
|
4045
|
+
(select count(*) from pg_locks where pid = pg_backend_pid() and locktype = 'advisory')::text,
|
|
4046
|
+
(pg_current_xact_id_if_assigned() is null)::text`)).rows[0];
|
|
4047
|
+
const before = await probe();
|
|
4048
|
+
const rolled = await failure2(db.tx({ isolation: "serializable" }, async (tx) => {
|
|
4049
|
+
await tx.advisoryLock("jobs");
|
|
4050
|
+
await tx.advisoryLock(42);
|
|
4051
|
+
await tx.accounts.insert({ id: "a", balance: 1 });
|
|
4052
|
+
throw new Error("rollback");
|
|
4053
|
+
}));
|
|
4054
|
+
expect(rolled).toBeInstanceOf(Error);
|
|
4055
|
+
const after = await probe();
|
|
4056
|
+
expect(after?.[0]).toBe(before?.[0]);
|
|
4057
|
+
expect(after?.slice(1)).toEqual(["read committed", "0", "true"]);
|
|
4058
|
+
await db.tx({ isolation: "repeatable read" }, async (tx) => {
|
|
4059
|
+
await tx.advisoryLock(7n);
|
|
4060
|
+
await tx.accounts.insert({ id: "b", balance: 1 });
|
|
4061
|
+
});
|
|
4062
|
+
await failure2(db.tx(async (tx) => {
|
|
4063
|
+
await tx.accounts.insert({ id: "b", balance: 1 });
|
|
4064
|
+
}));
|
|
4065
|
+
expect((await probe())?.slice(1)).toEqual(["read committed", "0", "true"]);
|
|
4066
|
+
expect(await balances(db)).toEqual({ b: 1 });
|
|
4067
|
+
});
|
|
4068
|
+
});
|
|
4069
|
+
race(`${name} an advisory lock is held for the transaction and released at its end`, async () => {
|
|
4070
|
+
await withApp({}, async ({ db }) => {
|
|
4071
|
+
const first = deferred();
|
|
4072
|
+
const gate = deferred();
|
|
4073
|
+
let secondGot = false;
|
|
4074
|
+
const holder = db.tx(async (tx) => {
|
|
4075
|
+
await tx.advisoryLock("queue");
|
|
4076
|
+
first.resolve();
|
|
4077
|
+
await gate.promise;
|
|
4078
|
+
});
|
|
4079
|
+
await first.promise;
|
|
4080
|
+
const waiter = db.tx(async (tx) => {
|
|
4081
|
+
await tx.advisoryLock("queue");
|
|
4082
|
+
secondGot = true;
|
|
4083
|
+
});
|
|
4084
|
+
await sleep(250);
|
|
4085
|
+
expect(secondGot).toBe(false);
|
|
4086
|
+
gate.resolve();
|
|
4087
|
+
await Promise.all([holder, waiter]);
|
|
4088
|
+
expect(secondGot).toBe(true);
|
|
4089
|
+
});
|
|
4090
|
+
});
|
|
4091
|
+
test(`${name} tx keeps the tenant scope, and a scoped call never leaks to the next user of the connection`, async () => {
|
|
4092
|
+
await withEnv(tenantApp, { max: 1 }, async ({ db }) => {
|
|
4093
|
+
const a = db.for({ tenantId: TENANT_A });
|
|
4094
|
+
const b = db.for({ tenantId: TENANT_B });
|
|
4095
|
+
const idA = "01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c01";
|
|
4096
|
+
const idB = "01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c02";
|
|
4097
|
+
await b.notes.insert({ id: idB, body: "b" });
|
|
4098
|
+
const seen = await a.tx(async (tx) => {
|
|
4099
|
+
await tx.notes.insert({ id: idA, body: "a" });
|
|
4100
|
+
const inner = await tx.tx(async (nested) => nested.notes.find({ limit: 10 }));
|
|
4101
|
+
return [await tx.notes.find({ limit: 10 }), inner];
|
|
4102
|
+
});
|
|
4103
|
+
expect(seen[0].map((row) => row.body)).toEqual(["a"]);
|
|
4104
|
+
expect(seen[1].map((row) => row.body)).toEqual(["a"]);
|
|
4105
|
+
expect((await b.notes.find({ limit: 10 })).map((row) => row.body)).toEqual(["b"]);
|
|
4106
|
+
const rolled = await failure2(b.tx(async (tx) => {
|
|
4107
|
+
await tx.notes.insert({ id: "01890c5a-8f0e-7c3a-9b2d-6e4f1a0b9c03", body: "gone" });
|
|
4108
|
+
throw new Error("rollback");
|
|
4109
|
+
}));
|
|
4110
|
+
expect(rolled).toBeInstanceOf(Error);
|
|
4111
|
+
expect((await a.notes.find({ limit: 10 })).map((row) => row.body)).toEqual(["a"]);
|
|
4112
|
+
expect((await b.notes.find({ limit: 10 })).map((row) => row.body)).toEqual(["b"]);
|
|
4113
|
+
});
|
|
4114
|
+
});
|
|
4115
|
+
test(`${name} hookm.onTransaction sees start, commit, and rollback, with savepoints one level down`, async () => {
|
|
4116
|
+
const seen = [];
|
|
4117
|
+
const hookm = { onTransaction: (event) => seen.push(`${event.phase}:${event.depth}`) };
|
|
4118
|
+
await withApp({ hookm: [hookm] }, async ({ db }) => {
|
|
4119
|
+
await db.tx(async (tx) => {
|
|
4120
|
+
await tx.tx(async () => {
|
|
4121
|
+
return;
|
|
4122
|
+
});
|
|
4123
|
+
await failure2(tx.tx(async () => {
|
|
4124
|
+
throw new Error("inner");
|
|
4125
|
+
}));
|
|
4126
|
+
});
|
|
4127
|
+
await failure2(db.tx(async () => {
|
|
4128
|
+
throw new Error("outer");
|
|
4129
|
+
}));
|
|
4130
|
+
});
|
|
4131
|
+
expect(seen).toEqual([
|
|
4132
|
+
"start:0",
|
|
4133
|
+
"start:1",
|
|
4134
|
+
"commit:1",
|
|
4135
|
+
"start:1",
|
|
4136
|
+
"rollback:1",
|
|
4137
|
+
"commit:0",
|
|
4138
|
+
"start:0",
|
|
4139
|
+
"rollback:0"
|
|
4140
|
+
]);
|
|
4141
|
+
});
|
|
4142
|
+
(suite.notices ? test : skipped(test, "no notices"))(`${name} server notices reach hookm.onNotice, inside tx too`, async () => {
|
|
4143
|
+
const notices = [];
|
|
4144
|
+
const hookm = { onNotice: (notice) => notices.push(notice.message) };
|
|
4145
|
+
await withApp({
|
|
4146
|
+
hookm: [hookm],
|
|
4147
|
+
sql: [
|
|
4148
|
+
`create function noisy() returns trigger language plpgsql as $$ begin raise notice 'event %', new.id; return new; end $$`,
|
|
4149
|
+
`create trigger noisy after insert on events for each row execute function noisy()`
|
|
4150
|
+
]
|
|
4151
|
+
}, async ({ db }) => {
|
|
4152
|
+
await db.events.insert({ id: "e1", note: "x" });
|
|
4153
|
+
await db.tx(async (tx) => {
|
|
4154
|
+
await tx.events.insert({ id: "e2", note: "y" });
|
|
4155
|
+
});
|
|
4156
|
+
await sleep(30);
|
|
4157
|
+
expect(notices).toEqual(["event e1", "event e2"]);
|
|
4158
|
+
});
|
|
4159
|
+
});
|
|
4160
|
+
const withRows = (options, fn) => withEnv(rowsApp, options, fn);
|
|
4161
|
+
test(`${name} r6.commit commit keeps the note and the outbox row`, async () => {
|
|
4162
|
+
await withRows({}, async ({ db }) => {
|
|
4163
|
+
await db.tx(async (tx) => {
|
|
4164
|
+
await tx.notes.insert({ id: "n1", body: "hello" });
|
|
4165
|
+
await tx.outbox.insert({ id: "o1", kind: "created" });
|
|
4166
|
+
});
|
|
4167
|
+
expect((await db.notes.find({ limit: 10 })).map((row) => row.id)).toEqual(["n1"]);
|
|
4168
|
+
expect((await db.outbox.find({ limit: 10 })).map((row) => row.id)).toEqual(["o1"]);
|
|
4169
|
+
});
|
|
4170
|
+
});
|
|
4171
|
+
test(`${name} r6.rollback rollback keeps neither the note nor the outbox row`, async () => {
|
|
4172
|
+
await withRows({}, async ({ db }) => {
|
|
4173
|
+
const error = await failure2(db.tx(async (tx) => {
|
|
4174
|
+
await tx.notes.insert({ id: "n1", body: "hello" });
|
|
4175
|
+
await tx.outbox.insert({ id: "o1", kind: "created" });
|
|
4176
|
+
throw new Error("rollback");
|
|
4177
|
+
}));
|
|
4178
|
+
expect(error).toBeInstanceOf(Error);
|
|
4179
|
+
expect(await db.notes.find({ limit: 10 })).toEqual([]);
|
|
4180
|
+
expect(await db.outbox.find({ limit: 10 })).toEqual([]);
|
|
4181
|
+
});
|
|
4182
|
+
});
|
|
4183
|
+
test(`${name} r6.savepoint a nested tx that throws rolls back only the inner outbox row`, async () => {
|
|
4184
|
+
await withRows({}, async ({ db }) => {
|
|
4185
|
+
await db.tx(async (tx) => {
|
|
4186
|
+
await tx.notes.insert({ id: "n1", body: "hello" });
|
|
4187
|
+
const error = await failure2(tx.tx(async (inner) => {
|
|
4188
|
+
await inner.outbox.insert({ id: "inner", kind: "dropped" });
|
|
4189
|
+
throw new Error("inner");
|
|
4190
|
+
}));
|
|
4191
|
+
expect(error).toBeInstanceOf(Error);
|
|
4192
|
+
if (error instanceof Error)
|
|
4193
|
+
expect(error.message).toBe("inner");
|
|
4194
|
+
await tx.outbox.insert({ id: "outer", kind: "kept" });
|
|
4195
|
+
});
|
|
4196
|
+
expect((await db.notes.find({ limit: 10 })).map((row) => row.id)).toEqual(["n1"]);
|
|
4197
|
+
expect((await db.outbox.find({ limit: 10 })).map((row) => row.id)).toEqual(["outer"]);
|
|
4198
|
+
});
|
|
4199
|
+
});
|
|
4200
|
+
test(`${name} r3.repeat-insert a repeated insert of the same key is 23505 kind unique`, async () => {
|
|
4201
|
+
await withRows({}, async ({ db }) => {
|
|
4202
|
+
await db.slots.insert({ id: "1", label: "first" });
|
|
4203
|
+
const error = await failure2(db.slots.insert({ id: "1", label: "again" }));
|
|
4204
|
+
expect(error).toBeInstanceOf(OkmError);
|
|
4205
|
+
if (error instanceof OkmError) {
|
|
4206
|
+
expect(error.kind).toBe("unique");
|
|
4207
|
+
expect(error.sqlstate).toBe("23505");
|
|
4208
|
+
}
|
|
4209
|
+
});
|
|
4210
|
+
});
|
|
4211
|
+
test(`${name} r3.on-conflict-ignore onConflict ignore leaves the stored row`, async () => {
|
|
4212
|
+
await withRows({}, async ({ db }) => {
|
|
4213
|
+
await db.slots.insert({ id: "1", label: "first" });
|
|
4214
|
+
await db.slots.insert({ id: "1", label: "ignored" }, { onConflict: "ignore" });
|
|
4215
|
+
const stored = await db.slots.find({ where: { id: "1" }, limit: 1 });
|
|
4216
|
+
expect(stored[0]?.label).toBe("first");
|
|
4217
|
+
});
|
|
4218
|
+
});
|
|
4219
|
+
test(`${name} r3.on-conflict-update onConflict update writes the listed columns`, async () => {
|
|
4220
|
+
await withRows({}, async ({ db }) => {
|
|
4221
|
+
await db.slots.insert({ id: "1", label: "first" });
|
|
4222
|
+
await db.slots.insert({ id: "1", label: "updated" }, { onConflict: { on: "id", update: ["label"] } });
|
|
4223
|
+
const stored = await db.slots.find({ where: { id: "1" }, limit: 1 });
|
|
4224
|
+
expect(stored[0]?.label).toBe("updated");
|
|
4225
|
+
});
|
|
4226
|
+
});
|
|
4227
|
+
test(`${name} r3.on-conflict-return onConflict return gives the stored row back`, async () => {
|
|
4228
|
+
await withRows({}, async ({ db }) => {
|
|
4229
|
+
await db.slots.insert({ id: "1", label: "first" });
|
|
4230
|
+
const returned = await db.slots.insert({ id: "1", label: "nope" }, { onConflict: { on: "id", return: true } });
|
|
4231
|
+
expect(returned.label).toBe("first");
|
|
4232
|
+
const stored = await db.slots.find({ where: { id: "1" }, limit: 1 });
|
|
4233
|
+
expect(stored[0]?.label).toBe("first");
|
|
4234
|
+
});
|
|
4235
|
+
});
|
|
4236
|
+
race(`${name} r3.retry-id a re-run transaction callback plans a new client-generated id`, async () => {
|
|
4237
|
+
let next = 0;
|
|
4238
|
+
const planned = [];
|
|
4239
|
+
await withRows({
|
|
4240
|
+
generators: {
|
|
4241
|
+
uuidv4: () => {
|
|
4242
|
+
next += 1;
|
|
4243
|
+
const id = `00000000-0000-4000-8000-${next.toString(16).padStart(12, "0")}`;
|
|
4244
|
+
planned.push(id);
|
|
4245
|
+
return id;
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
}, async ({ db }) => {
|
|
4249
|
+
await db.ledger.insert({ id: "a", balance: 1 });
|
|
4250
|
+
await db.ledger.insert({ id: "b", balance: 1 });
|
|
4251
|
+
const attempts = { a: 0, b: 0 };
|
|
4252
|
+
const ids = { a: [], b: [] };
|
|
4253
|
+
const ready = barrier(2);
|
|
4254
|
+
const run = (me, first, second) => db.tx({ retry: 2 }, async (tx) => {
|
|
4255
|
+
attempts[me] += 1;
|
|
4256
|
+
const row = await tx.issued.insert({ body: me });
|
|
4257
|
+
ids[me].push(row.id);
|
|
4258
|
+
await tx.ledger.find({ where: { id: first }, limit: 1, lock: "update" });
|
|
4259
|
+
if (attempts[me] === 1)
|
|
4260
|
+
await ready();
|
|
4261
|
+
await tx.ledger.find({ where: { id: second }, limit: 1, lock: "update" });
|
|
4262
|
+
});
|
|
4263
|
+
await Promise.all([run("a", "a", "b"), run("b", "b", "a")]);
|
|
4264
|
+
expect(attempts.a + attempts.b).toBe(3);
|
|
4265
|
+
const victim = attempts.a === 2 ? "a" : "b";
|
|
4266
|
+
const victimIds = ids[victim];
|
|
4267
|
+
expect(victimIds).toHaveLength(2);
|
|
4268
|
+
expect(victimIds[0]).not.toBe(victimIds[1]);
|
|
4269
|
+
const stored = new Set((await db.issued.find({ limit: 10 })).map((row) => row.id));
|
|
4270
|
+
expect(stored.has(victimIds[0] ?? "")).toBe(false);
|
|
4271
|
+
expect(stored.has(victimIds[1] ?? "")).toBe(true);
|
|
4272
|
+
expect(planned).toContain(victimIds[0] ?? "");
|
|
4273
|
+
expect(planned).toContain(victimIds[1] ?? "");
|
|
4274
|
+
});
|
|
4275
|
+
}, 20000);
|
|
4276
|
+
test(`${name} r3.supply-id a caller cannot supply a client-generator id (OKM1190)`, async () => {
|
|
4277
|
+
await withRows({}, async ({ db }) => {
|
|
4278
|
+
const error = await failure2(db.issued.insert({
|
|
4279
|
+
id: "00000000-0000-4000-8000-000000000099",
|
|
4280
|
+
body: "no"
|
|
4281
|
+
}));
|
|
4282
|
+
expect(error).toBeInstanceOf(OkmError);
|
|
4283
|
+
if (error instanceof OkmError)
|
|
4284
|
+
expect(error.code).toBe("OKM1190");
|
|
4285
|
+
});
|
|
4286
|
+
});
|
|
4287
|
+
async function holdRow(db, rowId) {
|
|
4288
|
+
const locked = deferred();
|
|
4289
|
+
const gate = deferred();
|
|
4290
|
+
const done = db.tx(async (tx) => {
|
|
4291
|
+
await tx.accounts.find({ where: { id: rowId }, limit: 1, lock: "update" });
|
|
4292
|
+
locked.resolve();
|
|
4293
|
+
await gate.promise;
|
|
4294
|
+
});
|
|
4295
|
+
await Promise.race([locked.promise, done]);
|
|
4296
|
+
return {
|
|
4297
|
+
release: async () => {
|
|
4298
|
+
gate.resolve();
|
|
4299
|
+
await done;
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
function skipped(test, reason) {
|
|
4305
|
+
return skipFor(test, reason, (name) => `${name} (${reason})`);
|
|
4306
|
+
}
|
|
4307
|
+
async function openSuiteAdmin(url) {
|
|
4308
|
+
const { openPostgres } = await import("./shared/wezxf3h8.js");
|
|
4309
|
+
return openPostgres(url);
|
|
4310
|
+
}
|
|
4311
|
+
async function failure2(promise) {
|
|
4312
|
+
try {
|
|
4313
|
+
await promise;
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
return error;
|
|
4316
|
+
}
|
|
4317
|
+
throw new Error("expected the call to reject");
|
|
4318
|
+
}
|
|
4319
|
+
function sleep(ms) {
|
|
4320
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
4321
|
+
}
|
|
4322
|
+
function deferred() {
|
|
4323
|
+
let resolve;
|
|
4324
|
+
const promise = new Promise((done) => {
|
|
4325
|
+
resolve = done;
|
|
4326
|
+
});
|
|
4327
|
+
return { promise, resolve };
|
|
4328
|
+
}
|
|
4329
|
+
function barrier(parties) {
|
|
4330
|
+
let arrived = 0;
|
|
4331
|
+
const open = deferred();
|
|
4332
|
+
return () => {
|
|
4333
|
+
arrived += 1;
|
|
4334
|
+
if (arrived >= parties)
|
|
4335
|
+
open.resolve();
|
|
4336
|
+
return open.promise;
|
|
4337
|
+
};
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
// packages/harness/src/driver-run.ts
|
|
4341
|
+
async function executeDriverTest(argv) {
|
|
4342
|
+
process.env.TZ = "UTC";
|
|
4343
|
+
const parsed = parseArgs(argv);
|
|
4344
|
+
const target = await loadTarget(parsed.driver, parsed.url);
|
|
4345
|
+
const rows = await runTarget(target);
|
|
4346
|
+
const failed = rows.filter((row) => row.result === "fail").length;
|
|
4347
|
+
return { text: render(target.name, rows), failed };
|
|
4348
|
+
}
|
|
4349
|
+
if (__require.main == __require.module) {
|
|
4350
|
+
executeDriverTest(process.argv.slice(2)).then((report) => {
|
|
4351
|
+
process.stdout.write(report.text);
|
|
4352
|
+
if (report.failed > 0) {
|
|
4353
|
+
process.stderr.write(`${String(report.failed)} conformance case(s) failed.
|
|
4354
|
+
`);
|
|
4355
|
+
process.exitCode = 1;
|
|
4356
|
+
}
|
|
4357
|
+
}).catch((error) => {
|
|
4358
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4359
|
+
process.stderr.write(`${message}
|
|
4360
|
+
`);
|
|
4361
|
+
process.exitCode = 1;
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
function parseArgs(argv) {
|
|
4365
|
+
let driver;
|
|
4366
|
+
let url;
|
|
4367
|
+
for (let index = 0;index < argv.length; index += 1) {
|
|
4368
|
+
const arg = argv[index] ?? "";
|
|
4369
|
+
if (arg === "--url") {
|
|
4370
|
+
const value = argv[index + 1];
|
|
4371
|
+
if (value === undefined || value.startsWith("--")) {
|
|
4372
|
+
throw new Error("okm driver test --url needs a postgres URL.");
|
|
4373
|
+
}
|
|
4374
|
+
url = value;
|
|
4375
|
+
index += 1;
|
|
4376
|
+
continue;
|
|
4377
|
+
}
|
|
4378
|
+
if (arg.startsWith("--url=")) {
|
|
4379
|
+
url = arg.slice("--url=".length);
|
|
4380
|
+
if (url.length === 0)
|
|
4381
|
+
throw new Error("okm driver test --url needs a postgres URL.");
|
|
4382
|
+
continue;
|
|
4383
|
+
}
|
|
4384
|
+
if (arg.startsWith("--"))
|
|
4385
|
+
throw new Error(`Unknown flag ${arg}.`);
|
|
4386
|
+
if (driver !== undefined)
|
|
4387
|
+
throw new Error("okm driver test takes one driver.");
|
|
4388
|
+
driver = arg;
|
|
4389
|
+
}
|
|
4390
|
+
if (driver === undefined) {
|
|
4391
|
+
throw new Error("okm driver test needs a driver: pglite, postgresjs, pg, bun, neon, or a module path.");
|
|
4392
|
+
}
|
|
4393
|
+
return { driver, url: url ?? process.env.DATABASE_URL };
|
|
4394
|
+
}
|
|
4395
|
+
async function loadTarget(driver, url) {
|
|
4396
|
+
if (driver === "pglite" || driver === "postgresjs" || driver === "pg" || driver === "bun" || driver === "neon") {
|
|
4397
|
+
return firstParty(driver, url);
|
|
4398
|
+
}
|
|
4399
|
+
if (driver.includes("/") || driver.startsWith(".") || driver.endsWith(".ts") || driver.endsWith(".js")) {
|
|
4400
|
+
return moduleTarget(driver, url);
|
|
4401
|
+
}
|
|
4402
|
+
throw new Error(`Unknown driver ${driver}. Use pglite, postgresjs, pg, bun, neon, or a module path.`);
|
|
4403
|
+
}
|
|
4404
|
+
async function firstParty(driver, url) {
|
|
4405
|
+
if (driver === "pglite") {
|
|
4406
|
+
const adapter = await import("./shared/s6qha57m.js");
|
|
4407
|
+
const runtime = await import("./shared/5p8km5jb.js");
|
|
4408
|
+
return {
|
|
4409
|
+
name: "pglite",
|
|
4410
|
+
capabilities: adapter.capabilities,
|
|
4411
|
+
memory: true,
|
|
4412
|
+
notices: true,
|
|
4413
|
+
size: 1,
|
|
4414
|
+
cut: true,
|
|
4415
|
+
open: (config) => config.timeouts === undefined ? adapter.open() : adapter.open({ timeouts: config.timeouts }),
|
|
4416
|
+
connect: (pool, options) => runtime.connect(pool, options)
|
|
4417
|
+
};
|
|
4418
|
+
}
|
|
4419
|
+
if (url === undefined)
|
|
4420
|
+
throw new Error(`okm driver test ${driver} needs --url or DATABASE_URL.`);
|
|
4421
|
+
if (driver === "postgresjs") {
|
|
4422
|
+
const adapter = await import("./shared/mw5npye0.js");
|
|
4423
|
+
const runtime = await import("./shared/nxq95eb6.js");
|
|
4424
|
+
return networked("postgres.js", adapter.capabilities, true, url, (config) => adapter.open({
|
|
4425
|
+
url: config.url ?? url,
|
|
4426
|
+
max: config.max,
|
|
4427
|
+
searchPath: config.schema,
|
|
4428
|
+
...config.timeouts !== undefined ? { timeouts: config.timeouts } : {}
|
|
4429
|
+
}), (pool, options) => runtime.connect(pool, options));
|
|
4430
|
+
}
|
|
4431
|
+
if (driver === "pg") {
|
|
4432
|
+
const adapter = await import("./shared/a4pzghha.js");
|
|
4433
|
+
const runtime = await import("./shared/v6j1ep6w.js");
|
|
4434
|
+
return networked("node-postgres", adapter.capabilities, true, url, (config) => adapter.open({
|
|
4435
|
+
url: config.url ?? url,
|
|
4436
|
+
max: config.max,
|
|
4437
|
+
searchPath: config.schema,
|
|
4438
|
+
...config.timeouts !== undefined ? { timeouts: config.timeouts } : {}
|
|
4439
|
+
}), (pool, options) => runtime.connect(pool, options));
|
|
4440
|
+
}
|
|
4441
|
+
if (driver === "neon") {
|
|
4442
|
+
const adapter = await import("./shared/68tf18v4.js");
|
|
4443
|
+
const runtime = await import("./shared/9sfbtwsk.js");
|
|
4444
|
+
return networked("neon", adapter.capabilities, false, url, (config) => adapter.open({
|
|
4445
|
+
url: config.url ?? url,
|
|
4446
|
+
max: config.max,
|
|
4447
|
+
searchPath: config.schema,
|
|
4448
|
+
...config.timeouts !== undefined ? { timeouts: config.timeouts } : {}
|
|
4449
|
+
}), (pool, options) => runtime.connect(pool, options), false);
|
|
4450
|
+
}
|
|
4451
|
+
if (typeof Bun === "undefined" || typeof Bun.SQL !== "function") {
|
|
4452
|
+
throw new Error("bun runs only under Bun.");
|
|
4453
|
+
}
|
|
4454
|
+
const adapter = await import("./shared/m3xt8b1h.js");
|
|
4455
|
+
const runtime = await import("./shared/1800eymw.js");
|
|
4456
|
+
return networked("bun.sql", adapter.capabilities, false, url, (config) => adapter.open({
|
|
4457
|
+
url: config.url ?? url,
|
|
4458
|
+
max: config.max,
|
|
4459
|
+
searchPath: config.schema,
|
|
4460
|
+
...config.timeouts !== undefined ? { timeouts: config.timeouts } : {}
|
|
4461
|
+
}), (pool, options) => runtime.connect(pool, options));
|
|
4462
|
+
}
|
|
4463
|
+
function networked(name, capabilities, notices, url, open, connect, cut = true) {
|
|
4464
|
+
return {
|
|
4465
|
+
name,
|
|
4466
|
+
capabilities,
|
|
4467
|
+
memory: false,
|
|
4468
|
+
notices,
|
|
4469
|
+
size: 4,
|
|
4470
|
+
url,
|
|
4471
|
+
cut,
|
|
4472
|
+
open,
|
|
4473
|
+
connect
|
|
4474
|
+
};
|
|
4475
|
+
}
|
|
4476
|
+
async function moduleTarget(driver, url) {
|
|
4477
|
+
const loaded = await import(pathToFileURL(resolve(driver)).href);
|
|
4478
|
+
const body = moduleBody(loaded);
|
|
4479
|
+
if (body === undefined)
|
|
4480
|
+
throw new Error(`${driver} must export the driver object.`);
|
|
4481
|
+
const name = body.name;
|
|
4482
|
+
const open = body.open;
|
|
4483
|
+
const connect = body.connect;
|
|
4484
|
+
if (typeof name !== "string" || name.trim() === "" || /\s/.test(name)) {
|
|
4485
|
+
throw new Error(`${driver} must export a name with no spaces.`);
|
|
4486
|
+
}
|
|
4487
|
+
if (!isCapabilities(body.capabilities)) {
|
|
4488
|
+
throw new Error(`${driver} must export capabilities.`);
|
|
4489
|
+
}
|
|
4490
|
+
if (typeof open !== "function" || typeof connect !== "function") {
|
|
4491
|
+
throw new Error(`${driver} must export open and connect.`);
|
|
4492
|
+
}
|
|
4493
|
+
const memory = body.memory === true;
|
|
4494
|
+
if (!memory && url === undefined) {
|
|
4495
|
+
throw new Error(`okm driver test ${name} needs --url or DATABASE_URL.`);
|
|
4496
|
+
}
|
|
4497
|
+
const notices = body.notices === undefined ? true : body.notices === true;
|
|
4498
|
+
const size = typeof body.size === "number" ? body.size : memory ? 1 : 4;
|
|
4499
|
+
return {
|
|
4500
|
+
name,
|
|
4501
|
+
capabilities: body.capabilities,
|
|
4502
|
+
memory,
|
|
4503
|
+
notices,
|
|
4504
|
+
size,
|
|
4505
|
+
cut: true,
|
|
4506
|
+
...url !== undefined ? { url } : {},
|
|
4507
|
+
open,
|
|
4508
|
+
connect
|
|
4509
|
+
};
|
|
4510
|
+
}
|
|
4511
|
+
function moduleBody(loaded) {
|
|
4512
|
+
if (!isRecord2(loaded))
|
|
4513
|
+
return;
|
|
4514
|
+
return isRecord2(loaded.default) ? loaded.default : loaded;
|
|
4515
|
+
}
|
|
4516
|
+
function isCapabilities(value) {
|
|
4517
|
+
if (!isRecord2(value))
|
|
4518
|
+
return false;
|
|
4519
|
+
return (value.transactions === "interactive" || value.transactions === "batch" || value.transactions === "none") && typeof value.stream === "boolean" && typeof value.listen === "boolean" && typeof value.cancel === "boolean" && (value.prepared === "named" || value.prepared === "unnamed" || value.prepared === "none") && typeof value.describe === "boolean";
|
|
4520
|
+
}
|
|
4521
|
+
function isRecord2(value) {
|
|
4522
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4523
|
+
}
|
|
4524
|
+
async function runTarget(target) {
|
|
4525
|
+
const jobs = [];
|
|
4526
|
+
const wire = recorder(target.name, "wire", jobs);
|
|
4527
|
+
const errors = recorder(target.name, "error", jobs);
|
|
4528
|
+
const tx = recorder(target.name, "tx", jobs);
|
|
4529
|
+
registerDriverSuite({
|
|
4530
|
+
name: target.name,
|
|
4531
|
+
open: () => target.open({ schema: "public", max: target.size, timeouts: { acquire: 5000 } }),
|
|
4532
|
+
openLimited: () => target.open({
|
|
4533
|
+
schema: "public",
|
|
4534
|
+
max: 1,
|
|
4535
|
+
timeouts: { acquire: target.memory ? 100 : 150 }
|
|
4536
|
+
}),
|
|
4537
|
+
openOther: () => target.open({ schema: "public", max: target.memory ? 1 : 2 }),
|
|
4538
|
+
size: target.size,
|
|
4539
|
+
capabilities: target.capabilities,
|
|
4540
|
+
...target.notices ? {} : { notices: false },
|
|
4541
|
+
test: wire
|
|
4542
|
+
});
|
|
4543
|
+
registerErrorMappingSuite({
|
|
4544
|
+
name: target.name,
|
|
4545
|
+
open: () => target.open({ schema: "public", max: target.size, timeouts: { acquire: 5000 } }),
|
|
4546
|
+
capabilities: target.capabilities,
|
|
4547
|
+
test: errors
|
|
4548
|
+
});
|
|
4549
|
+
registerTxSuite({
|
|
4550
|
+
name: target.name,
|
|
4551
|
+
capabilities: target.capabilities,
|
|
4552
|
+
open: (config) => target.open(config),
|
|
4553
|
+
connect: (pool, options) => target.connect(pool, options),
|
|
4554
|
+
memory: target.memory,
|
|
4555
|
+
notices: target.notices,
|
|
4556
|
+
...target.cut ? {} : { cut: false },
|
|
4557
|
+
...target.url !== undefined ? { url: target.url } : {},
|
|
4558
|
+
test: tx
|
|
4559
|
+
});
|
|
4560
|
+
const rows = [];
|
|
4561
|
+
for (const job of jobs) {
|
|
4562
|
+
rows.push(await runJob(job));
|
|
4563
|
+
}
|
|
4564
|
+
return rows;
|
|
4565
|
+
}
|
|
4566
|
+
function recorder(driver, group, jobs) {
|
|
4567
|
+
const register = (title, fn, timeoutMs) => {
|
|
4568
|
+
jobs.push({
|
|
4569
|
+
title: strip(driver, title),
|
|
4570
|
+
group,
|
|
4571
|
+
fn,
|
|
4572
|
+
...timeoutMs !== undefined ? { timeoutMs } : {}
|
|
4573
|
+
});
|
|
4574
|
+
};
|
|
4575
|
+
register.skip = (title) => {
|
|
4576
|
+
jobs.push({ title: strip(driver, title), group, capability: "undeclared" });
|
|
4577
|
+
};
|
|
4578
|
+
register.unsupported = (title, capability) => {
|
|
4579
|
+
jobs.push({ title: strip(driver, title), group, capability });
|
|
4580
|
+
};
|
|
4581
|
+
return register;
|
|
4582
|
+
}
|
|
4583
|
+
async function runJob(job) {
|
|
4584
|
+
const identified = identify(job.group, job.title);
|
|
4585
|
+
if (job.fn === undefined) {
|
|
4586
|
+
return {
|
|
4587
|
+
id: identified.id,
|
|
4588
|
+
behavior: identified.behavior,
|
|
4589
|
+
result: "unsupported",
|
|
4590
|
+
capability: job.capability ?? "undeclared"
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
const timeoutMs = Math.max(job.timeoutMs ?? 0, 20000);
|
|
4594
|
+
try {
|
|
4595
|
+
await withTimeout(job.fn(), timeoutMs);
|
|
4596
|
+
return { id: identified.id, behavior: identified.behavior, result: "pass", capability: "" };
|
|
4597
|
+
} catch (error) {
|
|
4598
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4599
|
+
return {
|
|
4600
|
+
id: identified.id,
|
|
4601
|
+
behavior: identified.behavior,
|
|
4602
|
+
result: "fail",
|
|
4603
|
+
capability: "",
|
|
4604
|
+
detail: message
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
function withTimeout(pending, timeoutMs) {
|
|
4609
|
+
return new Promise((resolve, reject) => {
|
|
4610
|
+
const timer = setTimeout(() => reject(new Error(`timed out after ${String(timeoutMs)}ms`)), timeoutMs);
|
|
4611
|
+
pending.then(() => {
|
|
4612
|
+
clearTimeout(timer);
|
|
4613
|
+
resolve();
|
|
4614
|
+
}, (error) => {
|
|
4615
|
+
clearTimeout(timer);
|
|
4616
|
+
reject(error);
|
|
4617
|
+
});
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
function identify(group, title) {
|
|
4621
|
+
const named = /^(r\d+\.[a-z0-9-]+) (.*)$/.exec(title);
|
|
4622
|
+
const id = named?.[1];
|
|
4623
|
+
const behavior = named?.[2];
|
|
4624
|
+
if (id !== undefined && behavior !== undefined)
|
|
4625
|
+
return { id, behavior };
|
|
4626
|
+
if (title.startsWith("a commit cut on the wire"))
|
|
4627
|
+
return { id: "r2.commit-loss", behavior: title };
|
|
4628
|
+
if (title.startsWith("a batch whose commit is cut")) {
|
|
4629
|
+
return { id: "r2.batch-commit-loss", behavior: title };
|
|
4630
|
+
}
|
|
4631
|
+
return { id: `${group}.${slug(title)}`, behavior: title };
|
|
4632
|
+
}
|
|
4633
|
+
function slug(title) {
|
|
4634
|
+
const text = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4635
|
+
return text.length > 80 ? text.slice(0, 80).replace(/-+$/g, "") : text;
|
|
4636
|
+
}
|
|
4637
|
+
function strip(driver, title) {
|
|
4638
|
+
const lead = `${driver} `;
|
|
4639
|
+
return title.startsWith(lead) ? title.slice(lead.length) : title;
|
|
4640
|
+
}
|
|
4641
|
+
function render(driver, rows) {
|
|
4642
|
+
const lines = [`driver ${driver}`, "dialect postgres", "id\tbehavior\tresult\tcapability"];
|
|
4643
|
+
for (const row of rows) {
|
|
4644
|
+
lines.push(`${row.id} ${row.behavior} ${row.result} ${row.capability}`);
|
|
4645
|
+
if (row.detail !== undefined)
|
|
4646
|
+
lines.push(` ${row.detail}`);
|
|
4647
|
+
}
|
|
4648
|
+
return `${lines.join(`
|
|
4649
|
+
`)}
|
|
4650
|
+
`;
|
|
4651
|
+
}
|
|
4652
|
+
export {
|
|
4653
|
+
executeDriverTest
|
|
4654
|
+
};
|