okmodel 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +86 -6
  2. package/dist/contracts/catalog/document.d.ts +20 -0
  3. package/dist/contracts/driver.d.ts +7 -0
  4. package/dist/dialects/pg/introspect.d.ts +6 -0
  5. package/dist/dialects/pg/role/sql.d.ts +10 -0
  6. package/dist/dialects/pg/schema.d.ts +15 -6
  7. package/dist/dialects/pg/view/index.d.ts +20 -6
  8. package/dist/dialects/pg/view/scratch.d.ts +2 -2
  9. package/dist/runtime/pg/bun.d.ts +12 -7
  10. package/dist/runtime/pg/bun.js +4 -1
  11. package/dist/runtime/pg/pg.d.ts +12 -6
  12. package/dist/runtime/pg/pg.js +4 -1
  13. package/dist/runtime/pg/pglite.d.ts +11 -3
  14. package/dist/runtime/pg/pglite.js +4 -1
  15. package/dist/runtime/pg/postgresjs.d.ts +12 -6
  16. package/dist/runtime/pg/postgresjs.js +4 -1
  17. package/dist/runtime/plan.d.ts +3 -1
  18. package/dist/runtime/tenancy/index.js +1 -1
  19. package/dist/runtime/topology.d.ts +191 -0
  20. package/dist/runtime/traits/index.js +6 -6
  21. package/dist/runtime/types.d.ts +57 -2
  22. package/dist/runtime/validate/index.js +1 -1
  23. package/dist/shared/{nxb42q7h.js → 47xzb9fc.js} +2 -2
  24. package/dist/shared/{6693j9rs.js → 5fqx9yh4.js} +6 -2
  25. package/dist/shared/{8jb4ymh2.js → 8bwbwy8h.js} +12 -1
  26. package/dist/shared/{41kv2xdm.js → 8xsbagad.js} +1 -1
  27. package/dist/shared/{sr5ms7b3.js → 922qbgnw.js} +3 -3
  28. package/dist/shared/{tny2t3sm.js → 990a291j.js} +4 -4
  29. package/dist/shared/{jns2wx4t.js → 9tgmr1gh.js} +1 -1
  30. package/dist/shared/{n5y9zyj1.js → ayz64vzj.js} +13 -12
  31. package/dist/shared/{kkzwp3d2.js → bt9sfq0z.js} +16 -14
  32. package/dist/shared/{c5g81ngb.js → bzsjtd7c.js} +7 -5
  33. package/dist/shared/{radd3z4g.js → ckjbyrt9.js} +1 -1
  34. package/dist/shared/{zg24x0hm.js → dxjp14rk.js} +10 -2
  35. package/dist/shared/{xghkt7a9.js → fancm5pt.js} +6 -233
  36. package/dist/shared/{yjw02enp.js → fyqkk1m0.js} +4 -4
  37. package/dist/shared/{ygem90rn.js → g516zdsb.js} +8 -3
  38. package/dist/shared/{v7tpnhnz.js → h4nqvjkp.js} +5 -3
  39. package/dist/shared/{edwqqryd.js → h8ne16fq.js} +1 -1
  40. package/dist/shared/{ady1ms74.js → jdgqqx31.js} +5 -5
  41. package/dist/shared/{kafaa5zv.js → k7cc9nzk.js} +24 -7
  42. package/dist/shared/{7nk68fj8.js → kdkxh3kb.js} +17 -10
  43. package/dist/shared/nkc9sfqb.js +1161 -0
  44. package/dist/shared/{awrsqrn7.js → p7pfx624.js} +2 -2
  45. package/dist/shared/{xrfnq5k8.js → qgvt8nx1.js} +11 -2
  46. package/dist/shared/{84ptn35q.js → qy9rdq0g.js} +6 -5
  47. package/dist/shared/{qn0srwwq.js → r6pj4fq2.js} +5 -3
  48. package/dist/shared/{2321avmn.js → r73pt594.js} +21 -13
  49. package/dist/shared/{kj680nxv.js → rasvvzgz.js} +8 -6
  50. package/dist/shared/{hcmdtwgh.js → szm0cksa.js} +4 -1
  51. package/dist/shared/{axhk06q1.js → tq9afp8d.js} +1 -1
  52. package/dist/shared/wxn4b7pp.js +252 -0
  53. package/dist/shared/{q6zsyh31.js → xdpj5kkm.js} +19 -8
  54. package/dist/shared/{s4mb6a4b.js → xhh1srw3.js} +54 -10
  55. package/dist/shared/{jgxzjzcz.js → xsq7v6wp.js} +2 -2
  56. package/dist/shared/{hqjsxnz7.js → ykv7wh5b.js} +5 -4
  57. package/dist/shared/{q795afwa.js → z2rnngj4.js} +3 -3
  58. package/dist/tooling/cli.js +12 -12
  59. package/dist/tooling/migrate/apply.d.ts +2 -0
  60. package/dist/tooling/migrate/files.d.ts +7 -0
  61. package/dist/tooling/migrate/index.js +8 -6
  62. package/dist/tooling/testing/facts.d.ts +3 -0
  63. package/dist/tooling/testing/index.js +3 -3
  64. package/package.json +1 -1
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.4.0. Apache-2.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)
@@ -990,6 +992,83 @@ export default async function seed(t: {
990
992
  bunx okm seed seed.ts
991
993
  ```
992
994
 
995
+ ### Topology
996
+
997
+ `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`.
998
+
999
+ 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.
1000
+
1001
+ 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.
1002
+
1003
+ `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.
1004
+
1005
+ 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).
1006
+
1007
+ 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`.
1008
+
1009
+ `replicas.ts`:
1010
+
1011
+ ```ts
1012
+ import { connect } from "okmodel/pg/postgresjs";
1013
+
1014
+ import schema from "./schema.ts";
1015
+
1016
+ const url = process.env.DATABASE_URL;
1017
+ if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");
1018
+ const east = process.env.REPLICA_URL;
1019
+ if (east === undefined || east.length === 0) throw new Error("REPLICA_URL is not set");
1020
+ const west = process.env.REPLICA_URL_WEST ?? east;
1021
+
1022
+ const endpoints = {
1023
+ primary: url,
1024
+ replicas: [{ url: east, weight: 2, name: "east" }, west],
1025
+ };
1026
+
1027
+ const db = await connect(endpoints, {
1028
+ schema,
1029
+ routing: { probe: "1s", fallback: "primary", consistency: "session", select: "weighted" },
1030
+ onRoute(event) {
1031
+ // { op: "read", endpoint: "east", reason: "auto:east" }
1032
+ void event;
1033
+ },
1034
+ });
1035
+
1036
+ await db.notes.find({ limit: 50 });
1037
+ await db.notes.find({ limit: 50, route: "primary" });
1038
+ await db.notes.find({ limit: 50, route: "replica" });
1039
+ const replica = db.using("replica");
1040
+ await replica.notes.find({ limit: 50 });
1041
+ await db.close();
1042
+
1043
+ const eventual = await connect(endpoints, {
1044
+ schema,
1045
+ routing: { consistency: "eventual" },
1046
+ });
1047
+ await eventual.notes.find({ limit: 50 });
1048
+ await eventual.close();
1049
+
1050
+ for (const select of ["roundRobin", "leastConnections", "latencyAware"] as const) {
1051
+ const client = await connect(endpoints, { schema, routing: { select } });
1052
+ await client.notes.find({ limit: 1 });
1053
+ await client.close();
1054
+ }
1055
+
1056
+ const custom = await connect(endpoints, {
1057
+ schema,
1058
+ routing: {
1059
+ select(candidates) {
1060
+ return candidates[0] ?? "east";
1061
+ },
1062
+ },
1063
+ });
1064
+ await custom.notes.find({ limit: 1 });
1065
+ await custom.close();
1066
+ ```
1067
+
1068
+ ### Reference app
1069
+
1070
+ 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).
1071
+
993
1072
  ### Startup
994
1073
 
995
1074
  `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)`.
@@ -1064,7 +1143,7 @@ Status is on the [board](https://github.com/users/omqkhafi/projects/1). Each rel
1064
1143
  - [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
1144
  - [x] [0.3](https://github.com/omqkhafi/okmodel/milestone/3) — Extensions, domains, functions, triggers, views, roles, and grants.
1066
1145
  - [x] [0.4](https://github.com/omqkhafi/okmodel/milestone/4) — Safer migration plans, backfill, drift checks, provisioning, reference data, and a testing package.
1067
- - [ ] [0.5](https://github.com/omqkhafi/okmodel/milestone/5) — A primary with replicas, read routing, and a reference app.
1146
+ - [x] [0.5](https://github.com/omqkhafi/okmodel/milestone/5) — A primary with replicas, read routing, and a reference app.
1068
1147
 
1069
1148
  `okmodel/internal` has no stability promise. Names on that subpath can change or disappear in any release.
1070
1149
 
@@ -1076,11 +1155,11 @@ Measured on this release.
1076
1155
 
1077
1156
  | | Minified | Gzip | Cold import |
1078
1157
  | -------------------------------------------- | -------: | -----: | ----------: |
1079
- | Runtime entry | 5,288 | 2,026 | 2.563 ms |
1080
- | App startup (10 tables, one find) | 89,461 | 29,720 | 13.379 ms |
1081
- | App startup, every 0.2 feature in use (full) | 119,936 | 39,121 | 13.745 ms |
1158
+ | Runtime entry | 5,288 | 2,026 | 2.347 ms |
1159
+ | App startup (10 tables, one find) | 89,751 | 29,801 | 15.061 ms |
1160
+ | App startup, every 0.2 feature in use (full) | 120,132 | 39,162 | 15.956 ms |
1082
1161
 
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,475 minified and 9,401 gzip bytes above the plain one. The runtime entry and the plain app are gated. The full app is printed, not gated.
1162
+ 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
1163
 
1085
1164
  The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/blob/main/docs/size.md).
1086
1165
 
@@ -1092,5 +1171,6 @@ The rest of the measurements are in [size](https://github.com/omqkhafi/okmodel/b
1092
1171
  - [Backfill](https://github.com/omqkhafi/okmodel/blob/main/docs/backfill.md)
1093
1172
  - [Provisioning](https://github.com/omqkhafi/okmodel/blob/main/docs/provisioning.md)
1094
1173
  - [Testing](https://github.com/omqkhafi/okmodel/blob/main/docs/testing.md)
1174
+ - [Topology](https://github.com/omqkhafi/okmodel/blob/main/docs/topology.md)
1095
1175
  - [Known limits](https://github.com/omqkhafi/okmodel/blob/main/docs/known-limits.md)
1096
1176
  - [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 = {
@@ -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
  *
@@ -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
- }): [TValidation] extends [undefined] ? TenancySchema<SchemaWithTraits<TTables, TTraits>, TTables, TTenancy> : TenancySchema<SchemaWithTraits<TTables, TTraits>, TTables, TTenancy> & {
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
- }): [TValidation] extends [undefined] ? TenancySchema<BuiltSchema<TTables>, TTables, TTenancy> : TenancySchema<BuiltSchema<TTables>, TTables, TTenancy> & {
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
  *
@@ -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: string;
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: string;
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: string, options: ViewOptions): ViewDeclaration;
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: string, options: MaterializedViewOptions): MaterializedViewDeclaration;
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 each view. `pg_get_viewdef` replaces the author query and `pg_depend`
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";
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * `connect` for Bun.sql (`okmodel/pg/bun`).
3
3
  *
4
- * One URL or an existing pool is one endpoint. The client is typed by the
5
- * schema passed in. This entry runs on Bun. Node cannot load the `bun` import
6
- * inside the adapter.
4
+ * A string or an existing pool is one endpoint. A `{ primary, replicas }` object
5
+ * loads the topology module. The client is typed by the schema passed in.
6
+ * This entry runs on Bun. Node cannot load the `bun` import inside the adapter.
7
7
  */
8
8
  import type { DriverPool } from "../../contracts/driver.js";
9
9
  import type { QuerySchema } from "../../dialects/pg/model.js";
10
10
  import { type BunSqlConfig } from "../../adapters/pg/bunsql.js";
11
+ import type { RoutedClient, TopologyInput, TopologyOptions } from "../topology.js";
11
12
  import type { ConnectOptions, Connected } from "../types.js";
12
13
  export { open, type BunSqlConfig, type BunSqlTls } from "../../adapters/pg/bunsql.js";
13
14
  export { capabilities, hasCapability, readCapability } from "../../adapters/pg/bunsql.js";
@@ -15,16 +16,20 @@ export { DriverError } from "../../adapters/error.js";
15
16
  /** A connection string or a pool from {@link open}. */
16
17
  export type BunSqlTarget = string | DriverPool;
17
18
  /** Driver options `connect` forwards to {@link open}. */
18
- export type BunSqlConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<BunSqlConfig, "url">;
19
+ export type BunSqlConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<BunSqlConfig, "url"> & TopologyOptions;
19
20
  /**
20
- * Connects one Postgres endpoint through Bun.sql and returns a client for `schema`.
21
+ * Connects one Postgres endpoint through Bun.sql, or a primary and its replicas.
22
+ *
23
+ * A string or a pool is one endpoint. A topology loads on demand and the promise
24
+ * resolves to the client. Reads use the first healthy replica.
21
25
  *
22
26
  * Named prepared statements are off unless `prepared` is `"named"`.
23
27
  * `prepared: "named"` is not for transaction-mode poolers.
24
28
  *
25
29
  * @typeParam S - Schema
26
- * @param target - URL or an existing pool
30
+ * @param target - URL, an existing pool, or `{ primary, replicas }`
27
31
  * @param options - Schema, errors, logger, and driver options
28
- * @returns The client. Await `connected` for the dialect check
32
+ * @returns The client. A topology returns a promise. Await `connected` for the dialect check
29
33
  */
34
+ export declare function connect<const S extends QuerySchema>(target: TopologyInput, options: BunSqlConnectOptions<S>): Promise<RoutedClient<S>>;
30
35
  export declare function connect<const S extends QuerySchema>(target: BunSqlTarget, options: BunSqlConnectOptions<S>): Connected<S>;
@@ -11,10 +11,13 @@ import {
11
11
  } from "../../shared/rrstp3kb.js";
12
12
  import {
13
13
  createClient
14
- } from "../../shared/kkzwp3d2.js";
14
+ } from "../../shared/bt9sfq0z.js";
15
15
 
16
16
  // src/runtime/pg/bun.ts
17
17
  function connect(target, options) {
18
+ if (typeof target === "object" && !isPool(target)) {
19
+ return import("../../shared/nkc9sfqb.js").then((mod) => mod.default(target, options, open5));
20
+ }
18
21
  const owned = !isPool(target);
19
22
  const pool = isPool(target) ? target : open5({
20
23
  url: target,
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * `connect` for node-postgres (`okmodel/pg/pg`).
3
3
  *
4
- * One URL or an existing pool is one endpoint. The client is typed by the
5
- * schema passed in, not by `Register`.
4
+ * A string or an existing pool is one endpoint. A `{ primary, replicas }` object
5
+ * loads the topology module. The client is typed by the schema passed in, not
6
+ * by `Register`.
6
7
  */
7
8
  import type { DriverPool } from "../../contracts/driver.js";
8
9
  import type { QuerySchema } from "../../dialects/pg/model.js";
9
10
  import { type NodePostgresConfig } from "../../adapters/pg/nodepostgres.js";
11
+ import type { RoutedClient, TopologyInput, TopologyOptions } from "../topology.js";
10
12
  import type { ConnectOptions, Connected } from "../types.js";
11
13
  export { open, type NodePostgresConfig } from "../../adapters/pg/nodepostgres.js";
12
14
  export { capabilities, hasCapability, readCapability } from "../../adapters/pg/nodepostgres.js";
@@ -14,16 +16,20 @@ export { DriverError } from "../../adapters/error.js";
14
16
  /** A connection string or a pool from {@link open}. */
15
17
  export type NodePostgresTarget = string | DriverPool;
16
18
  /** Driver options `connect` forwards to {@link open}. */
17
- export type NodePostgresConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<NodePostgresConfig, "url">;
19
+ export type NodePostgresConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<NodePostgresConfig, "url"> & TopologyOptions;
18
20
  /**
19
- * Connects one Postgres endpoint through node-postgres and returns a client for `schema`.
21
+ * Connects one Postgres endpoint through node-postgres, or a primary and its replicas.
22
+ *
23
+ * A string or a pool is one endpoint. A topology loads on demand and the promise
24
+ * resolves to the client. Reads use the first healthy replica.
20
25
  *
21
26
  * Named prepared statements are off unless `prepared` is `"named"`.
22
27
  * `prepared: "named"` is not for transaction-mode poolers.
23
28
  *
24
29
  * @typeParam S - Schema
25
- * @param target - URL or an existing pool
30
+ * @param target - URL, an existing pool, or `{ primary, replicas }`
26
31
  * @param options - Schema, errors, logger, and driver options
27
- * @returns The client. Await `connected` for the dialect check
32
+ * @returns The client. A topology returns a promise. Await `connected` for the dialect check
28
33
  */
34
+ export declare function connect<const S extends QuerySchema>(target: TopologyInput, options: NodePostgresConnectOptions<S>): Promise<RoutedClient<S>>;
29
35
  export declare function connect<const S extends QuerySchema>(target: NodePostgresTarget, options: NodePostgresConnectOptions<S>): Connected<S>;
@@ -11,10 +11,13 @@ import {
11
11
  } from "../../shared/rrstp3kb.js";
12
12
  import {
13
13
  createClient
14
- } from "../../shared/kkzwp3d2.js";
14
+ } from "../../shared/bt9sfq0z.js";
15
15
 
16
16
  // src/runtime/pg/pg.ts
17
17
  function connect(target, options) {
18
+ if (typeof target === "object" && !isPool(target)) {
19
+ return import("../../shared/nkc9sfqb.js").then((mod) => mod.default(target, options, open4));
20
+ }
18
21
  const owned = !isPool(target);
19
22
  const pool = isPool(target) ? target : open4({
20
23
  url: target,
@@ -2,10 +2,13 @@
2
2
  * `connect` for PGlite (`okmodel/pg/pglite`).
3
3
  *
4
4
  * Opening a database is async. An existing pool is reused and not closed.
5
+ * A `{ primary, replicas }` object loads the topology module. A replica URL is
6
+ * that endpoint's data directory.
5
7
  */
6
8
  import type { DriverPool } from "../../contracts/driver.js";
7
9
  import type { QuerySchema } from "../../dialects/pg/model.js";
8
10
  import { type PgliteConfig } from "../../adapters/pg/pglite.js";
11
+ import type { RoutedClient, TopologyInput, TopologyOptions } from "../topology.js";
9
12
  import type { ConnectOptions, Connected } from "../types.js";
10
13
  export { open, type PgliteConfig } from "../../adapters/pg/pglite.js";
11
14
  export { capabilities, hasCapability, readCapability } from "../../adapters/pg/pglite.js";
@@ -13,13 +16,18 @@ export { DriverError } from "../../adapters/error.js";
13
16
  /** A data directory, a pool from {@link open}, or omitted for memory. */
14
17
  export type PgliteTarget = string | DriverPool | undefined;
15
18
  /** Driver options `connect` forwards to {@link open}. */
16
- export type PgliteConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<PgliteConfig, "dataDir">;
19
+ export type PgliteConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<PgliteConfig, "dataDir"> & TopologyOptions;
17
20
  /**
18
- * Connects one PGlite endpoint and returns a client for `schema`.
21
+ * Connects one PGlite endpoint, or a primary and its replicas, and returns a client for `schema`.
22
+ *
23
+ * A string, a pool, or an omitted target is one endpoint. A topology loads on
24
+ * demand. Reads use the first healthy replica. A replica `url` is that
25
+ * endpoint's data directory.
19
26
  *
20
27
  * @typeParam S - Schema
21
- * @param target - Data directory, existing pool, or omitted for memory
28
+ * @param target - Data directory, existing pool, omitted for memory, or `{ primary, replicas }`
22
29
  * @param options - Schema, errors, and logger
23
30
  * @returns The client
24
31
  */
32
+ export declare function connect<const S extends QuerySchema>(target: TopologyInput, options: PgliteConnectOptions<S>): Promise<RoutedClient<S>>;
25
33
  export declare function connect<const S extends QuerySchema>(target: PgliteTarget, options: PgliteConnectOptions<S>): Promise<Connected<S>>;
@@ -11,10 +11,13 @@ import {
11
11
  } from "../../shared/rrstp3kb.js";
12
12
  import {
13
13
  createClient
14
- } from "../../shared/kkzwp3d2.js";
14
+ } from "../../shared/bt9sfq0z.js";
15
15
 
16
16
  // src/runtime/pg/pglite.ts
17
17
  async function connect(target, options) {
18
+ if (typeof target === "object" && !isPool(target)) {
19
+ return import("../../shared/nkc9sfqb.js").then((mod) => mod.default(target, options, open3));
20
+ }
18
21
  const pool = isPool(target) ? target : await open3({
19
22
  ...target !== undefined ? { dataDir: target } : {},
20
23
  ...options.timeouts !== undefined ? { timeouts: options.timeouts } : {}
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * `connect` for postgres.js (`okmodel/pg/postgresjs`).
3
3
  *
4
- * One URL or an existing pool is one endpoint. The client is typed by the
5
- * schema passed in, not by `Register`.
4
+ * A string or an existing pool is one endpoint. A `{ primary, replicas }` object
5
+ * loads the topology module. The client is typed by the schema passed in, not
6
+ * by `Register`.
6
7
  */
7
8
  import type { DriverPool } from "../../contracts/driver.js";
8
9
  import type { QuerySchema } from "../../dialects/pg/model.js";
9
10
  import { type PostgresJsConfig } from "../../adapters/pg/postgresjs.js";
11
+ import type { RoutedClient, TopologyInput, TopologyOptions } from "../topology.js";
10
12
  import type { ConnectOptions, Connected } from "../types.js";
11
13
  export { open, type PostgresJsConfig } from "../../adapters/pg/postgresjs.js";
12
14
  export { capabilities, hasCapability, readCapability } from "../../adapters/pg/postgresjs.js";
@@ -14,16 +16,20 @@ export { DriverError } from "../../adapters/error.js";
14
16
  /** A connection string or a pool from {@link open}. */
15
17
  export type PostgresTarget = string | DriverPool;
16
18
  /** Driver options `connect` forwards to {@link open}. */
17
- export type PostgresConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<PostgresJsConfig, "url">;
19
+ export type PostgresConnectOptions<S extends QuerySchema> = ConnectOptions<S> & Omit<PostgresJsConfig, "url"> & TopologyOptions;
18
20
  /**
19
- * Connects one Postgres endpoint and returns a client for `schema`.
21
+ * Connects one Postgres endpoint, or a primary and its replicas, and returns a client for `schema`.
22
+ *
23
+ * A string or a pool is one endpoint. A topology loads on demand and the promise
24
+ * resolves to the client. Reads use the first healthy replica.
20
25
  *
21
26
  * Named prepared statements are off unless `prepared` is `"named"`.
22
27
  * `prepared: "named"` is not for transaction-mode poolers.
23
28
  *
24
29
  * @typeParam S - Schema
25
- * @param target - URL or an existing pool
30
+ * @param target - URL, an existing pool, or `{ primary, replicas }`
26
31
  * @param options - Schema, errors, logger, and driver options
27
- * @returns The client. Await `connected` for the dialect check
32
+ * @returns The client. A topology returns a promise. Await `connected` for the dialect check
28
33
  */
34
+ export declare function connect<const S extends QuerySchema>(target: TopologyInput, options: PostgresConnectOptions<S>): Promise<RoutedClient<S>>;
29
35
  export declare function connect<const S extends QuerySchema>(target: PostgresTarget, options: PostgresConnectOptions<S>): Connected<S>;
@@ -11,10 +11,13 @@ import {
11
11
  } from "../../shared/rrstp3kb.js";
12
12
  import {
13
13
  createClient
14
- } from "../../shared/kkzwp3d2.js";
14
+ } from "../../shared/bt9sfq0z.js";
15
15
 
16
16
  // src/runtime/pg/postgresjs.ts
17
17
  function connect(target, options) {
18
+ if (typeof target === "object" && !isPool(target)) {
19
+ return import("../../shared/nkc9sfqb.js").then((mod) => mod.default(target, options, open2));
20
+ }
18
21
  const owned = !isPool(target);
19
22
  const pool = isPool(target) ? target : open2({
20
23
  url: target,
@@ -41,9 +41,11 @@ export type ReadCall = {
41
41
  readonly build?: ReadBuild;
42
42
  /** Writes the end of the statement, after `limit`. A row lock sets it. */
43
43
  readonly tail?: (sink: Sink) => void;
44
- /** The caller's `signal` and `timeout`. They never enter the plan or its key. */
44
+ /** The caller's `signal`, `timeout`, and `route`. They never enter the plan or its key. */
45
45
  readonly signal?: AbortSignal;
46
46
  readonly timeout?: number;
47
+ /** `"primary"` or `"replica"`. Absent means the router classifies the statement. */
48
+ readonly route?: "primary" | "replica";
47
49
  /** Set by `for()` or `unscoped()`. Absent on a schema with no tenancy. */
48
50
  readonly scope?: CallScope;
49
51
  /** Inspect lines from the tenancy object. Absent when the schema has no tenancy. */
@@ -11,7 +11,7 @@ import {
11
11
  } from "../../shared/tmbdywx0.js";
12
12
  import {
13
13
  quote
14
- } from "../../shared/xrfnq5k8.js";
14
+ } from "../../shared/qgvt8nx1.js";
15
15
  import {
16
16
  assertIdentifier2
17
17
  } from "../../shared/nfzc5z0w.js";