okmodel 0.2.1 → 0.3.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 (108) hide show
  1. package/README.md +710 -36
  2. package/dist/contracts/catalog/enum.d.ts +31 -4
  3. package/dist/contracts/catalog/extension.d.ts +61 -0
  4. package/dist/contracts/catalog/privilege.d.ts +64 -0
  5. package/dist/contracts/catalog/routine.d.ts +56 -0
  6. package/dist/contracts/catalog/types.d.ts +164 -10
  7. package/dist/contracts/catalog/view.d.ts +73 -0
  8. package/dist/dialects/pg/column.d.ts +3 -1
  9. package/dist/dialects/pg/ddl.d.ts +65 -1
  10. package/dist/dialects/pg/enum.d.ts +10 -8
  11. package/dist/dialects/pg/ext/citext.d.ts +14 -0
  12. package/dist/dialects/pg/ext/citext.js +11 -0
  13. package/dist/dialects/pg/ext/index.d.ts +52 -0
  14. package/dist/dialects/pg/ext/index.js +6 -0
  15. package/dist/dialects/pg/ext/pg-trgm.d.ts +53 -0
  16. package/dist/dialects/pg/ext/pg-trgm.js +42 -0
  17. package/dist/dialects/pg/fn/index.d.ts +107 -0
  18. package/dist/dialects/pg/fn/index.js +175 -0
  19. package/dist/dialects/pg/fn/touch.d.ts +14 -0
  20. package/dist/dialects/pg/index.js +104 -19
  21. package/dist/dialects/pg/introspect.d.ts +10 -1
  22. package/dist/dialects/pg/operators.d.ts +8 -1
  23. package/dist/dialects/pg/role/check.d.ts +45 -0
  24. package/dist/dialects/pg/role/index.d.ts +50 -0
  25. package/dist/dialects/pg/role/sql.d.ts +44 -0
  26. package/dist/dialects/pg/table.d.ts +7 -0
  27. package/dist/dialects/pg/tenancy.d.ts +9 -0
  28. package/dist/dialects/pg/view/client.d.ts +30 -0
  29. package/dist/dialects/pg/view/depend.d.ts +35 -0
  30. package/dist/dialects/pg/view/index.d.ts +114 -0
  31. package/dist/dialects/pg/view/index.js +289 -0
  32. package/dist/dialects/pg/view/scratch.d.ts +21 -0
  33. package/dist/dialects/pg/where.d.ts +2 -2
  34. package/dist/runtime/pg/bun.js +3 -3
  35. package/dist/runtime/pg/pg.js +3 -3
  36. package/dist/runtime/pg/pglite.js +3 -3
  37. package/dist/runtime/pg/postgresjs.js +3 -3
  38. package/dist/runtime/tenancy/index.js +6 -3
  39. package/dist/runtime/traits/index.d.ts +7 -2
  40. package/dist/runtime/traits/index.js +149 -12
  41. package/dist/runtime/validate/index.js +4 -4
  42. package/dist/shared/{ytg3xf9p.js → 01yr20s5.js} +3 -3
  43. package/dist/shared/{cstgkzdx.js → 0jc3ffzf.js} +1 -1
  44. package/dist/shared/{xkz8g6by.js → 1g0drakv.js} +2 -2
  45. package/dist/shared/{eaxwrgby.js → 1tfdfvhh.js} +4 -3
  46. package/dist/shared/2ssqrv66.js +1576 -0
  47. package/dist/shared/{10tjenmd.js → 3ekj9x92.js} +1 -1
  48. package/dist/shared/49ypnfwt.js +1122 -0
  49. package/dist/shared/{sacqha1y.js → 4m9fn52z.js} +2 -2
  50. package/dist/shared/5nmdenb6.js +1229 -0
  51. package/dist/shared/{qa5dsc7z.js → 6bazvqwm.js} +3 -3
  52. package/dist/shared/{x9v1qgeb.js → 6w5j844a.js} +1 -1
  53. package/dist/shared/{0qkxbm1f.js → 75xkjv0t.js} +1 -1
  54. package/dist/shared/7fvtrkwr.js +84 -0
  55. package/dist/shared/94ez8kzh.js +19 -0
  56. package/dist/shared/{1dp5knt7.js → 999ade2q.js} +2 -2
  57. package/dist/shared/{3a5ne750.js → 9m80mkac.js} +2 -2
  58. package/dist/shared/9x25fxhc.js +150 -0
  59. package/dist/shared/{4b9da9d7.js → axhk06q1.js} +19 -1
  60. package/dist/shared/{38crxbgn.js → bqha5cg6.js} +3 -3
  61. package/dist/shared/{zd93g3rq.js → bryqk9k9.js} +1 -1
  62. package/dist/shared/{n47rd821.js → c8aghh05.js} +1 -1
  63. package/dist/shared/{dd4f8mnx.js → c9z5g0fm.js} +4 -2
  64. package/dist/shared/{eqmtn1bj.js → dbjej1tw.js} +71 -14
  65. package/dist/shared/{ps43134h.js → dyj86cmj.js} +4 -4
  66. package/dist/shared/{sfh4pk75.js → fptpmvxp.js} +3 -3
  67. package/dist/shared/{97g2jnrw.js → fs0ex00p.js} +3 -3
  68. package/dist/shared/{rtnaqre0.js → g4f5kcx9.js} +2 -2
  69. package/dist/shared/{yw5c610b.js → gjthds9j.js} +2 -2
  70. package/dist/shared/hcmdtwgh.js +102 -0
  71. package/dist/shared/{qxa0yb9j.js → jkc6ws1p.js} +2 -54
  72. package/dist/shared/{mxmcdsh5.js → k34e0xf2.js} +9 -9
  73. package/dist/shared/{py3nh7jy.js → kgnc8grt.js} +2 -2
  74. package/dist/shared/{g9f0zqce.js → m78s7898.js} +1 -1
  75. package/dist/shared/meck53jv.js +99 -0
  76. package/dist/shared/{v18jva5b.js → mt5wswjz.js} +1 -1
  77. package/dist/shared/{g1b5zzj9.js → n0dgs0pb.js} +1 -1
  78. package/dist/shared/{zt8y0wzb.js → n7mcc1gk.js} +1 -1
  79. package/dist/shared/ne02ng5c.js +68 -0
  80. package/dist/shared/{e7a268fy.js → p24x2334.js} +1 -1
  81. package/dist/shared/qbxzvwwc.js +88 -0
  82. package/dist/shared/{a1gdp7j9.js → qybh48nd.js} +1 -1
  83. package/dist/shared/{d04j53v8.js → scwxrpx2.js} +5 -4
  84. package/dist/shared/{d2whjz4k.js → t616ynfn.js} +530 -24
  85. package/dist/shared/th14ygv2.js +69 -0
  86. package/dist/shared/{xatfg0gn.js → tmbdywx0.js} +4 -19
  87. package/dist/shared/{ge03bfca.js → ty3v3hfk.js} +2 -2
  88. package/dist/shared/wekze34d.js +246 -0
  89. package/dist/shared/{7mht49yd.js → x6wysh83.js} +59 -18
  90. package/dist/shared/{4jzzghr6.js → xva8v7qk.js} +8 -8
  91. package/dist/shared/{ecdzej21.js → xw1d69vd.js} +7 -7
  92. package/dist/shared/y005b761.js +112 -0
  93. package/dist/shared/{mrq77sgw.js → ydaz1sv3.js} +1 -1
  94. package/dist/tooling/cli.js +32 -8
  95. package/dist/tooling/migrate/apply.d.ts +16 -0
  96. package/dist/tooling/migrate/commands.d.ts +2 -1
  97. package/dist/tooling/migrate/config.d.ts +8 -0
  98. package/dist/tooling/migrate/doctor.d.ts +25 -0
  99. package/dist/tooling/migrate/drift.d.ts +19 -0
  100. package/dist/tooling/migrate/ext.d.ts +17 -0
  101. package/dist/tooling/migrate/extensions.d.ts +51 -0
  102. package/dist/tooling/migrate/managed.d.ts +17 -0
  103. package/dist/tooling/migrate/plan.d.ts +14 -1
  104. package/dist/tooling/migrate/project.d.ts +25 -1
  105. package/dist/tooling/migrate/type-name.d.ts +19 -0
  106. package/package.json +22 -2
  107. package/dist/shared/5z4q2zda.js +0 -2073
  108. /package/dist/shared/{9k6vetvf.js → rrstp3kb.js} +0 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Role checks for `okm doctor` and apply preflight.
