Skip to content

LQL: support x in (a, b, c) membership predicate inside filter() lambda body [NAP Tier 2] #48

@MelbourneDeveloper

Description

@MelbourneDeveloper

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions