Problem
DP v0.9.9-beta successfully ships LQL function bodies (#43) and the current_setting builtin (#44). Excellent. NAP migrated 4 of 5 SECURITY DEFINER helpers to LQL on first try; the 5th (is_tenant_writer) hit a wall:
- schema: public
name: is_tenant_writer
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 and m.role in ('owner', 'admin'))
)
executeRoles: [app_user, app_admin]
DataProviderMigrate migrate fails with:
MIG-E-RLS-LQL-PARSE: policy 'public.is_tenant_writer': Syntax error: Unsupported comparison type: ComparisonContext
The triggering construct is the IN (...) membership predicate inside filter() — this is a Postgres-native + SQL-standard idiom and the most readable way to express "role is one of {owner, admin}".
Workaround NAP shipped
Rewrote the predicate as an explicit or chain:
m.role = 'owner' or m.role = 'admin'
This works but doesn't scale — for any future role enum with N values it produces N-way OR chains. RLS policies on tenant-membership tables in larger systems will hit this fast.
Proposed shape
LQL expr in (literal, literal, ...) should desugar to the SQL emission expr IN (literal, literal, ...). Same parse path inside filter(fn(m) => ...), exists(... |> filter(...)), and any future predicate context.
Tests:
m.role in ('owner', 'admin') → m.role IN ('owner', 'admin')
m.role in ('owner') → m.role IN ('owner') (single-element list)
m.role in () → parse error (empty list disallowed)
m.role not in ('viewer') → m.role NOT IN ('viewer') (negated form, optional but cheap)
Type-check: the literals on the RHS must be coercible to the column type. m.role in (1, 2) for a text column = parse-time error.
NAP Tier 2
Not blocking — the or workaround compiles and tests pass. Filing so the gap is tracked and a future revisit drops the OR chain when this ships. Lower priority than #46 (still NAP's last raw-DDL escape).
Problem
DP v0.9.9-beta successfully ships LQL function bodies (#43) and the
current_settingbuiltin (#44). Excellent. NAP migrated 4 of 5 SECURITY DEFINER helpers to LQL on first try; the 5th (is_tenant_writer) hit a wall:DataProviderMigrate migratefails with:The triggering construct is the
IN (...)membership predicate insidefilter()— this is a Postgres-native + SQL-standard idiom and the most readable way to express "role is one of {owner, admin}".Workaround NAP shipped
Rewrote the predicate as an explicit
orchain:This works but doesn't scale — for any future role enum with N values it produces N-way OR chains. RLS policies on tenant-membership tables in larger systems will hit this fast.
Proposed shape
LQL
expr in (literal, literal, ...)should desugar to the SQL emissionexpr IN (literal, literal, ...). Same parse path insidefilter(fn(m) => ...),exists(... |> filter(...)), and any future predicate context.Tests:
m.role in ('owner', 'admin')→m.role IN ('owner', 'admin')m.role in ('owner')→m.role IN ('owner')(single-element list)m.role in ()→ parse error (empty list disallowed)m.role not in ('viewer')→m.role NOT IN ('viewer')(negated form, optional but cheap)Type-check: the literals on the RHS must be coercible to the column type.
m.role in (1, 2)for atextcolumn = parse-time error.NAP Tier 2
Not blocking — the
orworkaround compiles and tests pass. Filing so the gap is tracked and a future revisit drops the OR chain when this ships. Lower priority than #46 (still NAP's last raw-DDL escape).