Skip to content

Commit 5f7bb3b

Browse files
authored
Merge pull request #195 from constructive-io/feat/view-docs-id-based-ownership
docs(data-modeling): correct view `data` to ID-based body + document ownership/AST validation
2 parents 9836796 + e328f75 commit 5f7bb3b

28 files changed

Lines changed: 57 additions & 26 deletions
-22.3 KB
Binary file not shown.
-9.93 KB
Binary file not shown.
-15.3 KB
Binary file not shown.
-18.8 KB
Binary file not shown.
-26.9 KB
Binary file not shown.
-29 KB
Binary file not shown.
-19.6 KB
Binary file not shown.

.agents/skills/constructive-data-modeling/SKILL.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,12 @@ escape hatch, and combining predicates with expressions.
340340
## Views
341341

342342
Create views via `db.view.create`; `viewType` selects the view body (`View*` node
343-
type). Three optional options control PostgreSQL storage attributes and update
344-
semantics: `securityInvoker` (default `true`), `securityBarrier` (default
345-
`false`), and `checkOption` (`null | 'local' | 'cascaded'`).
343+
type). The body in `data` references its source by **catalog ID**
344+
(`source_table_id` / `field_ids`, etc.), never a raw schema/table name — the server
345+
resolves names and enforces same-database ownership + AST validation. Three optional
346+
options control PostgreSQL storage attributes and update semantics: `securityInvoker`
347+
(default `true`), `securityBarrier` (default `false`), and `checkOption`
348+
(`null | 'local' | 'cascaded'`).
346349

347350
```typescript
348351
await db.view.create({
@@ -352,7 +355,7 @@ await db.view.create({
352355
name: 'owners_view',
353356
viewType: 'ViewTableProjection',
354357
tableId: ownersTableId,
355-
data: { source_schema: 'app_public', source_table: 'owners' },
358+
data: { source_table_id: ownersTableId },
356359
securityBarrier: true,
357360
checkOption: 'cascaded',
358361
isReadOnly: false,
@@ -364,7 +367,8 @@ await db.view.create({
364367
// SELECT ... WITH CASCADED CHECK OPTION
365368
```
366369

367-
See [views.md](./references/views.md) for all view options and generated SQL.
370+
See [views.md](./references/views.md) for all view options, the ID-based body, and
371+
ownership/validation guarantees.
368372

369373
## `api_required` (Required API Fields)
370374

.agents/skills/constructive-data-modeling/references/views.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ each row compiles to a PostgreSQL `CREATE VIEW`. The view body is chosen by
77
for 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
1118
await 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.
-19.2 KB
Binary file not shown.

0 commit comments

Comments
 (0)