From a869af5fe5c1ef497f37fc45a3ffbbe527237447 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Fri, 4 Sep 2026 06:31:00 +0300 Subject: [PATCH 1/2] docs: close Packet 7 against what actually merged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The delivery record was written at packet close, and fifteen commits landed after it from the pull request's own review rounds — including the sharpest defect of the packet. A record that stopped before them would be a false account, so it now carries a § What the pull-request review found: the shipped invariant that did not hold (PromoteDefault's order is not EF's, and this packet's PR description cited it as the migration's safety rationale), the canonical template that admitted an organization-scoped session to tenant-wide rows, three credential and transaction gaps, two architecture rules with escapes, three ADR edits made outside the rules that govern them, and two findings the measurement refuted. The measured total moves with it: 1208, counted from a run. Three architecture rules the review added were never registered — Every_Write_Port_Is_Countable_Or_Enumerated, which closes the census's blind spot for ports that do not derive; Out_Of_Band_Setters_Open_Read_Only_Transactions, which a behavioural test cannot express; and Registering_The_Pipeline_Twice_Registers_It_Once, which pins a property MediatR provides and the repository depends on. Database Standards now names ADR-0003 Amendment 4 in its Derives from header, because the template it carries is what that amendment corrects. ADR: 0003, 0042 Co-Authored-By: Claude Opus 5 (1M context) --- docs/roadmap/phase-02a-kernel-tenancy.md | 66 ++++++++++++++++++- docs/standards/05-database.md | 3 +- .../21-architecture-tests-catalogue.md | 60 +++++++++++++++++ 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/docs/roadmap/phase-02a-kernel-tenancy.md b/docs/roadmap/phase-02a-kernel-tenancy.md index ad8a1ff..16b13a7 100644 --- a/docs/roadmap/phase-02a-kernel-tenancy.md +++ b/docs/roadmap/phase-02a-kernel-tenancy.md @@ -2437,9 +2437,11 @@ one — and the second round repeatedly found the first round's fix. > **Packet 7 — Tenant and organization resolution, isolation, two tenants ✅** > -> **Measured at close: 1187 tests green** — 1 contract, 76 architecture, 802 unit, -> 308 integration. Counted from a run, not computed from a plan; an earlier commit -> message in this packet carried an arithmetic total that was eight short. +> **Measured at merge: 1208 tests green** — 1 contract, 79 architecture, 813 unit, +> 315 integration. Counted from a run, not computed from a plan; an earlier commit +> message in this packet carried an arithmetic total that was eight short. The number +> moved after this record was first written, because the pull request's own review found +> more — see § What the pull-request review found. ### What shipped @@ -2506,6 +2508,50 @@ one — and the second round repeatedly found the first round's fix. took a raw `Guid` tenant after the ADR said it and `ITenantContext.TenantId` "both move together". Amendment 4 records the conversion and the divergence. +### What the pull-request review found + +The record above was written at packet close. Fifteen commits landed after it, from three +review rounds on the pull request itself, and the sharpest defect of the packet was among +them — so the record would be a false account without them. + +- **A shipped invariant that did not hold.** Promoting a default locale through the + aggregate raised `23505` every time. `PromoteDefault` clears the incumbent and then sets + the target in memory, and EF does not preserve that order: same-table commands go out in + its comparer's order, and the composite key `(tenant_id, locale)` sorts `en-US` before + `tr-TR` — exactly the seeded pair. Nothing caught it because the cases covering that + index drive raw SQL in an order they choose, so they pin what PostgreSQL does with two + statements rather than what EF emits for one save. **This packet's own PR description + cited that invariant as the migration's safety rationale.** The store now saves in two + passes; a partial unique index permits zero defaults, so the intermediate state is one + the schema allows. +- **A canonical template that admitted the wrong writer.** Both `AS RESTRICTIVE` write + guards began with a bare `organization_id IS NULL` — the arm that lets a *tenant*-scope + session write rows belonging to no organization. It admitted an *organization*-scoped + session to them as well, so one organization could rewrite the tenant-wide fallback + every other organization reads. Measured. Corrected in the template, because every + organization-scoped table is told to copy it, and recorded as + [ADR-0003 Amendment 4](../decisions/0003-tenant-isolation-defense-in-depth.md). +- **Three credential and transaction gaps.** The host resolver announced a session + variable in a transaction four documents call read-only and that was not; the + platform-admin guard accepted `rolsuper`, which bypasses the GRANT matrix that bounds + the role; and an unparseable connection string was echoed through a redaction pattern + that could not cross a `/` or a second `@` inside a password. +- **Two architecture rules with escapes, and one blind test.** The effective-host rule + banned a header the code never reads instead of the one it does; the resolving-host rule + exempted its sole setter by filename suffix; and the host-reclaim case passed with the + hostname uniqueness dropped entirely, because nothing in it asked for a conflict. +- **Three ADR edits made outside the rules that govern them** — a stale statement rewritten + rather than amended, a normative paragraph added to an Accepted body, and an amendment + placed above the decision it amends. +- **Two findings refuted by measurement**, which is why they are worth recording: MediatR + already deduplicates pipeline behaviours, so a guard written for it was removed rather + than shipped; and the query filter's pinned `DbContext` is harmless because its + `CurrentTenantId` delegates to the process-wide accessor. + +**A hook now catches what CI caught three times.** Three commits on the branch tripped the +72-character subject limit, each found after a push — and a subject is only fixable by +rewriting history. `.githooks/commit-msg` enforces exactly what CI's `meta` job enforces. + ### What it did not ship, and who owns each - **Nothing is audited.** `AuditLogBehavior` lights up in Packet 9; `TransactionBehavior` @@ -2518,3 +2564,17 @@ one — and the second round repeatedly found the first round's fix. - **The host-resolution cache is invalidated before the commit, not after.** The guarantee is therefore the request *after* the write, not the one racing it; closing the rest needs a post-commit seam on `IUnitOfWork`, whose surface ADR-0040 governs. +- **Two idempotency limits do not bound memory,** and are recorded at the line rather than + fixed. The store's admission check reads a census refreshed once per sweep interval, so + within that window its caps admit every new key; and the filter buffers a response in + full before applying the 256 KiB cap. Both are Packet 4's, both fixes revisit an + ordering [ADR-0037](../decisions/0037-idempotency-key-contract.md) chose deliberately, + and neither is reachable through an endpoint that exists — every idempotent surface + today answers with an identifier, a receipt or a status. They belong with the durable + store, on that ADR's trigger. +- **Two migrations shipped in one pull request,** which + [Git Standards § Branching](../standards/14-git-workflow.md) says should not happen. + Splitting was attempted and measured infeasible: the migration commit carries the model + change it was generated from, and cherry-picking it onto `main` conflicts through the + marker and typed-identifier commits before it — a "migration PR" would have carried + roughly the first thirty commits of the packet. diff --git a/docs/standards/05-database.md b/docs/standards/05-database.md index 0a4dcff..a811404 100644 --- a/docs/standards/05-database.md +++ b/docs/standards/05-database.md @@ -5,7 +5,8 @@ (Amendments 1 + 2), [ADR-0003 Tenant Isolation Defense in Depth](../decisions/0003-tenant-isolation-defense-in-depth.md) (Amendment 1: Organization Scope; **Amendment 3: corrected RLS policy template and -database role model**), +database role model**; **Amendment 4: the write guards exclude an organization-scoped +session from tenant-wide rows**), [ADR-0006 Events and Outbox](../decisions/0006-events-and-outbox.md) (Amendment 1: Dapr pub/sub dispatch transport), [ADR-0038 Cross-Cutting Port and Event Contracts](../decisions/0038-cross-cutting-port-and-event-contracts.md), diff --git a/docs/standards/21-architecture-tests-catalogue.md b/docs/standards/21-architecture-tests-catalogue.md index 3ff5281..8f66f37 100644 --- a/docs/standards/21-architecture-tests-catalogue.md +++ b/docs/standards/21-architecture-tests-catalogue.md @@ -1082,6 +1082,66 @@ because the filters hold, and removing both turns all five red. - **Status:** **Implemented** (Packet 7, `LearnStack.Tests.Architecture`). - **Phase:** 02a Packet 7. +#### `Every_Write_Port_Is_Countable_Or_Enumerated` + +- **Asserts:** every interface in a production assembly whose method takes a type from a + module's `Domain` assembly either derives from `IAggregateWriteStore` — and + is therefore visible to the cross-aggregate census above — or appears on a literal + allow-list. The list holds one name: `IPlatformHostMappingStore`. +- **Why it exists.** The census counts derivations, so a port that does not derive is + invisible to it. One already is, deliberately: `PlatformHostMapping` is a projection + with a string key rather than an aggregate root. That exemption is fine; being *silent* + about it is not, because a second such port would join the first with nothing to notice, + and the census that keeps ADR-0042's exception at one entry would stop describing the + system. +- **Detected by shape, not by name.** "Takes a domain type" rather than "ends in `Store`": + a rule keyed on a suffix is satisfied by renaming. +- **Source:** [ADR-0042](../decisions/0042-tenant-provisioning-cross-aggregate-transaction.md). +- **Type:** xUnit + reflection. **Kind:** structural. +- **Status:** **Implemented** (Packet 7 review, `LearnStack.Tests.Architecture`, + `AggregateWriteTests`). +- **Phase:** 02a Packet 7. + +#### `Out_Of_Band_Setters_Open_Read_Only_Transactions` + +- **Asserts:** the two components that announce a session variable outside the ambient + unit of work — `CachedHostToTenantResolver` and `OrganizationScopeValidator` — each + issue `SET TRANSACTION READ ONLY`, and issue it **before** their first `set_config`. +- **Both halves matter.** PostgreSQL refuses `SET TRANSACTION` after the transaction's + first statement, so a setter that issued it late would fail at runtime rather than + quietly; and read-only is the property that makes an out-of-band setter acceptable at + all, because `learnstack_app` holds write grants on the tables these connections reach. +- **Why a scan and not a behavioural test.** The transaction is opened, used and disposed + inside one method, so nothing outside can observe its settings. Measured: the resolver + shipped without the statement while four carriers — Database Standards, Security + Standards, the glossary and ADR-0040 — described it as read-only, and the validator two + files away had carried it since Packet 6. +- **Source:** [ADR-0040](../decisions/0040-ambient-unit-of-work.md); + [05-database.md](05-database.md); [11-security.md](11-security.md). +- **Type:** xUnit + source scan. **Kind:** structural. +- **Status:** **Implemented** (Packet 7 review, `LearnStack.Tests.Architecture`, + `TenancyConventionTests`). +- **Phase:** 02a Packet 7. + +#### `Registering_The_Pipeline_Twice_Registers_It_Once` + +- **Asserts:** calling `AddLearnStackMediatRPipeline` twice on one `ServiceCollection` + yields the same registrations as calling it once — the same behaviour count and the same + total. +- **The property is MediatR's, not ours.** `AddBehavior` deduplicates; measured at seven + behaviours and eleven registrations either way. It is pinned because the repository + depends on it and did not write it: every test fixture registers its probe handler by + hand specifically to avoid re-running `AddMediatR`, and if deduplication stopped + holding, that workaround would become load-bearing rather than cautious with nothing to + say so. A doubled `TransactionBehavior` is a nested frame on every request. +- **A guard of our own was written and removed.** It changed nothing under mutation, and a + guard no test can kill is a comment. +- **Source:** [ADR-0032 § Sub-decision 2](../decisions/0032-exception-handling-logging-and-observability.md). +- **Type:** xUnit + DI registration inspection. **Kind:** structural. +- **Status:** **Implemented** (Packet 7 review, `LearnStack.Tests.Architecture`, + `CrossCuttingFoundationTests`). +- **Phase:** 02a Packet 7. + #### `Tenant_Context_Guard_Fires_Only_On_An_Unmarked_Transaction` - **Asserts:** both arms of the `DbCommandInterceptor` guard. A command a module `DbContext` issues on a transaction no sanctioned setter announced throws `TenantContextMissingException`; the same command on an announced transaction runs. One arm is not the rule: a guard keyed on `TransactionBehavior` instead of on the marker passes the first arm and rejects the writes the idempotency store and the audit store legitimately make on their own short transactions. From 57b0c583db815a68b95c98475718938369390d7c Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Fri, 4 Sep 2026 07:22:23 +0300 Subject: [PATCH 2/2] fix(decisions): renumber a colliding amendment, widen a rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0003 already had an Amendment 4 — the 2026-08-29 note that its Phase 02a table list had gone stale — and the write-guard amendment was appended with the same number. An erratum inside the same file says "Recorded in Amendment 4" and now pointed at two of them. It becomes Amendment 5, takes the file's own `##` heading shape, and every carrier that named the wrong number follows: Database Standards, the delivery record, the schema test and the migration's remarks. The Status header listed Amendments 1-3 only, so a reader following the authority metadata could not tell either Amendment 4 or 5 was part of the record. Both are named now. Every_Write_Port_Is_Countable_Or_Enumerated claimed to find interfaces taking a domain type and checked the parameter's own assembly, so IEnumerable answered System.Private.CoreLib and escaped — which is how a bulk write port is written. It unwraps generic arguments, array elements and by-ref transitively now, the same shape the IEventBus rule uses, and a probe bulk port is caught. Out_Of_Band_Setters_Open_Read_Only_Transactions is a source scan that orders SET TRANSACTION against the first set_config, and the catalogue described it as ordering against the first statement. PostgreSQL refuses SET TRANSACTION after any statement, so a setter that ran a SELECT in between would satisfy the rule and fail at runtime. The entry now says what the comparison measures and what it therefore does not catch — loudly, since that failure is immediate rather than silent, and parsing every command execution is the expensive alternative. ADR: 0003, 0042 Co-Authored-By: Claude Opus 5 (1M context) --- ...3213832_tenant_settings_org_write_guard.cs | 2 +- .../AggregateWriteTests.cs | 38 ++++++++++++++++++- .../Database/TenancySchemaTests.cs | 2 +- .../0003-tenant-isolation-defense-in-depth.md | 6 ++- docs/roadmap/phase-02a-kernel-tenancy.md | 2 +- docs/standards/05-database.md | 2 +- .../21-architecture-tests-catalogue.md | 31 +++++++++++---- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/backend/src/Modules/Tenancy/LearnStack.Modules.Tenancy.Infrastructure/Persistence/Migrations/20260903213832_tenant_settings_org_write_guard.cs b/backend/src/Modules/Tenancy/LearnStack.Modules.Tenancy.Infrastructure/Persistence/Migrations/20260903213832_tenant_settings_org_write_guard.cs index f8b7d26..dad0b5f 100644 --- a/backend/src/Modules/Tenancy/LearnStack.Modules.Tenancy.Infrastructure/Persistence/Migrations/20260903213832_tenant_settings_org_write_guard.cs +++ b/backend/src/Modules/Tenancy/LearnStack.Modules.Tenancy.Infrastructure/Persistence/Migrations/20260903213832_tenant_settings_org_write_guard.cs @@ -25,7 +25,7 @@ namespace LearnStack.Modules.Tenancy.Infrastructure.Persistence.Migrations /// /// Intra-tenant rather than cross-tenant — the tenant term is untouched, and no row /// crosses a tenant boundary — so this is a write-scope correction, not an isolation - /// fix. See ADR-0003 Amendment 4 and Database Standards § Tenant-Owned and + /// fix. See ADR-0003 Amendment 5 and Database Standards § Tenant-Owned and /// Organization-Scoped Tables, which carries the corrected template. /// /// diff --git a/backend/tests/LearnStack.Tests.Architecture/AggregateWriteTests.cs b/backend/tests/LearnStack.Tests.Architecture/AggregateWriteTests.cs index 543d722..349980d 100644 --- a/backend/tests/LearnStack.Tests.Architecture/AggregateWriteTests.cs +++ b/backend/tests/LearnStack.Tests.Architecture/AggregateWriteTests.cs @@ -80,7 +80,8 @@ public void Every_Write_Port_Is_Countable_Or_Enumerated() .Where(type => type.IsInterface) .Where(type => type.GetMethods().Any(method => method.GetParameters().Any(parameter => - domainAssemblies.Contains(parameter.ParameterType.Assembly)))) + Unwrap(parameter.ParameterType).Any(inner => + domainAssemblies.Contains(inner.Assembly))))) .Where(type => !WriteStoreConstructions(type).Any()) .Select(type => type.Name) .Distinct() @@ -163,4 +164,39 @@ private static IEnumerable ProductionAssemblies() => .Select(Path.GetFileNameWithoutExtension) .Where(name => !string.IsNullOrEmpty(name)) .Select(name => name!); + + /// A declared type and every type reachable through it. + /// + /// Through wrappers, because a bulk write port takes one: IEnumerable<Course> + /// lives in System.Private.CoreLib, so a check on the parameter's own assembly + /// sees the wrapper and not the domain type inside it — and the port escapes the + /// enumeration while satisfying every word of what this rule claims to detect. + /// Transitive, and through arrays and by-ref, because those nest too: + /// Course[] and in Course are the same port with different syntax. + /// + private static IEnumerable Unwrap(Type declared) + { + yield return declared; + + if (declared.HasElementType && declared.GetElementType() is { } element) + { + foreach (var inner in Unwrap(element)) + { + yield return inner; + } + } + + if (!declared.IsGenericType || declared.IsGenericTypeDefinition) + { + yield break; + } + + foreach (var argument in declared.GetGenericArguments()) + { + foreach (var inner in Unwrap(argument)) + { + yield return inner; + } + } + } } diff --git a/backend/tests/LearnStack.Tests.Integration/Database/TenancySchemaTests.cs b/backend/tests/LearnStack.Tests.Integration/Database/TenancySchemaTests.cs index 7850e13..fd476dc 100644 --- a/backend/tests/LearnStack.Tests.Integration/Database/TenancySchemaTests.cs +++ b/backend/tests/LearnStack.Tests.Integration/Database/TenancySchemaTests.cs @@ -569,7 +569,7 @@ public async Task An_Organization_Scoped_Session_Cannot_Write_A_Tenant_Wide_Row( // // Both AS RESTRICTIVE guards used a bare `organization_id IS NULL` first arm, // which exists so a TENANT-scope session can write those rows — and admitted an - // org-scoped one to them as well. Measured before ADR-0003 Amendment 4: a session + // org-scoped one to them as well. Measured before ADR-0003 Amendment 5: a session // announcing tenant A and organization A1 rewrote tenant A's tenant-wide row. // // The refusal is silent by construction: a RESTRICTIVE USING clause on UPDATE diff --git a/docs/decisions/0003-tenant-isolation-defense-in-depth.md b/docs/decisions/0003-tenant-isolation-defense-in-depth.md index 480a2df..103b010 100644 --- a/docs/decisions/0003-tenant-isolation-defense-in-depth.md +++ b/docs/decisions/0003-tenant-isolation-defense-in-depth.md @@ -4,7 +4,9 @@ Accepted (Amendment 1: 2026-05-18 — adds Organization scope; Amendment 2: 2026-05-19 — identity row terminology; **Amendment 3: 2026-08-08 — corrects the RLS policy template -and adds the database role model**; see bottom of document) +and adds the database role model**; Amendment 4: 2026-08-29 — the Phase 02a table list +has gone stale; **Amendment 5: 2026-09-04 — the write guards exclude an +organization-scoped session from tenant-wide rows**; see bottom of document) ## Decision @@ -301,7 +303,7 @@ The single authority for which class a table belongs to is into three documents drifts in three directions, and this copy already had — which is why the assignment lives in one file and this section keeps only the *rule*. -### Amendment 4 (2026-09-04): the write guards admitted an organization-scoped session to tenant-wide rows +## Amendment 5 — The write guards admitted an organization-scoped session to tenant-wide rows (2026-09-04) **What was wrong.** Amendment 3's template closes the `USING`-only write paths with two `AS RESTRICTIVE` guards, one `FOR UPDATE` and one `FOR DELETE`. Both read: diff --git a/docs/roadmap/phase-02a-kernel-tenancy.md b/docs/roadmap/phase-02a-kernel-tenancy.md index 16b13a7..b706db7 100644 --- a/docs/roadmap/phase-02a-kernel-tenancy.md +++ b/docs/roadmap/phase-02a-kernel-tenancy.md @@ -2530,7 +2530,7 @@ them — so the record would be a false account without them. session to them as well, so one organization could rewrite the tenant-wide fallback every other organization reads. Measured. Corrected in the template, because every organization-scoped table is told to copy it, and recorded as - [ADR-0003 Amendment 4](../decisions/0003-tenant-isolation-defense-in-depth.md). + [ADR-0003 Amendment 5](../decisions/0003-tenant-isolation-defense-in-depth.md). - **Three credential and transaction gaps.** The host resolver announced a session variable in a transaction four documents call read-only and that was not; the platform-admin guard accepted `rolsuper`, which bypasses the GRANT matrix that bounds diff --git a/docs/standards/05-database.md b/docs/standards/05-database.md index a811404..4c51bbb 100644 --- a/docs/standards/05-database.md +++ b/docs/standards/05-database.md @@ -5,7 +5,7 @@ (Amendments 1 + 2), [ADR-0003 Tenant Isolation Defense in Depth](../decisions/0003-tenant-isolation-defense-in-depth.md) (Amendment 1: Organization Scope; **Amendment 3: corrected RLS policy template and -database role model**; **Amendment 4: the write guards exclude an organization-scoped +database role model**; **Amendment 5: the write guards exclude an organization-scoped session from tenant-wide rows**), [ADR-0006 Events and Outbox](../decisions/0006-events-and-outbox.md) (Amendment 1: Dapr pub/sub dispatch transport), diff --git a/docs/standards/21-architecture-tests-catalogue.md b/docs/standards/21-architecture-tests-catalogue.md index 8f66f37..7e92c1f 100644 --- a/docs/standards/21-architecture-tests-catalogue.md +++ b/docs/standards/21-architecture-tests-catalogue.md @@ -1085,9 +1085,14 @@ because the filters hold, and removing both turns all five red. #### `Every_Write_Port_Is_Countable_Or_Enumerated` - **Asserts:** every interface in a production assembly whose method takes a type from a - module's `Domain` assembly either derives from `IAggregateWriteStore` — and - is therefore visible to the cross-aggregate census above — or appears on a literal - allow-list. The list holds one name: `IPlatformHostMappingStore`. + module's `Domain` assembly — **directly, or inside a generic, array or by-ref wrapper** + — either derives from `IAggregateWriteStore`, and is therefore visible to + the cross-aggregate census above, or appears on a literal allow-list. The list holds one + name: `IPlatformHostMappingStore`. +- **Wrappers are unwrapped transitively,** because a bulk write port is written with one: + `IEnumerable` lives in `System.Private.CoreLib`, so a check on the parameter's + own assembly sees the wrapper rather than the domain type inside it — and the port would + escape the enumeration while satisfying every word of what this rule claims. - **Why it exists.** The census counts derivations, so a port that does not derive is invisible to it. One already is, deliberately: `PlatformHostMapping` is a projection with a string key rather than an aggregate root. That exemption is fine; being *silent* @@ -1106,11 +1111,21 @@ because the filters hold, and removing both turns all five red. - **Asserts:** the two components that announce a session variable outside the ambient unit of work — `CachedHostToTenantResolver` and `OrganizationScopeValidator` — each - issue `SET TRANSACTION READ ONLY`, and issue it **before** their first `set_config`. -- **Both halves matter.** PostgreSQL refuses `SET TRANSACTION` after the transaction's - first statement, so a setter that issued it late would fail at runtime rather than - quietly; and read-only is the property that makes an out-of-band setter acceptable at - all, because `learnstack_app` holds write grants on the tables these connections reach. + contain `SET TRANSACTION READ ONLY`, and contain it **at a lower source offset than + their first `set_config(`**. +- **What the offset comparison does and does not prove.** PostgreSQL refuses + `SET TRANSACTION` after the transaction's *first statement of any kind*, and this scan + only orders it against the announcement. A setter that ran some other statement — a + `SELECT`, a second `SET` — between `BEGIN` and `SET TRANSACTION READ ONLY` would satisfy + the rule and fail at runtime. That failure is loud and immediate rather than silent, + which is why the cheap ordering check is the one that ships; the expensive alternative + is parsing the method for every command execution, and + [§ What a structural test proves](#what-a-structural-test-proves--and-what-it-does-not) + states the general limit. +- **Why the property matters at all.** Read-only is what makes an out-of-band setter of a + session variable acceptable, because `learnstack_app` holds write grants on the tables + these connections reach — so nothing but this statement stops a future edit from writing + under an announcement no request made. - **Why a scan and not a behavioural test.** The transaction is opened, used and disposed inside one method, so nothing outside can observe its settings. Measured: the resolver shipped without the statement while four carriers — Database Standards, Security