okmodel 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -12
- package/dist/contracts/catalog/document.d.ts +20 -0
- package/dist/contracts/driver.d.ts +7 -0
- package/dist/dialects/pg/filter-parse.d.ts +8 -2
- package/dist/dialects/pg/fn/index.js +8 -0
- package/dist/dialects/pg/index.js +8 -2
- package/dist/dialects/pg/introspect.d.ts +6 -0
- package/dist/dialects/pg/ops/or.d.ts +1 -1
- package/dist/dialects/pg/role/sql.d.ts +10 -0
- package/dist/dialects/pg/schema.d.ts +15 -6
- package/dist/dialects/pg/temporal.d.ts +5 -76
- package/dist/dialects/pg/view/index.d.ts +20 -6
- package/dist/dialects/pg/view/index.js +1 -1
- package/dist/dialects/pg/view/scratch.d.ts +2 -2
- package/dist/runtime/client.d.ts +6 -1
- package/dist/runtime/conflict.d.ts +2 -1
- package/dist/runtime/pg/bun.d.ts +12 -7
- package/dist/runtime/pg/bun.js +7 -4
- package/dist/runtime/pg/pg.d.ts +12 -6
- package/dist/runtime/pg/pg.js +7 -4
- package/dist/runtime/pg/pglite.d.ts +11 -3
- package/dist/runtime/pg/pglite.js +7 -4
- package/dist/runtime/pg/postgresjs.d.ts +12 -6
- package/dist/runtime/pg/postgresjs.js +7 -4
- package/dist/runtime/plan.d.ts +16 -1
- package/dist/runtime/tenancy/index.js +1 -1
- package/dist/runtime/topology.d.ts +191 -0
- package/dist/runtime/traits/index.js +7 -7
- package/dist/runtime/types.d.ts +57 -2
- package/dist/runtime/validate/index.js +3 -3
- package/dist/shared/{radd3z4g.js → 0t22wwh1.js} +5 -9
- package/dist/shared/{ady1ms74.js → 2pqx26h8.js} +25 -10
- package/dist/shared/{kyxb62mc.js → 31vm914h.js} +9 -1
- package/dist/shared/{jns2wx4t.js → 32zqc1dw.js} +18 -7
- package/dist/shared/{nxb42q7h.js → 47x8x9ze.js} +2 -2
- package/dist/shared/{zvbjc949.js → 4az561fn.js} +1 -1
- package/dist/shared/{s6gqfn5j.js → 51mvgj8w.js} +27 -3
- package/dist/shared/{k3ted12y.js → 54tqkxja.js} +20 -2
- package/dist/shared/{3p4b1fg0.js → 5enne4ma.js} +1 -1
- package/dist/shared/{6693j9rs.js → 5fqx9yh4.js} +6 -2
- package/dist/shared/{c5g81ngb.js → 6mnp99hx.js} +11 -8
- package/dist/shared/{hqjsxnz7.js → 6qd27mx0.js} +5 -4
- package/dist/shared/{v7tpnhnz.js → 6rk2gen5.js} +5 -3
- package/dist/shared/{2321avmn.js → 7zwwyh0v.js} +46 -16
- package/dist/shared/{fa9vt245.js → 87jvvhnt.js} +2 -2
- package/dist/shared/{8jb4ymh2.js → 8bwbwy8h.js} +12 -1
- package/dist/shared/{c8aghh05.js → 8rsy3hxz.js} +7 -1
- package/dist/shared/{41kv2xdm.js → 8xsbagad.js} +1 -1
- package/dist/shared/{6p5k20sm.js → 96qcj21c.js} +3 -3
- package/dist/shared/{jgxzjzcz.js → aj2qxyhj.js} +2 -2
- package/dist/shared/{5vdm4sgp.js → caw8b9d8.js} +4 -4
- package/dist/shared/{s4mb6a4b.js → cctw17hj.js} +74 -15
- package/dist/shared/{rrstp3kb.js → cgbamx3e.js} +2 -2
- package/dist/shared/{axhk06q1.js → cr74e96e.js} +1 -1
- package/dist/shared/{b066g80h.js → efmy2gcg.js} +2 -2
- package/dist/shared/{gdh9mrtp.js → f5cw5q5r.js} +2 -2
- package/dist/shared/{xghkt7a9.js → fancm5pt.js} +6 -233
- package/dist/shared/{n5y9zyj1.js → ga3sfy93.js} +369 -14
- package/dist/shared/{xrfnq5k8.js → gbtcc2d7.js} +55 -5
- package/dist/shared/{7nk68fj8.js → gde73es6.js} +20 -15
- package/dist/shared/{kkzwp3d2.js → hf22e0wt.js} +55 -19
- package/dist/shared/{tny2t3sm.js → janx09m5.js} +8 -7
- package/dist/shared/{4ef7mmqf.js → jwtxdj06.js} +5 -5
- package/dist/shared/{sf8m6tfy.js → k8v5v5wj.js} +3 -3
- package/dist/shared/{qn0srwwq.js → kfjp0gck.js} +5 -3
- package/dist/shared/{wqem4jed.js → krxx77g0.js} +7 -7
- package/dist/shared/{edwqqryd.js → nkaxvt9q.js} +1 -1
- package/dist/shared/{jsd07prz.js → ntneex2a.js} +1 -1
- package/dist/shared/nxdp2ze3.js +1197 -0
- package/dist/shared/{tbszx8mz.js → pf0d8f77.js} +1 -1
- package/dist/shared/{84ptn35q.js → ps7a6h14.js} +10 -9
- package/dist/shared/{kj680nxv.js → q58y17hy.js} +9 -7
- package/dist/shared/{awrsqrn7.js → qm7cdtvr.js} +22 -9
- package/dist/shared/{q6zsyh31.js → qv2ydck9.js} +156 -205
- package/dist/shared/{kafaa5zv.js → r2sp8hrc.js} +28 -8
- package/dist/shared/{fb00y8fp.js → r8x5qhd7.js} +5 -5
- package/dist/shared/{yjw02enp.js → sdc7r2ww.js} +8 -7
- package/dist/shared/sycnx8gt.js +289 -0
- package/dist/shared/{hcmdtwgh.js → szm0cksa.js} +4 -1
- package/dist/shared/{zg24x0hm.js → tm5ab8we.js} +10 -2
- package/dist/shared/{ygem90rn.js → v36c5g6p.js} +85 -21
- package/dist/shared/wq9n6pwr.js +139 -0
- package/dist/shared/{7p2wbz7f.js → x72jb7ad.js} +1 -1
- package/dist/shared/{q795afwa.js → z2rnngj4.js} +3 -3
- package/dist/shared/{sr5ms7b3.js → zqrnhwsq.js} +3 -3
- package/dist/tooling/cli.js +70 -23
- package/dist/tooling/migrate/apply.d.ts +2 -0
- package/dist/tooling/migrate/commands.d.ts +1 -2
- package/dist/tooling/migrate/drift.d.ts +1 -1
- package/dist/tooling/migrate/estimate.d.ts +14 -1
- package/dist/tooling/migrate/files.d.ts +7 -0
- package/dist/tooling/migrate/index.js +12 -9
- package/dist/tooling/migrate/lint.d.ts +3 -3
- package/dist/tooling/migrate/plan.d.ts +7 -3
- package/dist/tooling/migrate/policy.d.ts +11 -0
- package/dist/tooling/migrate/sql-lex.d.ts +22 -0
- package/dist/tooling/testing/factories.d.ts +10 -0
- package/dist/tooling/testing/facts.d.ts +3 -0
- package/dist/tooling/testing/index.js +3 -3
- package/dist/tooling/testing/isolation.d.ts +4 -3
- package/package.json +6 -5
- package/dist/shared/0v1g424p.js +0 -26
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
okmodel is a catalog-first TypeScript ORM, PostgreSQL first. Other SQL databases are the direction. CI runs PostgreSQL 15 to 18. The schema is the single source. Migrations and queries come from it.
|
|
4
4
|
|
|
5
|
-
Version 0.
|
|
5
|
+
Version 0.5.0. Apache-2.0.
|
|
6
6
|
|
|
7
7
|
## Contents
|
|
8
8
|
|
|
@@ -58,6 +58,8 @@ Version 0.4.0. Apache-2.0.
|
|
|
58
58
|
- [Reference data](#reference-data)
|
|
59
59
|
- [Backfill](#backfill)
|
|
60
60
|
- [Testing](#testing)
|
|
61
|
+
- [Topology](#topology)
|
|
62
|
+
- [Reference app](#reference-app)
|
|
61
63
|
- [Startup](#startup)
|
|
62
64
|
- [Protected targets](#protected-targets)
|
|
63
65
|
- [Commands](#commands)
|
|
@@ -78,9 +80,11 @@ bun add okmodel @electric-sql/pglite
|
|
|
78
80
|
|
|
79
81
|
Bun.sql needs no extra package: `import { connect } from "okmodel/pg/bun"`. okmodel has no runtime dependencies. Each installed driver is an optional peer.
|
|
80
82
|
|
|
83
|
+
The `okm` bin's shebang is node, so `bunx okm` runs it with Node even inside a Bun project. Run `bun --bun okm` when the command must use the Bun runtime (Bun.sql runs only there). A `.ts` config file under plain Node needs Node 22.18 or newer for type stripping.
|
|
84
|
+
|
|
81
85
|
## Quickstart
|
|
82
86
|
|
|
83
|
-
This builds two tables, applies them, and runs one script. The script writes an author and a note, then reads the note with its author. `DATABASE_URL` is a direct Postgres URL, not a pooler.
|
|
87
|
+
This builds two tables, applies them, and runs one script. The script writes an author and a note, then reads the note with its author. `DATABASE_URL` is a direct Postgres URL, not a pooler. Port `6432` and port `6543` count as a pooler. Apply, seed, and check also refuse when the backend pid changes between two statements (OKM1854), unless `--allow-pooler` is set.
|
|
84
88
|
|
|
85
89
|
### Configure
|
|
86
90
|
|
|
@@ -371,7 +375,7 @@ if (!(await db.notes.exists({ where: { id: note.id } }))) {
|
|
|
371
375
|
|
|
372
376
|
### Update
|
|
373
377
|
|
|
374
|
-
`update` changes the columns in `set` on the rows that match `where`. A
|
|
378
|
+
`update` changes the columns in `set` on the rows that match `where`. A write with no effective predicate, including undefined values, is OKM1102. An empty `or()` branch is OKM1121. `.all(reason)` is the form that means every row.
|
|
375
379
|
|
|
376
380
|
```ts
|
|
377
381
|
// where picks the row. set names the new title.
|
|
@@ -380,7 +384,7 @@ await db.notes.update({ where: { id: note.id }, set: { title: "next" } });
|
|
|
380
384
|
|
|
381
385
|
### Delete
|
|
382
386
|
|
|
383
|
-
`delete` removes the rows that match `where`. A
|
|
387
|
+
`delete` removes the rows that match `where`. A write with no effective predicate, including undefined values, is OKM1102. An empty `or()` branch is OKM1121. `.all(reason)` is the form that means every row.
|
|
384
388
|
|
|
385
389
|
```ts
|
|
386
390
|
// where picks the row. Without it the call is refused.
|
|
@@ -826,7 +830,7 @@ export const app = schema({
|
|
|
826
830
|
|
|
827
831
|
### Views
|
|
828
832
|
|
|
829
|
-
A view is a saved query. You give it the SQL and the columns that query returns. `db.views.active.find({ limit: 20 })` reads `active`. That handle has no insert or update. There is no query-builder form yet. `okm check` reprints the query on the connected server before it compares, so a difference that is only how that server prints the query is not drift. A materialized view that refreshes while people are reading it needs a unique index, or the declaration is OKM1822. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
|
|
833
|
+
A view is a saved query. You give it the SQL and the columns that query returns. `db.views.active.find({ limit: 20 })` reads `active`. That handle has no insert or update. View handles are camelCased: a view named `active_projects` reads as `db.views.activeProjects`. There is no query-builder form yet. `okm check` reprints the query on the connected server before it compares, so a difference that is only how that server prints the query is not drift. A materialized view refreshes with `REFRESH MATERIALIZED VIEW`: the plan populates it with a plain refresh first (Postgres rejects `CONCURRENTLY` on an empty view), then refreshes concurrently when the declaration sets `refresh: "concurrently"`. A materialized view that refreshes while people are reading it needs a unique index, or the declaration is OKM1822. See [known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md).
|
|
830
834
|
|
|
831
835
|
`views.ts`:
|
|
832
836
|
|
|
@@ -943,7 +947,7 @@ update "public"."notes" set "title" = 'noted' where "title" = 'hello' and ("id"
|
|
|
943
947
|
|
|
944
948
|
### Testing
|
|
945
949
|
|
|
946
|
-
`okmodel/testing` opens a real pool. `factories` insert rows, `expectQueries` counts statements, and `isolation()` checks that tenant A cannot see tenant B. `okm seed <file>` calls the file's default export with that harness. The target must already be migrated. A protected target is refused unless `--allow-protected`.
|
|
950
|
+
`okmodel/testing` opens a real pool. `factories` insert rows, `expectQueries` counts statements, and `isolation()` checks that tenant A cannot see the row it inserted for tenant B. Each call uses a new pair of tenant ids, deletes what it inserted, and names a factory that is missing. `okm seed <file>` calls the file's default export with that harness. The target must already be migrated. A protected target is refused unless `--allow-protected`.
|
|
947
951
|
|
|
948
952
|
`factory.ts`:
|
|
949
953
|
|
|
@@ -990,9 +994,86 @@ export default async function seed(t: {
|
|
|
990
994
|
bunx okm seed seed.ts
|
|
991
995
|
```
|
|
992
996
|
|
|
997
|
+
### Topology
|
|
998
|
+
|
|
999
|
+
`connect({ primary, replicas }, options)` opens one pool per endpoint. An automatic read chooses a replica that has replayed at least as far as this client's last committed write. `routing.consistency` is `"session"` by default. `"eventual"` does not keep that watermark. `routing.maxLag` is a duration (`"5s"`, `"500ms"`, `"2m"`) or a size (`"16MB"`, `"512KB"`, `"1GB"`, `"4096B"`). A bare number is rejected. `find({ route: "primary" })` reads the primary. `find({ route: "replica" })` requires a replica that satisfies the same watermark and lag, and it does not fall back to the primary. `db.using("primary")` or `db.using("replica")` returns a client that forces that route. That client has no `close` and no `using`. `onRoute` receives `{ op, endpoint, reason }`. A throw from it is ignored. A string or an existing pool is one endpoint and serves either `route` from it, and it has no `using`.
|
|
1000
|
+
|
|
1001
|
+
The watermark is one value for the whole `connect()`. The root, `for()`, `unscoped()`, `using()`, and `reserve()` share it. A write through any of them moves it for the others. A watermark per `for()` client is not in this version.
|
|
1002
|
+
|
|
1003
|
+
When replicas are configured and consistency is `"session"`, a committed write reads `pg_current_wal_insert_lsn()` before its promise resolves. That is one extra round trip: on the primary pool after an autocommit `execute` or `batch`, and on the reserved connection after a successful `COMMIT`. A rollback does not read it. `"eventual"`, and a connect with no replicas, do not read it either.
|
|
1004
|
+
|
|
1005
|
+
`routing.select` picks among the replicas that passed health, the watermark, and `maxLag`. The default is `"weighted"`. `weighted` is smooth weighted round-robin. Equal weights take turns. A weight of `0` is rejected. `roundRobin` ignores weights and rotates through the eligible replicas. `leastConnections` uses the replica with the fewest statements already in flight on this client. A tie follows configuration order. `latencyAware` uses the replica with the lowest moving average of round-trip time. A function receives each candidate as `{ name, weight, inflight, latencyMs, lag }` and `{ op: "read" }`. `lag` is the number of bytes the replica is behind the primary, or `null` when that distance is unknown. The function returns one of those objects or its `name`. It runs only when an automatic read has two or more candidates.
|
|
1006
|
+
|
|
1007
|
+
A write, a batch, a transaction, or a locking read on a replica route is OKM1840. `route: "replica"` or `using("replica")` with no eligible replica is OKM1843, including a topology with no replicas. An automatic read with `routing.fallback: "error"` and no eligible replica is OKM1844. A migrate target that carries `primary`, `replicas`, `weight`, or `pool` is OKM1845. Waiting for a connection past `timeouts.acquire` is OKM1846, and that wait stays on the endpoint's own pool. The rules are in [topology](https://github.com/omqkhafi/okmodel/blob/main/docs/topology.md).
|
|
1008
|
+
|
|
1009
|
+
A replica whose pool is at its maximum, with no idle connection and callers waiting, is skipped. If every replica that can serve the read is in that state, the read uses the primary and `onRoute` reports `fallback:saturated`. When healthy replicas exist and none has caught up, or none is inside `maxLag`, the reason is `fallback:behind`. `route: "replica"` still uses a saturated replica, and it still requires the watermark and `maxLag`.
|
|
1010
|
+
|
|
1011
|
+
`replicas.ts`:
|
|
1012
|
+
|
|
1013
|
+
```ts
|
|
1014
|
+
import { connect } from "okmodel/pg/postgresjs";
|
|
1015
|
+
|
|
1016
|
+
import schema from "./schema.ts";
|
|
1017
|
+
|
|
1018
|
+
const url = process.env.DATABASE_URL;
|
|
1019
|
+
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
|
|
1020
|
+
const east = process.env.REPLICA_URL;
|
|
1021
|
+
if (east === undefined || east.length === 0) throw new Error("REPLICA_URL is not set");
|
|
1022
|
+
const west = process.env.REPLICA_URL_WEST ?? east;
|
|
1023
|
+
|
|
1024
|
+
const endpoints = {
|
|
1025
|
+
primary: url,
|
|
1026
|
+
replicas: [{ url: east, weight: 2, name: "east" }, west],
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const db = await connect(endpoints, {
|
|
1030
|
+
schema,
|
|
1031
|
+
routing: { probe: "1s", fallback: "primary", consistency: "session", select: "weighted" },
|
|
1032
|
+
onRoute(event) {
|
|
1033
|
+
// { op: "read", endpoint: "east", reason: "auto:east" }
|
|
1034
|
+
void event;
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
await db.notes.find({ limit: 50 });
|
|
1039
|
+
await db.notes.find({ limit: 50, route: "primary" });
|
|
1040
|
+
await db.notes.find({ limit: 50, route: "replica" });
|
|
1041
|
+
const replica = db.using("replica");
|
|
1042
|
+
await replica.notes.find({ limit: 50 });
|
|
1043
|
+
await db.close();
|
|
1044
|
+
|
|
1045
|
+
const eventual = await connect(endpoints, {
|
|
1046
|
+
schema,
|
|
1047
|
+
routing: { consistency: "eventual" },
|
|
1048
|
+
});
|
|
1049
|
+
await eventual.notes.find({ limit: 50 });
|
|
1050
|
+
await eventual.close();
|
|
1051
|
+
|
|
1052
|
+
for (const select of ["roundRobin", "leastConnections", "latencyAware"] as const) {
|
|
1053
|
+
const client = await connect(endpoints, { schema, routing: { select } });
|
|
1054
|
+
await client.notes.find({ limit: 1 });
|
|
1055
|
+
await client.close();
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const custom = await connect(endpoints, {
|
|
1059
|
+
schema,
|
|
1060
|
+
routing: {
|
|
1061
|
+
select(candidates) {
|
|
1062
|
+
return candidates[0] ?? "east";
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
await custom.notes.find({ limit: 1 });
|
|
1067
|
+
await custom.close();
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
### Reference app
|
|
1071
|
+
|
|
1072
|
+
The worked example is a project tracker: workspaces as tenants, archive, a function, two views, three generated migrations, and this topology. It is a private package in the repository, not on npm. CI runs it on the primary and two standbys. Board reads go to a replica. A read just after a write, with replay paused, comes back from the primary. The report uses `route: "replica"`. See the [example app](https://github.com/omqkhafi/okmodel/blob/main/docs/example-app.md) and the [reference app notes](https://github.com/omqkhafi/okmodel/blob/main/packages/reference-app/README.md).
|
|
1073
|
+
|
|
993
1074
|
### Startup
|
|
994
1075
|
|
|
995
|
-
`connect()` compares the app with `okm_history`. A database that is ahead by expand migrations still opens. Ahead by a contract migration, or behind the app, is OKM1520 and names the migration. `okm migrate status` prints the same states, plus `failed at step N (resume with okm migrate apply)
|
|
1076
|
+
`connect()` compares the app with `okm_history`. A database that is ahead by expand migrations still opens. Ahead by a contract migration, or behind the app, is OKM1520 and names the migration. `okm migrate status` prints the same states, plus `failed at step N (resume with okm migrate apply)`, where N is the stored step index. Apply's own `failed at step N` counts from 1, the same number the linter prints.
|
|
996
1077
|
|
|
997
1078
|
### Protected targets
|
|
998
1079
|
|
|
@@ -1064,7 +1145,8 @@ Status is on the [board](https://github.com/users/omqkhafi/projects/1). Each rel
|
|
|
1064
1145
|
- [x] [0.2](https://github.com/omqkhafi/okmodel/milestone/2) — Hidden and sensitive fields, validation, traits, tenancy, archive and restore, richer relations, presets, transactions, and operators for JSON, arrays, ranges, and search.
|
|
1065
1146
|
- [x] [0.3](https://github.com/omqkhafi/okmodel/milestone/3) — Extensions, domains, functions, triggers, views, roles, and grants.
|
|
1066
1147
|
- [x] [0.4](https://github.com/omqkhafi/okmodel/milestone/4) — Safer migration plans, backfill, drift checks, provisioning, reference data, and a testing package.
|
|
1067
|
-
- [
|
|
1148
|
+
- [x] [0.5](https://github.com/omqkhafi/okmodel/milestone/5) — A primary with replicas, read routing, and a reference app.
|
|
1149
|
+
- [ ] [0.5.1](https://github.com/omqkhafi/okmodel/milestone/7) — QA fixes for 0.5.0.
|
|
1068
1150
|
|
|
1069
1151
|
`okmodel/internal` has no stability promise. Names on that subpath can change or disappear in any release.
|
|
1070
1152
|
|
|
@@ -1076,11 +1158,11 @@ Measured on this release.
|
|
|
1076
1158
|
|
|
1077
1159
|
| | Minified | Gzip | Cold import |
|
|
1078
1160
|
| -------------------------------------------- | -------: | -----: | ----------: |
|
|
1079
|
-
| Runtime entry | 5,288 | 2,026 | 2.
|
|
1080
|
-
| App startup (10 tables, one find) | 89,
|
|
1081
|
-
| App startup, every 0.2 feature in use (full) |
|
|
1161
|
+
| Runtime entry | 5,288 | 2,026 | 2.347 ms |
|
|
1162
|
+
| App startup (10 tables, one find) | 89,751 | 29,801 | 15.061 ms |
|
|
1163
|
+
| App startup, every 0.2 feature in use (full) | 120,132 | 39,162 | 15.956 ms |
|
|
1082
1164
|
|
|
1083
|
-
The full app has column tenancy, `archivable()`, `timestamps()`, validation rules, `one`, `many` and `manyThrough` relations, presets, and calls `include`, `page`, `aggregate`, `tx` and `batch`. A feature costs bytes only in an app that uses it: the full app is 30,
|
|
1165
|
+
The full app has column tenancy, `archivable()`, `timestamps()`, validation rules, `one`, `many` and `manyThrough` relations, presets, and calls `include`, `page`, `aggregate`, `tx` and `batch`. A feature costs bytes only in an app that uses it: the full app is 30,381 minified and 9,361 gzip bytes above the plain one. The runtime entry and the plain app are gated. The full app is printed, not gated. The plain app's cold import on this sample is above the 15 ms local reference. That figure is printed and is not a gate.
|
|
1084
1166
|
|
|
1085
1167
|
The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md).
|
|
1086
1168
|
|
|
@@ -1092,5 +1174,6 @@ The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/b
|
|
|
1092
1174
|
- [Backfill](https://github.com/omqkhafi/okmodel/blob/main/docs/backfill.md)
|
|
1093
1175
|
- [Provisioning](https://github.com/omqkhafi/okmodel/blob/main/docs/provisioning.md)
|
|
1094
1176
|
- [Testing](https://github.com/omqkhafi/okmodel/blob/main/docs/testing.md)
|
|
1177
|
+
- [Topology](https://github.com/omqkhafi/okmodel/blob/main/docs/topology.md)
|
|
1095
1178
|
- [Known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md)
|
|
1096
1179
|
- [Changelog](https://github.com/omqkhafi/okmodel/blob/main/changelog.md)
|
|
@@ -62,6 +62,26 @@ export declare function serializeCatalog(source: Catalog): string;
|
|
|
62
62
|
* @returns Lowercase hex digest
|
|
63
63
|
*/
|
|
64
64
|
export declare function catalogHash(source: Catalog): string;
|
|
65
|
+
/**
|
|
66
|
+
* The catalog `connect()` compares with `okm_meta` (D208).
|
|
67
|
+
*
|
|
68
|
+
* Roles, grants, and default privileges come from the migration config, not
|
|
69
|
+
* the schema, so they are left out. A `schema()` catalog never has them, so
|
|
70
|
+
* `connect()` hashes it unchanged. `okm check` still verifies them. A
|
|
71
|
+
* catalog without them is returned as is.
|
|
72
|
+
*
|
|
73
|
+
* @param source - Catalog document
|
|
74
|
+
* @returns The catalog without role, grant, and default-privilege objects
|
|
75
|
+
*/
|
|
76
|
+
export declare function startupCatalog(source: Catalog): Catalog;
|
|
77
|
+
/**
|
|
78
|
+
* {@link catalogHash} of {@link startupCatalog}. The hash `okm_meta` and
|
|
79
|
+
* `okm_history` store, and the one `connect()` computes.
|
|
80
|
+
*
|
|
81
|
+
* @param source - Catalog document
|
|
82
|
+
* @returns Lowercase hex digest
|
|
83
|
+
*/
|
|
84
|
+
export declare function startupHash(source: Catalog): string;
|
|
65
85
|
/**
|
|
66
86
|
* Loads a build artifact without validating objects.
|
|
67
87
|
*
|
|
@@ -61,6 +61,13 @@ export type ExecuteOptions = {
|
|
|
61
61
|
* A deadline is kind `timeout`.
|
|
62
62
|
*/
|
|
63
63
|
readonly timeout?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Read routing hint.
|
|
66
|
+
*
|
|
67
|
+
* Drivers ignore it. A topology router reads it. Absent, the router
|
|
68
|
+
* classifies the statement.
|
|
69
|
+
*/
|
|
70
|
+
readonly route?: "primary" | "replica";
|
|
64
71
|
};
|
|
65
72
|
/** Column names and parameter count from a describe. */
|
|
66
73
|
export type DescribeResult = {
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Loaded the first time `parse()` is called. `filters()` validates the spec
|
|
5
5
|
* before this module exists.
|
|
6
|
+
*
|
|
7
|
+
* A request key is equality when the value is a scalar and `eq` is allowed.
|
|
8
|
+
* An object value names operators: `eq`, `in` (`inList`), `notIn`, `lt`,
|
|
9
|
+
* `lte`, `gt`, `gte`, `between`, `startsWith`, `endsWith`, `contains`,
|
|
10
|
+
* `like`, and `ilike`. `sort` is a field name from the sort list.
|
|
6
11
|
*/
|
|
7
12
|
import type { ParsedFilters } from "./table.js";
|
|
8
13
|
/**
|
|
9
14
|
* Reads one request object.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* Lookups use own properties, so `constructor`, `__proto__`, `toString`, and
|
|
17
|
+
* `hasOwnProperty` are ordinary keys. A key that is not in `allow` is OKM1123.
|
|
18
|
+
* `sort` is the exception.
|
|
13
19
|
*
|
|
14
20
|
* @param allow - Fields and the operators each one accepts
|
|
15
21
|
* @param sort - Fields a caller may sort by
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
staticNamespace2
|
|
7
7
|
} from "../../../shared/t4tbsy9h.js";
|
|
8
|
+
import {
|
|
9
|
+
assertIdentifier2
|
|
10
|
+
} from "../../../shared/nfzc5z0w.js";
|
|
8
11
|
import {
|
|
9
12
|
OkmError2
|
|
10
13
|
} from "../../../shared/52szzakm.js";
|
|
@@ -26,6 +29,11 @@ function fn(name, options) {
|
|
|
26
29
|
fix: { summary: "Set search_path on the function to the schemas it is allowed to see." }
|
|
27
30
|
});
|
|
28
31
|
}
|
|
32
|
+
if (options.searchPath !== undefined && options.searchPath.trim().length > 0) {
|
|
33
|
+
for (const part of options.searchPath.split(",")) {
|
|
34
|
+
assertIdentifier2(part.trim(), `function ${name} search_path`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
29
37
|
const argTypes = (options.arguments ?? []).map((argument) => argument.type);
|
|
30
38
|
const schema = options.schema ?? "public";
|
|
31
39
|
return {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
many2,
|
|
21
21
|
manyThrough2,
|
|
22
22
|
schema2
|
|
23
|
-
} from "../../shared/
|
|
23
|
+
} from "../../shared/5enne4ma.js";
|
|
24
24
|
import {
|
|
25
25
|
domainType
|
|
26
26
|
} from "../../shared/g7ez901n.js";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
table2,
|
|
33
33
|
index2,
|
|
34
34
|
sql2
|
|
35
|
-
} from "../../shared/
|
|
35
|
+
} from "../../shared/ntneex2a.js";
|
|
36
36
|
import {
|
|
37
37
|
text2,
|
|
38
38
|
varchar2,
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
tag
|
|
60
60
|
} from "../../shared/cn0n68ft.js";
|
|
61
61
|
import {
|
|
62
|
+
OkmError2,
|
|
62
63
|
catalogError2
|
|
63
64
|
} from "../../shared/52szzakm.js";
|
|
64
65
|
var __defProp = Object.defineProperty;
|
|
@@ -909,6 +910,11 @@ function notIn(values) {
|
|
|
909
910
|
}
|
|
910
911
|
// src/dialects/pg/ops/or.ts
|
|
911
912
|
function or(branches) {
|
|
913
|
+
if (arguments.length !== 1 || !Array.isArray(branches)) {
|
|
914
|
+
throw new OkmError2("OKM1121", "Pass an array: or([a, b]).", {
|
|
915
|
+
fix: { summary: "Pass an array: or([a, b])." }
|
|
916
|
+
});
|
|
917
|
+
}
|
|
912
918
|
return tag("or", branches);
|
|
913
919
|
}
|
|
914
920
|
// src/dialects/pg/ops/overlaps.ts
|
|
@@ -39,6 +39,12 @@ export type CatalogQuery = {
|
|
|
39
39
|
* @returns A checked catalog
|
|
40
40
|
*/
|
|
41
41
|
export declare function introspectSchema(runner: CatalogQuery, concrete: string, logical?: string, options?: IntrospectOptions): Promise<Catalog>;
|
|
42
|
+
/**
|
|
43
|
+
* A function's argument types as a grant names them, for `pg_proc p`.
|
|
44
|
+
*
|
|
45
|
+
* Types only, comma-separated, no spaces. Argument names are left out.
|
|
46
|
+
*/
|
|
47
|
+
export declare const FUNCTION_ARG_TYPES = "replace(oidvectortypes(p.proargtypes), ' ', '')";
|
|
42
48
|
/**
|
|
43
49
|
* Applies one expression as a check and reads Postgres's reprint.
|
|
44
50
|
*
|
|
@@ -3,7 +3,7 @@ import { type Or } from "../operators.js";
|
|
|
3
3
|
* Disjunction of where objects.
|
|
4
4
|
*
|
|
5
5
|
* @typeParam V - One branch
|
|
6
|
-
* @param branches - Where objects, or-ed
|
|
6
|
+
* @param branches - Where objects, or-ed. One array: `or([a, b])`
|
|
7
7
|
* @returns A tagged operator
|
|
8
8
|
*/
|
|
9
9
|
export declare function or<const V>(branches: readonly V[]): Or<V>;
|
|
@@ -42,6 +42,16 @@ export declare function privilegeSql(before: readonly CatalogObject[], after: re
|
|
|
42
42
|
* @returns The role, or `undefined` when the statement is not `CREATE ROLE`
|
|
43
43
|
*/
|
|
44
44
|
export declare function createdRoleName(sql: string): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Runs a plan's `CREATE ROLE` so a role another session created first is not an error.
|
|
47
|
+
*
|
|
48
|
+
* Roles belong to the cluster. Two databases provisioned at once both find the
|
|
49
|
+
* role absent, and the second `CREATE ROLE` fails on `pg_authid`.
|
|
50
|
+
*
|
|
51
|
+
* @param sql - A `CREATE ROLE` statement from the plan
|
|
52
|
+
* @returns The statement in a `DO` block that ignores a duplicate role
|
|
53
|
+
*/
|
|
54
|
+
export declare function createRoleOnce(sql: string): string;
|
|
45
55
|
/**
|
|
46
56
|
* Reports whether a statement creates or alters a role.
|
|
47
57
|
*
|
|
@@ -144,6 +144,13 @@ type ApplySchemaTraits<TTable, TFields, TTraits> = TTable extends {
|
|
|
144
144
|
readonly "~insert": InsertFrom<TFields>;
|
|
145
145
|
readonly "~update": UpdateFrom<TFields>;
|
|
146
146
|
} & HasArchive<TTraits> & PresetsOfList<TTraits>;
|
|
147
|
+
/**
|
|
148
|
+
* `~views` when `schema({ views })` declared any, so `db.views` is typed.
|
|
149
|
+
* Nothing at runtime carries the key.
|
|
150
|
+
*/
|
|
151
|
+
type SchemaViews<TViews extends readonly unknown[]> = TViews extends readonly [] ? unknown : {
|
|
152
|
+
readonly "~views": TViews;
|
|
153
|
+
};
|
|
147
154
|
/**
|
|
148
155
|
* Compiles tables into a catalog.
|
|
149
156
|
*
|
|
@@ -156,19 +163,21 @@ type ApplySchemaTraits<TTable, TFields, TTraits> = TTable extends {
|
|
|
156
163
|
*/
|
|
157
164
|
export declare function schema<const TTables extends readonly AnyTable[], const TTraits extends readonly {
|
|
158
165
|
readonly fields: Readonly<Record<string, object>>;
|
|
159
|
-
}[], const TTenancy extends ColumnTenancy | undefined = undefined, const TValidation = undefined>(config: Omit<SchemaInput<TTables>, "traits" | "tenancy" | "validation"> & {
|
|
166
|
+
}[], const TTenancy extends ColumnTenancy | undefined = undefined, const TValidation = undefined, const TViews extends readonly unknown[] = readonly []>(config: Omit<SchemaInput<TTables>, "traits" | "tenancy" | "validation" | "views"> & {
|
|
160
167
|
readonly traits: TTraits;
|
|
161
168
|
readonly tenancy?: TTenancy;
|
|
162
169
|
readonly validation?: TValidation;
|
|
163
|
-
|
|
170
|
+
readonly views?: TViews;
|
|
171
|
+
}): ([TValidation] extends [undefined] ? TenancySchema<SchemaWithTraits<TTables, TTraits>, TTables, TTenancy> : TenancySchema<SchemaWithTraits<TTables, TTraits>, TTables, TTenancy> & {
|
|
164
172
|
readonly "~validation": TValidation;
|
|
165
|
-
}
|
|
166
|
-
export declare function schema<const TTables extends readonly AnyTable[], const TTenancy extends ColumnTenancy | undefined = undefined, const TValidation = undefined>(config: Omit<SchemaInput<TTables>, "tenancy" | "validation"> & {
|
|
173
|
+
}) & SchemaViews<TViews>;
|
|
174
|
+
export declare function schema<const TTables extends readonly AnyTable[], const TTenancy extends ColumnTenancy | undefined = undefined, const TValidation = undefined, const TViews extends readonly unknown[] = readonly []>(config: Omit<SchemaInput<TTables>, "tenancy" | "validation" | "views"> & {
|
|
167
175
|
readonly tenancy?: TTenancy;
|
|
168
176
|
readonly validation?: TValidation;
|
|
169
|
-
|
|
177
|
+
readonly views?: TViews;
|
|
178
|
+
}): ([TValidation] extends [undefined] ? TenancySchema<BuiltSchema<TTables>, TTables, TTenancy> : TenancySchema<BuiltSchema<TTables>, TTables, TTenancy> & {
|
|
170
179
|
readonly "~validation": TValidation;
|
|
171
|
-
}
|
|
180
|
+
}) & SchemaViews<TViews>;
|
|
172
181
|
/**
|
|
173
182
|
* Registers a hook from an opt-in module.
|
|
174
183
|
*
|
|
@@ -114,100 +114,29 @@ export declare function decodeDuration(wire: string): Temporal.Duration;
|
|
|
114
114
|
*/
|
|
115
115
|
export declare function intervalFields(fields: string): string;
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
118
|
-
* TypeScript
|
|
117
|
+
* Empty names so `Temporal.Instant` and the other codec types merge with
|
|
118
|
+
* TypeScript's Temporal lib and with `temporal-polyfill`. Method signatures
|
|
119
|
+
* live in `temporal.local.d.ts` for this repository's typecheck only. They
|
|
120
|
+
* are not shipped: a second `toString` or `var Temporal` conflicts with those
|
|
121
|
+
* libraries (QA-L2).
|
|
119
122
|
*/
|
|
120
123
|
declare global {
|
|
121
124
|
/** Instant, plain date-time, date, time, and duration. */
|
|
122
125
|
namespace Temporal {
|
|
123
126
|
/** UTC instant. */
|
|
124
127
|
interface Instant {
|
|
125
|
-
/**
|
|
126
|
-
* @param options - Fractional digits to keep
|
|
127
|
-
* @returns ISO-8601 text
|
|
128
|
-
*/
|
|
129
|
-
toString(options?: {
|
|
130
|
-
readonly fractionalSecondDigits?: number;
|
|
131
|
-
}): string;
|
|
132
128
|
}
|
|
133
129
|
/** Date and time without a zone. */
|
|
134
130
|
interface PlainDateTime {
|
|
135
|
-
/**
|
|
136
|
-
* @param options - Fractional digits to keep
|
|
137
|
-
* @returns ISO-8601 text
|
|
138
|
-
*/
|
|
139
|
-
toString(options?: {
|
|
140
|
-
readonly fractionalSecondDigits?: number;
|
|
141
|
-
}): string;
|
|
142
131
|
}
|
|
143
132
|
/** Calendar date. */
|
|
144
133
|
interface PlainDate {
|
|
145
|
-
/**
|
|
146
|
-
* @returns `YYYY-MM-DD`
|
|
147
|
-
*/
|
|
148
|
-
toString(): string;
|
|
149
|
-
/**
|
|
150
|
-
* @param duration - Days to add
|
|
151
|
-
* @returns A later or earlier date
|
|
152
|
-
*/
|
|
153
|
-
add(duration: {
|
|
154
|
-
readonly days: number;
|
|
155
|
-
}): Temporal.PlainDate;
|
|
156
134
|
}
|
|
157
135
|
/** Time of day. */
|
|
158
136
|
interface PlainTime {
|
|
159
|
-
/**
|
|
160
|
-
* @param options - Fractional digits to keep
|
|
161
|
-
* @returns Time text
|
|
162
|
-
*/
|
|
163
|
-
toString(options?: {
|
|
164
|
-
readonly fractionalSecondDigits?: number;
|
|
165
|
-
}): string;
|
|
166
137
|
}
|
|
167
138
|
/** ISO-8601 duration. */
|
|
168
139
|
interface Duration {
|
|
169
|
-
/**
|
|
170
|
-
* @returns ISO-8601 duration text
|
|
171
|
-
*/
|
|
172
|
-
toString(): string;
|
|
173
140
|
}
|
|
174
141
|
}
|
|
175
|
-
/** Temporal constructors used by the codecs. */
|
|
176
|
-
var Temporal: {
|
|
177
|
-
readonly Instant: {
|
|
178
|
-
/**
|
|
179
|
-
* @param item - ISO-8601 instant
|
|
180
|
-
* @returns An instant
|
|
181
|
-
*/
|
|
182
|
-
from(item: string): Temporal.Instant;
|
|
183
|
-
};
|
|
184
|
-
readonly PlainDateTime: {
|
|
185
|
-
/**
|
|
186
|
-
* @param item - ISO-8601 date-time
|
|
187
|
-
* @returns A plain date-time
|
|
188
|
-
*/
|
|
189
|
-
from(item: string): Temporal.PlainDateTime;
|
|
190
|
-
};
|
|
191
|
-
readonly PlainDate: {
|
|
192
|
-
/**
|
|
193
|
-
* @param item - `YYYY-MM-DD`
|
|
194
|
-
* @returns A date
|
|
195
|
-
*/
|
|
196
|
-
from(item: string): Temporal.PlainDate;
|
|
197
|
-
};
|
|
198
|
-
readonly PlainTime: {
|
|
199
|
-
/**
|
|
200
|
-
* @param item - Time text
|
|
201
|
-
* @returns A time
|
|
202
|
-
*/
|
|
203
|
-
from(item: string): Temporal.PlainTime;
|
|
204
|
-
};
|
|
205
|
-
readonly Duration: {
|
|
206
|
-
/**
|
|
207
|
-
* @param item - ISO-8601 duration, or numeric fields
|
|
208
|
-
* @returns A duration
|
|
209
|
-
*/
|
|
210
|
-
from(item: string | Readonly<Record<string, number>>): Temporal.Duration;
|
|
211
|
-
};
|
|
212
|
-
};
|
|
213
142
|
}
|
|
@@ -34,10 +34,15 @@ export type ViewOptions = {
|
|
|
34
34
|
*
|
|
35
35
|
* `contribute` builds the catalog record. Column dependencies are filled
|
|
36
36
|
* from `pg_depend` by scratch verification, not at this call.
|
|
37
|
+
*
|
|
38
|
+
* @typeParam N - View name
|
|
39
|
+
* @typeParam C - Declared output columns
|
|
37
40
|
*/
|
|
38
|
-
export type ViewDeclaration = {
|
|
39
|
-
readonly name:
|
|
41
|
+
export type ViewDeclaration<N extends string = string, C extends readonly ViewColumnInput[] = readonly ViewColumnInput[]> = {
|
|
42
|
+
readonly name: N;
|
|
40
43
|
readonly schema: string;
|
|
44
|
+
/** Declared columns, for types only. `db.views` reads them. */
|
|
45
|
+
readonly "~columns"?: C;
|
|
41
46
|
/**
|
|
42
47
|
* Catalog record for this declaration.
|
|
43
48
|
*
|
|
@@ -73,10 +78,15 @@ export type MaterializedViewOptions = ViewOptions & {
|
|
|
73
78
|
* One declared materialized view.
|
|
74
79
|
*
|
|
75
80
|
* Identity is `(namespace, name)`. Indexes are separate catalog objects.
|
|
81
|
+
*
|
|
82
|
+
* @typeParam N - Materialized view name
|
|
83
|
+
* @typeParam C - Declared output columns
|
|
76
84
|
*/
|
|
77
|
-
export type MaterializedViewDeclaration = {
|
|
78
|
-
readonly name:
|
|
85
|
+
export type MaterializedViewDeclaration<N extends string = string, C extends readonly ViewColumnInput[] = readonly ViewColumnInput[]> = {
|
|
86
|
+
readonly name: N;
|
|
79
87
|
readonly schema: string;
|
|
88
|
+
/** Declared columns, for types only. `db.views` reads them. */
|
|
89
|
+
readonly "~columns"?: C;
|
|
80
90
|
/**
|
|
81
91
|
* Catalog records for this declaration.
|
|
82
92
|
*
|
|
@@ -100,7 +110,9 @@ export type MaterializedViewDeclaration = {
|
|
|
100
110
|
* @param options - Columns and query
|
|
101
111
|
* @returns The definition object `schema({ views })` stores
|
|
102
112
|
*/
|
|
103
|
-
export declare function view(name:
|
|
113
|
+
export declare function view<const N extends string, const C extends readonly ViewColumnInput[]>(name: N, options: ViewOptions & {
|
|
114
|
+
readonly columns: C;
|
|
115
|
+
}): ViewDeclaration<N, C>;
|
|
104
116
|
/**
|
|
105
117
|
* Declares a materialized view.
|
|
106
118
|
*
|
|
@@ -111,4 +123,6 @@ export declare function view(name: string, options: ViewOptions): ViewDeclaratio
|
|
|
111
123
|
* @param options - Columns, query, indexes, and refresh mode
|
|
112
124
|
* @returns The definition object `schema({ views })` stores
|
|
113
125
|
*/
|
|
114
|
-
export declare function materializedView(name:
|
|
126
|
+
export declare function materializedView<const N extends string, const C extends readonly ViewColumnInput[]>(name: N, options: MaterializedViewOptions & {
|
|
127
|
+
readonly columns: C;
|
|
128
|
+
}): MaterializedViewDeclaration<N, C>;
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Verifies declared views on a scratch schema.
|
|
3
3
|
*
|
|
4
4
|
* Domain and enum types from the catalog are created there, then the tables,
|
|
5
|
-
* then
|
|
6
|
-
* supplies column edges (D117).
|
|
5
|
+
* then the functions a view may call, then each view. `pg_get_viewdef`
|
|
6
|
+
* replaces the author query and `pg_depend` supplies column edges (D117).
|
|
7
7
|
* The scratch schema is dropped before this returns. Plans never use it.
|
|
8
8
|
*/
|
|
9
9
|
import type { Catalog } from "../../../contracts/catalog/types.js";
|
package/dist/runtime/client.d.ts
CHANGED
|
@@ -37,9 +37,14 @@ export type Session = {
|
|
|
37
37
|
};
|
|
38
38
|
/** Tenant value or an unscoped reason. Absent on the root client. */
|
|
39
39
|
readonly scope?: CallScope;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Shared close. `for()` and `unscoped()` reuse the same promise.
|
|
42
|
+
*
|
|
43
|
+
* `early` is set when `close()` runs before `connected` settles (QA-M5).
|
|
44
|
+
*/
|
|
41
45
|
readonly closing: {
|
|
42
46
|
current: Promise<void> | undefined;
|
|
47
|
+
early: boolean;
|
|
43
48
|
};
|
|
44
49
|
};
|
|
45
50
|
/** Per-call modifiers of a read. */
|
|
@@ -18,9 +18,10 @@ export type ConflictPlan = {
|
|
|
18
18
|
* @param table - Target table
|
|
19
19
|
* @param value - The option the caller passed
|
|
20
20
|
* @param allow - Guarded fields this write may set
|
|
21
|
+
* @param tenantKey - The tenant key on a tenant schema, which every unique includes
|
|
21
22
|
* @returns The plan, or `undefined` for the default `"error"`
|
|
22
23
|
*/
|
|
23
|
-
export declare function readConflict(table: Indexed, value: unknown, allow?: ReadonlySet<string
|
|
24
|
+
export declare function readConflict(table: Indexed, value: unknown, allow?: ReadonlySet<string>, tenantKey?: string): ConflictPlan | undefined;
|
|
24
25
|
/**
|
|
25
26
|
* Appends the conflict clause, or wraps a first-or-create insert.
|
|
26
27
|
*
|