Declarative trigger guards in schema YAML (SQLite + PostgreSQL)#85
Open
MelbourneDeveloper wants to merge 5 commits into
Open
Declarative trigger guards in schema YAML (SQLite + PostgreSQL)#85MelbourneDeveloper wants to merge 5 commits into
MelbourneDeveloper wants to merge 5 commits into
Conversation
The default user-data-dir under the repo produced an IPC socket path over the macOS 103-char AF_UNIX limit (listen EINVAL), failing make ci locally. The test host now uses a short dir under the OS temp folder. The trigger guard tests raised Migration.Core coverage, ratcheting its threshold 86->87. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Adds declarative trigger guards to schema YAML — per-table
triggers:with LQLraiseWhenpredicates that become native BEFORE/AFTER row triggers on SQLite and PostgreSQL, diffed by name exactly like RLS policies. Closes #82.What Was Added?
TriggerDefinition(Name, Timing, Events, ForEachRow,raiseWhenLQL, ErrorMessage) onTableDefinition.Triggers, withTriggerTiming/TriggerEventenums and round-trippable YAML (defaultstiming: Before,forEachRow: trueomitted on write).SchemaDiff.Triggers— triggers missing from the database produceCreateTriggerOperation; triggers removed from YAML produceDropTriggerOperationonly underMigrationOptions.AllowDestructive(logged at Warning), mirroring RLS policy semantics.RlsPredicateTranspiler.TriggerGuard.cs): supportsold.<col>/new.<col>row references,and/orcomposition, and[not] exists(pipeline)subqueries. On Postgres, exists-pipeline SQL gets an identifier-quoting pass so mixed-case columns survive case folding. The internal__TRG_sentinel token is reserved and rejected in user predicates.usr_{event}_{trigger}_{table}usingSELECT RAISE(ABORT, '<message>') WHERE <predicate>; read back fromsqlite_masterand grouped by base name so re-diff is a no-op.{table}_{trigger}_trgfn+ triggerusr_{trigger}.RAISE EXCEPTION USING MESSAGEavoids%format interpretation; the dollar-quote tag is chosen to never occur in the body; identifiers over PostgreSQL's 63-byte limit fail loudly. The inspector reads onlyusr_-prefixed triggers, so unmanaged triggers (e.g. Sync change tracking) are never dropped by destructive runs, and*_trgfnguard functions are excluded from support-function read-back.forEachRow: falserejected (guards are row-level on every platform);old.references with an Insert event andnew.references with a Delete event rejected.SqliteTestDb/PostgresTestDbhelpers extracted from the RLS tests (moved, not copied).What Was Changed or Deleted?
SqliteRlsSchemaInspectorrefactored onto the new sharedSqliteTriggerNamesparsing (behaviour unchanged).SqliteRlsMigrationTests/PostgresRlsE2ETestsnow use the shared test-db helpers (pure moves; assertions unchanged).MigrationRunner.IsDestructivenow includesDropTriggerOperation.PostgresSupportSchemaInspectorexcludes%_trgfnfunctions from function read-back (they are owned by the trigger lifecycle)..gitignore: added.deslop-cache/.Lql/LqlExtension/src/test/runTest.ts: the VS Code test host now uses a short--user-data-dirunder the OS temp dir. The default dir lives under the repo, and on deep checkouts its IPC socket path exceeds the macOS 103-char AF_UNIX limit (listen EINVAL), makingmake cifail on macOS. No test behaviour or assertions changed.How Do The Automated Tests Prove It Works?
make ci(lint + test + build) is fully green locally, including all 533 Migration tests, Rust coverage 95.27% ≥ 95%, and the 70 VS Code extension tests. Highlights:SqliteDeclarativeTriggerTests(real temp-file SQLite): deleting/demoting the lastownerof a tenant fails with the declarederrorMessageand the row survives; succeeds when a second owner exists; guard is tenant-scoped; re-diff after apply yields zero operations; removing the trigger from YAML is a no-op withoutallowDestructiveand drops it with;old.-on-Insert,forEachRow: false, and reserved__TRG_predicates fail loudly.PostgresDeclarativeTriggerE2ETests(Testcontainers, real Postgres): same last-owner guard end-to-end;PgTriggerGuard_PascalCaseIdentifiers_EnforcedAtRuntimeproves mixed-case columns work at trigger-fire time;PgTrigger_DestructiveRerun_PreservesUnmanagedTriggerAndGuardFunctionproves unmanaged triggers survive destructive re-runs and the guard keeps enforcing.PostgresTriggerDdlTests: dollar-quote tag collision picks an alternate tag; trigger objects get theusr_managed prefix; identifiers over 63 bytes throw with a clear message.TriggerYamlSerializerTests: YAML round-trip preserves the declaration and omits defaults.Spec / Doc Changes
New
docs/specs/declarative-triggers-spec.mdwith hierarchical spec IDs ([MIG-TRIGGER-YAML],[MIG-TRIGGER-MODEL],[MIG-TRIGGER-DIFF],[MIG-TRIGGER-GUARD-LQL],[MIG-TRIGGER-SQLITE],[MIG-TRIGGER-PG]), referenced from the implementing code and tests.Follow-up filed as #84: the core LQL→PostgreSQL transpiler emits unquoted identifiers in pipeline SQL (latent in the RLS
exists()path); this PR works around it for trigger guards via a quoting pass.Breaking Changes
triggers:is a new optional key; existing schemas are unaffected.🤖 Generated with Claude Code