3
+ *
4
+ * Both run before any catalog statement. `connect()` does not call them.
5
+ */
6
+ import type { DriverPool } from "../../../contracts/driver.js";
7
+ import type { CatalogObject } from "../../../contracts/catalog/types.js";
8
+ import type { RolesInput } from "./index.js";
9
+ /** A pool or a reserved connection. Both can run one statement. */
10
+ type Runner = Pick<DriverPool, "execute">;
11
+ /**
12
+ * Checks named roles, `CREATEROLE`, and the application role's privileges.
13
+ *
14
+ * A missing external role is reported. A managed role that is absent is not:
15
+ * the plan creates it. OKM1825 is raised only when the object exists and the
16
+ * privilege is missing.
17
+ *
18
+ * @param runner - Database connection
19
+ * @param roles - `defineConfig({ roles })`
20
+ * @param objects - Catalog, including grants
21
+ */
22
+ export declare function assertRoleHealth(runner: Runner, roles: RolesInput, objects: readonly CatalogObject[]): Promise<void>;
23
+ /**
24
+ * Refuses to apply role changes when the acting role lacks `CREATEROLE`.
25
+ *
26
+ * @param runner - Database connection
27
+ * @param role - The role that will run `CREATE ROLE` or `ALTER ROLE`
28
+ */
29
+ export declare function assertCreateRole(runner: Runner, role: string): Promise<void>;
30
+ /**
31
+ * Reads `current_user`.
32
+ *
33
+ * @param runner - Database connection
34
+ * @returns The role this session is acting as
35
+ */
36
+ export declare function currentUser(runner: Runner): Promise<string>;
37
+ /**
38
+ * Reports whether `pg_roles` has the name.
39
+ *
40
+ * @param runner - Database connection
41
+ * @param name - Role name
42
+ * @returns `true` when the role exists
43
+ */
44
+ export declare function roleExists(runner: Runner, name: string): Promise<boolean>;
45
+ export {};
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `roles: { migration, app }`.
3
+ *
4
+ * The object contributes role, grant, and default-privilege records. `schema()`
5
+ * does not import this module. Tooling calls {@link attachRoles} when the
6
+ * config names roles.
7
+ */
8
+ import type { Catalog, CatalogObject } from "../../../contracts/catalog/types.js";
9
+ /** A role the plan creates and alters. It is never dropped. */
10
+ export type ManagedRole = {
11
+ readonly name: string;
12
+ /** `LOGIN` when true. The default is `NOLOGIN`. */
13
+ readonly login?: boolean;
14
+ /** `INHERIT` when not `false`. */
15
+ readonly inherit?: boolean;
16
+ };
17
+ /**
18
+ * Roles named by `defineConfig`.
19
+ *
20
+ * A string is external: it must already exist. A name listed in `managed` is
21
+ * created when `pg_roles` does not have it.
22
+ */
23
+ export type RolesInput = {
24
+ /** Role that runs migrations. */
25
+ readonly migration: string;
26
+ /** Role the application connects as. */
27
+ readonly app: string;
28
+ /** Roles this catalog creates and alters. */
29
+ readonly managed?: readonly ManagedRole[];
30
+ };
31
+ /**
32
+ * Adds role, grant, and default-privilege objects to a catalog.
33
+ *
34
+ * Grants and default privileges are for {@link RolesInput.app} on every
35
+ * managed table, view, materialized view, sequence, and function. Fine-grained
36
+ * grants are not accepted here.
37
+ *
38
+ * @param source - Catalog from `schema()`
39
+ * @param input - Migration role, application role, and managed roles
40
+ * @returns A catalog that includes those objects
41
+ */
42
+ export declare function attachRoles(source: Catalog, input: RolesInput): Catalog;
43
+ /**
44
+ * Catalog records for one `roles` configuration.
45
+ *
46
+ * @param input - Migration role, application role, and managed roles
47
+ * @param built - Objects already in the catalog
48
+ * @returns Role, grant, and default-privilege records
49
+ */
50
+ export declare function contributeRoles(input: RolesInput, built: readonly CatalogObject[]): readonly CatalogObject[];
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SQL for roles, grants, and default privileges.
3
+ *
4
+ * The plan calls these. `SET ROLE` is not one of them: the runner issues it
5
+ * once, outside the plan (D119).
6
+ */
7
+ import type { CatalogObject } from "../../../contracts/catalog/types.js";
8
+ /** Statements the planner inserts around the rest of the catalog. */
9
+ export type PrivilegeSql = {
10
+ /** Revokes, run while the objects still exist. */
11
+ readonly revoke: readonly string[];
12
+ /** Role creates, role alters, and default privileges. Before new tables. */
13
+ readonly prepare: readonly string[];
14
+ /** Grants on objects that exist, or that this plan creates first. */
15
+ readonly grant: readonly string[];
16
+ };
17
+ /**
18
+ * Diffs role, grant, and default-privilege objects into SQL.
19
+ *
20
+ * A role is created only when it is managed and absent. It is altered only
21
+ * when both sides are managed. It is never dropped. Grants and default
22
+ * privileges are revoked and granted. None of the statements use
23
+ * `IF NOT EXISTS`.
24
+ *
25
+ * @param before - Previous catalog objects
26
+ * @param after - Next catalog objects
27
+ * @param schema - Concrete schema name
28
+ * @returns Statements in three groups
29
+ */
30
+ export declare function privilegeSql(before: readonly CatalogObject[], after: readonly CatalogObject[], schema: string): PrivilegeSql;
31
+ /**
32
+ * Role name in `create role`, when the statement is one.
33
+ *
34
+ * @param sql - One plan statement
35
+ * @returns The role, or `undefined` when the statement is not `CREATE ROLE`
36
+ */
37
+ export declare function createdRoleName(sql: string): string | undefined;
38
+ /**
39
+ * Reports whether a statement creates or alters a role.
40
+ *
41
+ * @param sql - One plan statement
42
+ * @returns `true` for `CREATE ROLE` and `ALTER ROLE`
43
+ */
44
+ export declare function changesRole(sql: string): boolean;
@@ -28,6 +28,13 @@ export type IndexCall = {
28
28
  * Absent on an ordinary index.
29
29
  */
30
30
  readonly predicate?: string;
31
+ /**
32
+ * Raw index tail an extension may set on the call.
33
+ *
34
+ * A `using …` tail is the extension's index method. The core factory does
35
+ * not read it. `compileIndexes` copies the field onto the catalog index.
36
+ */
37
+ readonly expression?: string;
31
38
  /**
32
39
  * Marks the index unique.
33
40
  *
@@ -81,6 +81,15 @@ export type ColumnTenancy = {
81
81
  * @param ctx - Names, scope, and the table map
82
82
  */
83
83
  hook(target: Record<string, unknown>, ctx: SchemaHookCtx): void;
84
+ /**
85
+ * Treats a view that exposes the tenant key as tenant-scoped.
86
+ *
87
+ * The predicate then applies to reads of that view. A view that does not
88
+ * expose the key is refused earlier (OKM1820) unless it is `global`.
89
+ *
90
+ * @param name - Client name of the view
91
+ */
92
+ scopeView(name: string): void;
84
93
  missing(table: string): never;
85
94
  };
