DataProvider v0.9.8-beta closed #33 (declarative `CREATE FUNCTION`). The `body:` field on `functions[]` accepts raw SQL — that's the only place NAP still ships SQL through migration after the operator's "no loose SQL" mandate.
NAP request: allow LQL in `functions[].body` so the entire migration is portable. Today every NAP helper is forced to be SQL:
```yaml
functions:
- name: is_member
arguments: [{name: u, type: uuid}, {name: t, type: uuid}]
returns: boolean
securityDefiner: true
body: |
SELECT EXISTS (
SELECT 1 FROM public.tenant_members
WHERE user_id = u AND tenant_id = t
)
```
The same body in LQL (per the README's `exists(pipeline)` syntax used in policy predicates):
```yaml
functions:
- name: is_member
arguments: [{name: u, type: uuid}, {name: t, type: uuid}]
returns: boolean
securityDefiner: true
bodyLql: |
exists(
tenant_members
|> filter(fn(m) => m.user_id = u and m.tenant_id = t)
)
```
Acceptance criteria:
- New optional field (suggested name: `bodyLql:`) accepts LQL pipeline expressions plus simple `SELECT expr` shapes.
- Mutually exclusive with `body:`. Schema validator rejects both.
- Postgres transpilation emits the same SQL the existing LQL policy-predicate transpiler does (e.g. `SELECT EXISTS (SELECT 1 FROM public.tenant_members WHERE m.user_id = u AND m.tenant_id = t)`).
- SQLite transpilation deferred until SQLite RLS emulation needs it.
NAP Tier 1. Until this lands, NAP's `schema.yaml` ships 5 SECURITY DEFINER functions with raw SQL bodies — a violation of the operator's "every migration in YAML, no loose SQL" rule. `body:` (raw SQL) stays as the documented escape hatch for platform-specific stuff that LQL can't express, but the default path must be LQL.
Pairs with the GUC-reader gap: separate issue tracking the need for an LQL builtin to read arbitrary session settings (Postgres GUCs / SQLite context-table rows) so `app_tenant_id()` / `app_user_id()` can also be written in LQL.
DataProvider v0.9.8-beta closed #33 (declarative `CREATE FUNCTION`). The `body:` field on `functions[]` accepts raw SQL — that's the only place NAP still ships SQL through migration after the operator's "no loose SQL" mandate.
NAP request: allow LQL in `functions[].body` so the entire migration is portable. Today every NAP helper is forced to be SQL:
```yaml
functions:
arguments: [{name: u, type: uuid}, {name: t, type: uuid}]
returns: boolean
securityDefiner: true
body: |
SELECT EXISTS (
SELECT 1 FROM public.tenant_members
WHERE user_id = u AND tenant_id = t
)
```
The same body in LQL (per the README's `exists(pipeline)` syntax used in policy predicates):
```yaml
functions:
arguments: [{name: u, type: uuid}, {name: t, type: uuid}]
returns: boolean
securityDefiner: true
bodyLql: |
exists(
tenant_members
|> filter(fn(m) => m.user_id = u and m.tenant_id = t)
)
```
Acceptance criteria:
NAP Tier 1. Until this lands, NAP's `schema.yaml` ships 5 SECURITY DEFINER functions with raw SQL bodies — a violation of the operator's "every migration in YAML, no loose SQL" rule. `body:` (raw SQL) stays as the documented escape hatch for platform-specific stuff that LQL can't express, but the default path must be LQL.
Pairs with the GUC-reader gap: separate issue tracking the need for an LQL builtin to read arbitrary session settings (Postgres GUCs / SQLite context-table rows) so `app_tenant_id()` / `app_user_id()` can also be written in LQL.