@@ -7,6 +7,13 @@ each row compiles to a PostgreSQL `CREATE VIEW`. The view body is chosen by
77for the catalog: ` ViewTableProjection ` , ` ViewJoinedTables ` , ` ViewAggregated ` ,
88` ViewFilteredTable ` , ` ViewComposite ` ).
99
10+ The view body lives in the ` data ` object and is ** referenced by ID, never by raw
11+ schema/table name** . Depending on ` viewType ` , ` data ` carries ` source_table_id ` /
12+ ` primary_table_id ` / per-join ` table_id ` (all catalog table UUIDs) and optional
13+ ` field_ids ` . The server resolves each ID to the physical schema/table name for you
14+ and validates it (see [ Referential integrity & ownership] ( #referential-integrity--ownership ) ),
15+ so you never hand-write a schema name into a view.
16+
1017``` typescript
1118await db .view .create ({
1219 data: {
@@ -15,7 +22,9 @@ await db.view.create({
1522 name: ' active_projects' ,
1623 viewType: ' ViewTableProjection' ,
1724 tableId: projectsTableId ,
18- data: { source_schema: ' app_public' , source_table: ' projects' },
25+ // ID-based body: names are resolved from the catalog server-side.
26+ // field_ids is optional — omit it to SELECT * from the source table.
27+ data: { source_table_id: projectsTableId },
1928 isReadOnly: true ,
2029 },
2130 select: { id: true },
@@ -49,7 +58,7 @@ await db.view.create({
4958 name: ' owners_view' ,
5059 viewType: ' ViewTableProjection' ,
5160 tableId: ownersTableId ,
52- data: { source_schema: ' app_public ' , source_table: ' owners ' },
61+ data: { source_table_id: ownersTableId },
5362 securityInvoker: true ,
5463 securityBarrier: true ,
5564 checkOption: ' cascaded' ,
@@ -82,3 +91,28 @@ Set `checkOption: null` to drop the check option again.
8291
8392> Only ` null | 'local' | 'cascaded' ` are valid for ` checkOption ` . A check option
8493> is meaningful only on updatable views (` isReadOnly: false ` ).
94+
95+ ## Referential integrity & ownership
96+
97+ View bodies reference their source objects by ** catalog ID** (` source_table_id ` ,
98+ ` primary_table_id ` , per-join ` table_id ` , ` field_ids ` ) — never by a raw
99+ schema/table name you supply. This is what keeps views safe and tenant-scoped:
100+
101+ - ** Ownership is enforced.** Every referenced table ID is checked to belong to the
102+ same ` databaseId ` as the view; a table from another database is rejected
103+ (generates a ` CROSS_DATABASE_REF ` error). ` field_ids ` are likewise scoped to the
104+ view's database. You cannot point a view at another tenant's table by ID or by
105+ guessing its physical schema name.
106+ - ** Names are derived, not trusted.** The physical ` source_schema ` / ` source_table `
107+ that end up in the generated ` CREATE VIEW ` are looked up from the ID server-side,
108+ so a caller can't inject an arbitrary schema name.
109+ - ** The query AST is validated.** Before the view is created/altered, the compiled
110+ query is run through the same AST validator used by check constraints, indexes,
111+ and functions — it restricts every referenced schema to the view's own database
112+ schemas (plus framework schemas). This also covers the ` ViewComposite ` escape
113+ hatch (` data.query_ast ` ): an out-of-scope schema reference there fails validation
114+ rather than reaching PostgreSQL.
115+
116+ So ` db.view.create ` gives the same ownership and AST-validation guarantees as the
117+ other declarative DDL surfaces — referencing by ID is the mechanism that provides
118+ them, which is why the name-based shortcut isn't accepted.
0 commit comments