86
95
  /**
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Read model and client hook for a declared view.
3
+ *
4
+ * `schema()` calls `install` and the client calls `hook`. Neither path is
5
+ * imported by a featureless app. Writes are not copied onto the handle.
6
+ */
7
+ import type { ColumnTenancy } from "../tenancy.js";
8
+ import type { SchemaHook, TableModel } from "../model.js";
9
+ import { type AnyTable } from "../table.js";
10
+ /** What `schema()` passes when it binds a view. */
11
+ export type ViewInstall = (model: {
12
+ [name: string]: TableModel;
13
+ }, tenancy: ColumnTenancy | undefined, tables: readonly AnyTable[], casing: "snake" | undefined, scoped: Set<string>) => void;
14
+ /**
15
+ * Client name, read model, and the hook that moves the handle under `views`.
16
+ *
17
+ * @param name - SQL name of the view
18
+ * @param columns - Declared output columns
19
+ * @param query - Author SQL
20
+ * @param tenancy - `global("reason")`, when the view opts out
21
+ * @returns Methods stored on the declaration
22
+ */
23
+ export declare function viewClient(name: string, columns: readonly {
24
+ readonly name: string;
25
+ readonly type: string;
26
+ }[], query: string, tenancy: unknown): {
27
+ readonly field: string;
28
+ readonly install: ViewInstall;
29
+ readonly hook: SchemaHook;
30
+ };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Column dependencies of a view, read from `pg_depend`.
3
+ *
4
+ * Postgres records them on the view's rewrite rule. The edge is column
5
+ * granularity when that column is a catalog object.
6
+ */
7
+ import type { CatalogObject, Namespace, ObjectIdentity } from "../../../contracts/catalog/types.js";
8
+ /**
9
+ * Reads a query cell as text.
10
+ *
11
+ * @param value - A driver cell
12
+ * @returns The string, or empty when the cell is not a scalar
13
+ */
14
+ export declare function cellText(value: unknown): string;
15
+ /**
16
+ * `pg_depend` rows for views and materialized views in one schema.
17
+ *
18
+ * `$1` is the concrete schema name. `refobjsubid > 0` keeps column edges.
19
+ */
20
+ export declare const VIEW_DEPENDENCIES = "\n select v.relname as view, t.relname as table, a.attname as column\n from pg_depend d\n join pg_rewrite r on r.oid = d.objid and d.classid = 'pg_rewrite'::regclass\n join pg_class v on v.oid = r.ev_class\n join pg_class t on t.oid = d.refobjid and d.refclassid = 'pg_class'::regclass\n join pg_attribute a on a.attrelid = t.oid and a.attnum = d.refobjsubid\n join pg_namespace n on n.oid = v.relnamespace\n where n.nspname = $1\n and v.relkind in ('v', 'm')\n and d.refobjsubid > 0\n and v.oid <> t.oid\n and t.relkind in ('r', 'p', 'v', 'm')\n";
21
+ /**
22
+ * Turns dependency rows into edges that exist in `objects`.
23
+ *
24
+ * A table column becomes a column edge. A view that is itself a catalog
25
+ * object becomes an object edge. Anything else is omitted: the catalog
26
+ * refuses an edge whose target is missing.
27
+ *
28
+ * @param rows - Rows from {@link VIEW_DEPENDENCIES}
29
+ * @param namespace - Logical namespace stored on the edges
30
+ * @param objects - Catalog the edges must point into
31
+ * @returns Edges keyed by view name
32
+ */
33
+ export declare function viewDependencyEdges(rows: readonly Readonly<Record<string, unknown>>[], namespace: Namespace, objects: readonly CatalogObject[]): Map<string, ObjectIdentity[]>;
34
+ /** Logical public namespace. Tests and the default schema use it. */
35
+ export declare const PUBLIC: Namespace;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * `okmodel/view`.
3
+ *
4
+ * `view()` and `materializedView()` return objects that produce their own
5
+ * catalog records. `schema()` only asks each declared object for those records.
6
+ * DDL, diffing, and scratch verification stay out of this module's callers
7
+ * until a plan or a check loads them.
8
+ */
9
+ import type { CatalogObject } from "../../../contracts/catalog/types.js";
10
+ import type { SchemaHook } from "../model.js";
11
+ import type { SqlText } from "../table.js";
12
+ import { type ViewInstall } from "./client.js";
13
+ /** One declared output column. `type` is the Postgres type name. */
14
+ export type ViewColumnInput = {
15
+ readonly name: string;
16
+ readonly type: string;
17
+ };
18
+ /** Options for {@link view}. */
19
+ export type ViewOptions = {
20
+ readonly columns: readonly ViewColumnInput[];
21
+ readonly query: SqlText | string;
22
+ /** Schema. The default is `public`. */
23
+ readonly schema?: string;
24
+ /**
25
+ * Opts the view out of tenancy.
26
+ *
27
+ * A view that reads a tenant table and does not expose the tenant key is
28
+ * OKM1820 unless this is `global("reason")`.
29
+ */
30
+ readonly tenancy?: unknown;
31
+ };
32
+ /**
33
+ * One declared view.
34
+ *
35
+ * `contribute` builds the catalog record. Column dependencies are filled
36
+ * from `pg_depend` by scratch verification, not at this call.
37
+ */
38
+ export type ViewDeclaration = {
39
+ readonly name: string;
40
+ readonly schema: string;
41
+ /**
42
+ * Catalog record for this declaration.
43
+ *
44
+ * @param peers - The `schema({ views })` list
45
+ * @param built - Objects the schema has already staged
46
+ * @returns The view record
47
+ */
48
+ contribute(peers: unknown, built: unknown): readonly CatalogObject[];
49
+ /** Publishes the read model. `schema()` calls it. */
50
+ install: ViewInstall;
51
+ /** Moves the read handle under `db.views`. */
52
+ hook: SchemaHook;
53
+ };
54
+ /** One index owned by a materialized view. */
55
+ export type MaterializedViewIndexInput = {
56
+ readonly name?: string;
57
+ readonly columns: readonly string[];
58
+ readonly unique?: boolean;
59
+ };
60
+ /** Options for {@link materializedView}. */
61
+ export type MaterializedViewOptions = ViewOptions & {
62
+ readonly indexes?: readonly MaterializedViewIndexInput[];
63
+ /**
64
+ * Populate mode.
65
+ *
66
+ * `"concurrently"` plans `REFRESH MATERIALIZED VIEW CONCURRENTLY` and
67
+ * requires a unique index (OKM1822). Omitted still populates, without
68
+ * `CONCURRENTLY`.
69
+ */
70
+ readonly refresh?: "concurrently";
71
+ };
72
+ /**
73
+ * One declared materialized view.
74
+ *
75
+ * Identity is `(namespace, name)`. Indexes are separate catalog objects.
76
+ */
77
+ export type MaterializedViewDeclaration = {
78
+ readonly name: string;
79
+ readonly schema: string;
80
+ /**
81
+ * Catalog records for this declaration.
82
+ *
83
+ * @param peers - The `schema({ views })` list
84
+ * @param built - Objects the schema has already staged
85
+ * @returns The materialized view and its indexes
86
+ */
87
+ contribute(peers: unknown, built: unknown): readonly CatalogObject[];
88
+ /** Publishes the read model. `schema()` calls it. */
89
+ install: ViewInstall;
90
+ /** Moves the read handle under `db.views`. */
91
+ hook: SchemaHook;
92
+ };
93
+ /**
94
+ * Declares a view as SQL with output columns.
95
+ *
96
+ * The query is stored as written. A scratch database reads `pg_depend` and
97
+ * `pg_get_viewdef` before a plan treats the body as canonical.
98
+ *
99
+ * @param name - View name
100
+ * @param options - Columns and query
101
+ * @returns The definition object `schema({ views })` stores
102
+ */
103
+ export declare function view(name: string, options: ViewOptions): ViewDeclaration;
104
+ /**
105
+ * Declares a materialized view.
106
+ *
107
+ * Creation is `WITH NO DATA`. Populate is a later plan step. `refresh:
108
+ * "concurrently"` without a unique index is OKM1822.
109
+ *
110
+ * @param name - Materialized view name
111
+ * @param options - Columns, query, indexes, and refresh mode
112
+ * @returns The definition object `schema({ views })` stores
113
+ */
114
+ export declare function materializedView(name: string, options: MaterializedViewOptions): MaterializedViewDeclaration;
@@ -0,0 +1,289 @@
1
+ import {
2
+ viewObject,
3
+ materializedViewObject,
4
+ materializedViewIndex
5
+ } from "../../../shared/y005b761.js";
6
+ import {
7
+ encodeUuid,
8
+ decodeUuid
9
+ } from "../../../shared/dyj86cmj.js";
10
+ import {
11
+ snakeCase
12
+ } from "../../../shared/6w5j844a.js";
13
+ import {
14
+ encodeText,
15
+ decodeText
16
+ } from "../../../shared/4m9fn52z.js";
17
+ import {
18
+ staticNamespace2
19
+ } from "../../../shared/t4tbsy9h.js";
20
+ import {
21
+ OkmError2
22
+ } from "../../../shared/52szzakm.js";
23
+
24
+ // src/dialects/pg/view/client.ts
25
+ function viewClient(name, columns, query, tenancy) {
26
+ const field = camelCase(name);
27
+ const marked = readGlobal(name, tenancy);
28
+ return {
29
+ field,
30
+ install(model, scope, tables, casing, scoped) {
31
+ if (Object.hasOwn(model, field)) {
32
+ throw new OkmError2("OKM1023", `View ${name} uses the client name ${field}.`, {
33
+ fix: { summary: "Give the view a name that no table uses." }
34
+ });
35
+ }
36
+ const exposes = exposesKey(columns, scope);
37
+ if (scope !== undefined && readsTenant(query, tables, casing, scoped)) {
38
+ if (!exposes && marked === undefined) {
39
+ throw new OkmError2("OKM1820", `View ${name} reads a tenant table and does not expose ${scope.key}.`, {
40
+ fix: {
41
+ summary: "Expose the tenant key so the predicate can be applied, or declare global(reason)."
42
+ }
43
+ });
44
+ }
45
+ if (exposes) {
46
+ scope.scopeView(field);
47
+ scoped.add(name);
48
+ }
49
+ }
50
+ model[field] = {
51
+ name: field,
52
+ sql: name,
53
+ primary: [],
54
+ uniques: [],
55
+ columns: columnModels(columns, scope),
56
+ relations: []
57
+ };
58
+ },
59
+ hook(target, ctx) {
60
+ publish(field, target, ctx);
61
+ }
62
+ };
63
+ }
64
+ function publish(field, target, ctx) {
65
+ if (ctx.tables === undefined || ctx.table !== undefined)
66
+ return;
67
+ const handle = ctx.tables[field];
68
+ if (typeof handle !== "object" || handle === null)
69
+ return;
70
+ const source = handle;
71
+ const views = target.views;
72
+ const map = typeof views === "object" && views !== null ? views : {};
73
+ map[field] = {
74
+ find: source.find,
75
+ one: source.one,
76
+ count: source.count,
77
+ exists: source.exists
78
+ };
79
+ target.views = map;
80
+ delete ctx.tables[field];
81
+ delete target[field];
82
+ }
83
+ function columnModels(columns, tenancy) {
84
+ const seen = new Set;
85
+ const models = [];
86
+ for (const column of columns) {
87
+ const field = fieldName(column.name, tenancy);
88
+ if (seen.has(field)) {
89
+ throw new OkmError2("OKM1020", `View column ${column.name} repeats ${field}.`, {
90
+ fix: { summary: "Declare each output column once." }
91
+ });
92
+ }
93
+ seen.add(field);
94
+ const uuid = column.type.toLowerCase() === "uuid";
95
+ const key = tenancy !== undefined && field === tenancy.key;
96
+ models.push({
97
+ field,
98
+ sql: column.name,
99
+ dataType: column.type,
100
+ encode: uuid ? (value) => encodeUuid(textValue(value)) : (value) => encodeText(textValue(value)),
101
+ decode: uuid ? decodeUuid : decodeText,
102
+ hidden: false,
103
+ sensitive: false,
104
+ guarded: key,
105
+ writable: false,
106
+ guardUpdate: false
107
+ });
108
+ }
109
+ return models;
110
+ }
111
+ function exposesKey(columns, tenancy) {
112
+ if (tenancy === undefined)
113
+ return false;
114
+ return columns.some((column) => fieldName(column.name, tenancy) === tenancy.key);
115
+ }
116
+ function readsTenant(query, tables, casing, scoped) {
117
+ for (const name of scoped) {
118
+ if (mentions(query, name))
119
+ return true;
120
+ }
121
+ for (const table of tables) {
122
+ const mark = table.options?.tenancy;
123
+ if (isGlobalMark(mark))
124
+ continue;
125
+ const sql = casing === "snake" ? snakeCase(table.name) : table.name;
126
+ if (mentions(query, sql) || mentions(query, table.name))
127
+ return true;
128
+ }
129
+ return false;
130
+ }
131
+ function readGlobal(name, tenancy) {
132
+ if (tenancy === undefined)
133
+ return;
134
+ if (!isGlobalMark(tenancy)) {
135
+ throw new OkmError2("OKM1020", `View ${name} tenancy must be global("reason").`, {
136
+ fix: { summary: 'Pass global("reason") from okmodel/tenancy, or omit tenancy.' }
137
+ });
138
+ }
139
+ return tenancy.reason;
140
+ }
141
+ function isGlobalMark(value) {
142
+ if (typeof value !== "object" || value === null)
143
+ return false;
144
+ const record = value;
145
+ return record.kind === "global" && typeof record.reason === "string" && record.reason.trim().length > 0;
146
+ }
147
+ function fieldName(column, tenancy) {
148
+ const field = camelCase(column);
149
+ if (tenancy !== undefined && (column === tenancy.key || field === tenancy.key))
150
+ return tenancy.key;
151
+ return field;
152
+ }
153
+ function camelCase(name) {
154
+ let out = "";
155
+ let upper = false;
156
+ for (const char of name) {
157
+ if (char === "_") {
158
+ upper = out.length > 0;
159
+ continue;
160
+ }
161
+ out += upper ? char.toUpperCase() : char;
162
+ upper = false;
163
+ }
164
+ return out;
165
+ }
166
+ function mentions(query, name) {
167
+ let pattern = '(^|[^A-Za-z0-9_])"?';
168
+ for (const char of name) {
169
+ if ("\\^$[]().*+?|{}".includes(char))
170
+ pattern += "\\";
171
+ pattern += char;
172
+ }
173
+ pattern += '"?(?![A-Za-z0-9_])';
174
+ return new RegExp(pattern).test(query);
175
+ }
176
+ function textValue(value) {
177
+ if (typeof value !== "string") {
178
+ throw new OkmError2("OKM1121", "A view column value must be text.", {
179
+ fix: { summary: "Pass a string." }
180
+ });
181
+ }
182
+ return value;
183
+ }
184
+
185
+ // src/dialects/pg/view/index.ts
186
+ function view(name, options) {
187
+ const schema = options.schema ?? "public";
188
+ const columns = options.columns.map((column) => ({ name: column.name, dataType: column.type }));
189
+ const query = bodyText(options.query);
190
+ const bound = viewClient(name, options.columns, query, options.tenancy);
191
+ return {
192
+ name,
193
+ schema,
194
+ install: bound.install,
195
+ hook: bound.hook,
196
+ contribute(peers, built) {
197
+ assertPeers(peers);
198
+ assertNoTable(built, name);
199
+ const provenance = { origin: "file", name };
200
+ return [
201
+ viewObject({
202
+ namespace: staticNamespace2(schema),
203
+ name,
204
+ columns,
205
+ query,
206
+ provenance
207
+ })
208
+ ];
209
+ }
210
+ };
211
+ }
212
+ function materializedView(name, options) {
213
+ const schema = options.schema ?? "public";
214
+ const columns = options.columns.map((column) => ({ name: column.name, dataType: column.type }));
215
+ const query = bodyText(options.query);
216
+ const bound = viewClient(name, options.columns, query, options.tenancy);
217
+ const indexes = options.indexes ?? [];
218
+ if (options.refresh === "concurrently" && !indexes.some((index) => index.unique === true)) {
219
+ throw new OkmError2("OKM1822", `Materialized view ${name} refreshes concurrently and has no unique index.`, { fix: { summary: "Add a unique index, or refresh without CONCURRENTLY." } });
220
+ }
221
+ const columnNames = new Set(columns.map((column) => column.name));
222
+ for (const index of indexes) {
223
+ for (const column of index.columns) {
224
+ if (!columnNames.has(column)) {
225
+ throw new OkmError2("OKM1020", `Index on ${name} names column ${column}, which the materialized view does not have.`, { fix: { summary: "Name a column of the materialized view." } });
226
+ }
227
+ }
228
+ }
229
+ return {
230
+ name,
231
+ schema,
232
+ install: bound.install,
233
+ hook: bound.hook,
234
+ contribute(peers, built) {
235
+ assertPeers(peers);
236
+ assertNoTable(built, name);
237
+ const namespace = staticNamespace2(schema);
238
+ const provenance = { origin: "file", name };
239
+ const parent = { namespace, name };
240
+ return [
241
+ materializedViewObject({
242
+ namespace,
243
+ name,
244
+ columns,
245
+ query,
246
+ provenance,
247
+ ...options.refresh === "concurrently" ? { refresh: "concurrently" } : {}
248
+ }),
249
+ ...indexes.map((index) => materializedViewIndex({
250
+ parent,
251
+ ...index.name !== undefined ? { name: index.name } : {},
252
+ columns: index.columns,
253
+ ...index.unique === true ? { unique: true } : {},
254
+ provenance
255
+ }))
256
+ ];
257
+ }
258
+ };
259
+ }
260
+ function bodyText(body) {
261
+ return typeof body === "string" ? body : body.text;
262
+ }
263
+ function assertPeers(peers) {
264
+ if (!Array.isArray(peers)) {
265
+ throw new OkmError2("OKM1020", "schema({ views }) must be a list.", {
266
+ fix: { summary: "Pass an array to schema({ views })." }
267
+ });
268
+ }
269
+ }
270
+ function assertNoTable(built, name) {
271
+ if (!Array.isArray(built))
272
+ return;
273
+ for (const object of built) {
274
+ if (!isRecord(object) || object.kind !== "table" || !isRecord(object.identity))
275
+ continue;
276
+ if (object.identity.name === name) {
277
+ throw new OkmError2("OKM1023", `View ${name} uses the name of a table.`, {
278
+ fix: { summary: "Give the view a name that no table uses." }
279
+ });
280
+ }
281
+ }
282
+ }
283
+ function isRecord(value) {
284
+ return typeof value === "object" && value !== null;
285
+ }
286
+ export {
287
+ materializedView,
288
+ view
289
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Verifies declared views on a scratch schema.
3
+ *
4
+ * Tables from the catalog are created there, then each view. `pg_get_viewdef`
5
+ * replaces the author query and `pg_depend` supplies column edges (D117).
6
+ * The scratch schema is dropped before this returns. Plans never use it.
7
+ */
8
+ import type { Catalog } from "../../../contracts/catalog/types.js";
9
+ import type { CatalogQuery } from "../introspect.js";
10
+ /**
11
+ * Reads dependencies and reprints for every view in `source`.
12
+ *
13
+ * The returned catalog matches what introspection prints for those views
14
+ * when the same query is applied. A declared column list that disagrees
15
+ * with the database is OKM1020.
16
+ *
17
+ * @param runner - Database connection. Statements share its session
18
+ * @param source - Catalog whose views are still author SQL
19
+ * @returns A catalog whose views carry the reprint and `pg_depend` edges
20
+ */
21
+ export declare function sealViews(runner: CatalogQuery, source: Catalog): Promise<Catalog>;
@@ -5,11 +5,11 @@
5
5
  * `where` without importing the client types. `runtime/types.ts` re-exports
6
6
  * the two names callers use.
7
7
  */
8
- import type { Between, Compare, Containment, Eq, HasAnyKey, HasKey, InList, Matches, Not, NotIn, Overlaps, Path, Pattern, RawPattern } from "./operators.js";
8
+ import type { Between, Compare, Containment, Eq, HasAnyKey, HasKey, InList, Matches, Trigram, Not, NotIn, Overlaps, Path, Pattern, RawPattern } from "./operators.js";
9
9
  import type { Range } from "./range.js";
10
10
  export type IsObject<V> = V extends object ? V extends readonly unknown[] ? false : null extends V ? false : true : false;
11
11
  /** Text patterns, including substring `contains`. `matches` is for tsvector, which is also `string`. */
12
- type TextOps = Pattern<"startsWith"> | Pattern<"contains"> | Pattern<"endsWith"> | RawPattern<"like"> | RawPattern<"ilike"> | Matches;
12
+ type TextOps = Pattern<"startsWith"> | Pattern<"contains"> | Pattern<"endsWith"> | RawPattern<"like"> | RawPattern<"ilike"> | Matches | Trigram<"similar"> | Trigram<"wordSimilar">;
13
13
  /** Array, jsonb, or range containment, plus overlap where that operator fits. */
14
14
  type StructuredOps<V> = Containment<"contains" | "containedBy", V> | Overlaps<V>;
15
15
  /** A JSON fragment. Containment matches part of a document, not the whole column type. */