Context
NAP (NimblesiteAgenticPlatform) has completed its `[AUTHZ-ONE-ROLE]` migration: every endpoint now runs as `app_user` under RLS policies declared in `migrations/schema.yaml`. One guard could NOT be expressed declaratively:
Last-owner protection on `tenant_members` — "refuse to demote/delete the only `owner` of a tenant". The RLS layer can restrict owner-row writes to owners (`tenant_members_writer_update/_delete` policies), but a policy predicate cannot count remaining rows after the write, and LQL has no negated-exists / aggregate predicates. The natural mechanism is a BEFORE UPDATE/DELETE trigger that raises when the target row is the last owner.
Today the guard lives only in the application route layer (`assert_not_last_owner`), which means raw SQL under `app_user` could still strand a tenant ownerless.
Ask
Declarative trigger support in schema.yaml, e.g.:
```yaml
tables:
- name: tenant_members
triggers:
- name: assert_not_last_owner
timing: Before
events: [Update, Delete]
forEachRow: true
bodyLql: |
raise when OLD.role = 'owner' and no other owner row exists for OLD.tenant_id
```
Notes:
- The RLS spec already emulates SQLite RLS with triggers internally, so the DDL machinery partially exists; this asks for user-declared triggers as first-class schema objects (diffed, inspected, dropped destructively like policies).
- A smaller alternative that would also unblock this case: support for `not exists(...)` / negation and row-count comparisons in LQL policy predicates, so the guard could be an RLS predicate instead of a trigger.
Per NAP's hard rule ("DataProvider owns the whole schema surface; if it can't express something, file an issue — never work around it") we have NOT hand-written any SQL; the guard stays app-side until this lands.
Context
NAP (NimblesiteAgenticPlatform) has completed its `[AUTHZ-ONE-ROLE]` migration: every endpoint now runs as `app_user` under RLS policies declared in `migrations/schema.yaml`. One guard could NOT be expressed declaratively:
Last-owner protection on `tenant_members` — "refuse to demote/delete the only `owner` of a tenant". The RLS layer can restrict owner-row writes to owners (`tenant_members_writer_update/_delete` policies), but a policy predicate cannot count remaining rows after the write, and LQL has no negated-exists / aggregate predicates. The natural mechanism is a BEFORE UPDATE/DELETE trigger that raises when the target row is the last owner.
Today the guard lives only in the application route layer (`assert_not_last_owner`), which means raw SQL under `app_user` could still strand a tenant ownerless.
Ask
Declarative trigger support in schema.yaml, e.g.:
```yaml
tables:
triggers:
timing: Before
events: [Update, Delete]
forEachRow: true
bodyLql: |
raise when OLD.role = 'owner' and no other owner row exists for OLD.tenant_id
```
Notes:
Per NAP's hard rule ("DataProvider owns the whole schema surface; if it can't express something, file an issue — never work around it") we have NOT hand-written any SQL; the guard stays app-side until this lands.