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.
- package/README.md +710 -36
- package/dist/contracts/catalog/enum.d.ts +31 -4
- package/dist/contracts/catalog/extension.d.ts +61 -0
- package/dist/contracts/catalog/privilege.d.ts +64 -0
- package/dist/contracts/catalog/routine.d.ts +56 -0
- package/dist/contracts/catalog/types.d.ts +164 -10
- package/dist/contracts/catalog/view.d.ts +73 -0
- package/dist/dialects/pg/column.d.ts +3 -1
- package/dist/dialects/pg/ddl.d.ts +65 -1
- package/dist/dialects/pg/enum.d.ts +10 -8
- package/dist/dialects/pg/ext/citext.d.ts +14 -0
- package/dist/dialects/pg/ext/citext.js +11 -0
- package/dist/dialects/pg/ext/index.d.ts +52 -0
- package/dist/dialects/pg/ext/index.js +6 -0
- package/dist/dialects/pg/ext/pg-trgm.d.ts +53 -0
- package/dist/dialects/pg/ext/pg-trgm.js +42 -0
- package/dist/dialects/pg/fn/index.d.ts +107 -0
- package/dist/dialects/pg/fn/index.js +175 -0
- package/dist/dialects/pg/fn/touch.d.ts +14 -0
- package/dist/dialects/pg/index.js +104 -19
- package/dist/dialects/pg/introspect.d.ts +10 -1
- package/dist/dialects/pg/operators.d.ts +8 -1
- package/dist/dialects/pg/role/check.d.ts +45 -0
- package/dist/dialects/pg/role/index.d.ts +50 -0
- package/dist/dialects/pg/role/sql.d.ts +44 -0
- package/dist/dialects/pg/table.d.ts +7 -0
- package/dist/dialects/pg/tenancy.d.ts +9 -0
- package/dist/dialects/pg/view/client.d.ts +30 -0
- package/dist/dialects/pg/view/depend.d.ts +35 -0
- package/dist/dialects/pg/view/index.d.ts +114 -0
- package/dist/dialects/pg/view/index.js +289 -0
- package/dist/dialects/pg/view/scratch.d.ts +21 -0
- package/dist/dialects/pg/where.d.ts +2 -2
- package/dist/runtime/pg/bun.js +3 -3
- package/dist/runtime/pg/pg.js +3 -3
- package/dist/runtime/pg/pglite.js +3 -3
- package/dist/runtime/pg/postgresjs.js +3 -3
- package/dist/runtime/tenancy/index.js +6 -3
- package/dist/runtime/traits/index.d.ts +7 -2
- package/dist/runtime/traits/index.js +149 -12
- package/dist/runtime/validate/index.js +4 -4
- package/dist/shared/{ytg3xf9p.js → 01yr20s5.js} +3 -3
- package/dist/shared/{cstgkzdx.js → 0jc3ffzf.js} +1 -1
- package/dist/shared/{xkz8g6by.js → 1g0drakv.js} +2 -2
- package/dist/shared/{eaxwrgby.js → 1tfdfvhh.js} +4 -3
- package/dist/shared/2ssqrv66.js +1576 -0
- package/dist/shared/{10tjenmd.js → 3ekj9x92.js} +1 -1
- package/dist/shared/49ypnfwt.js +1122 -0
- package/dist/shared/{sacqha1y.js → 4m9fn52z.js} +2 -2
- package/dist/shared/5nmdenb6.js +1229 -0
- package/dist/shared/{qa5dsc7z.js → 6bazvqwm.js} +3 -3
- package/dist/shared/{x9v1qgeb.js → 6w5j844a.js} +1 -1
- package/dist/shared/{0qkxbm1f.js → 75xkjv0t.js} +1 -1
- package/dist/shared/7fvtrkwr.js +84 -0
- package/dist/shared/94ez8kzh.js +19 -0
- package/dist/shared/{1dp5knt7.js → 999ade2q.js} +2 -2
- package/dist/shared/{3a5ne750.js → 9m80mkac.js} +2 -2
- package/dist/shared/9x25fxhc.js +150 -0
- package/dist/shared/{4b9da9d7.js → axhk06q1.js} +19 -1
- package/dist/shared/{38crxbgn.js → bqha5cg6.js} +3 -3
- package/dist/shared/{zd93g3rq.js → bryqk9k9.js} +1 -1
- package/dist/shared/{n47rd821.js → c8aghh05.js} +1 -1
- package/dist/shared/{dd4f8mnx.js → c9z5g0fm.js} +4 -2
- package/dist/shared/{eqmtn1bj.js → dbjej1tw.js} +71 -14
- package/dist/shared/{ps43134h.js → dyj86cmj.js} +4 -4
- package/dist/shared/{sfh4pk75.js → fptpmvxp.js} +3 -3
- package/dist/shared/{97g2jnrw.js → fs0ex00p.js} +3 -3
- package/dist/shared/{rtnaqre0.js → g4f5kcx9.js} +2 -2
- package/dist/shared/{yw5c610b.js → gjthds9j.js} +2 -2
- package/dist/shared/hcmdtwgh.js +102 -0
- package/dist/shared/{qxa0yb9j.js → jkc6ws1p.js} +2 -54
- package/dist/shared/{mxmcdsh5.js → k34e0xf2.js} +9 -9
- package/dist/shared/{py3nh7jy.js → kgnc8grt.js} +2 -2
- package/dist/shared/{g9f0zqce.js → m78s7898.js} +1 -1
- package/dist/shared/meck53jv.js +99 -0
- package/dist/shared/{v18jva5b.js → mt5wswjz.js} +1 -1
- package/dist/shared/{g1b5zzj9.js → n0dgs0pb.js} +1 -1
- package/dist/shared/{zt8y0wzb.js → n7mcc1gk.js} +1 -1
- package/dist/shared/ne02ng5c.js +68 -0
- package/dist/shared/{e7a268fy.js → p24x2334.js} +1 -1
- package/dist/shared/qbxzvwwc.js +88 -0
- package/dist/shared/{a1gdp7j9.js → qybh48nd.js} +1 -1
- package/dist/shared/{d04j53v8.js → scwxrpx2.js} +5 -4
- package/dist/shared/{d2whjz4k.js → t616ynfn.js} +530 -24
- package/dist/shared/th14ygv2.js +69 -0
- package/dist/shared/{xatfg0gn.js → tmbdywx0.js} +4 -19
- package/dist/shared/{ge03bfca.js → ty3v3hfk.js} +2 -2
- package/dist/shared/wekze34d.js +246 -0
- package/dist/shared/{7mht49yd.js → x6wysh83.js} +59 -18
- package/dist/shared/{4jzzghr6.js → xva8v7qk.js} +8 -8
- package/dist/shared/{ecdzej21.js → xw1d69vd.js} +7 -7
- package/dist/shared/y005b761.js +112 -0
- package/dist/shared/{mrq77sgw.js → ydaz1sv3.js} +1 -1
- package/dist/tooling/cli.js +32 -8
- package/dist/tooling/migrate/apply.d.ts +16 -0
- package/dist/tooling/migrate/commands.d.ts +2 -1
- package/dist/tooling/migrate/config.d.ts +8 -0
- package/dist/tooling/migrate/doctor.d.ts +25 -0
- package/dist/tooling/migrate/drift.d.ts +19 -0
- package/dist/tooling/migrate/ext.d.ts +17 -0
- package/dist/tooling/migrate/extensions.d.ts +51 -0
- package/dist/tooling/migrate/managed.d.ts +17 -0
- package/dist/tooling/migrate/plan.d.ts +14 -1
- package/dist/tooling/migrate/project.d.ts +25 -1
- package/dist/tooling/migrate/type-name.d.ts +19 -0
- package/package.json +22 -2
- package/dist/shared/5z4q2zda.js +0 -2073
- /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. */
|