Repository navigation
Conversation
Schema middleware keeps its own map of prepared statement and portal intents. These tests show three ways it goes out of step with PostgreSQL: - A re-Parse of a DDL statement name that PostgreSQL rejects replaces the intent, so executing the retained DDL is not seen as DDL. - Close, DEALLOCATE and DISCARD ALL never release the intents. - DISCARD ALL drops the temporary table that shadowed an encrypted table, but the connection stays fail-closed for the next client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schema middleware kept prepared statement and portal intents in maps of its own, outside Context's rollback journal and lifecycle. A rejected re-Parse replaced the intent of the statement PostgreSQL kept, and Close, DEALLOCATE and DISCARD ALL never released it. Context now keeps the intent in the prepared statement's entry and carries it onto each portal bound from it, as it does a deallocation. The journal restores it with the rest of the statement, and closing or deallocating the statement releases it. Schema middleware classifies a statement and receives the intent when the portal executes. DISCARD ALL now also ends the fail-closed state left by a temporary table that shadowed an encrypted table. PostgreSQL refuses DISCARD ALL in a transaction block, so no rollback can bring the table back. DISCARD TEMP can be rolled back, so it keeps the connection fail-closed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cipherstash-agent
left a comment
There was a problem hiding this comment.
Automated review attestation
Source: Claude Code /code-review medium + company-skills:cross-check-review-findings, run against head 1a40c0e.
Result: no critical, blocking or new findings.
Checked against the head tree:
- Rollback of schema intent:
statement_snapshotis taken beforeprepare_schema_statement(lifecycle.rs:99 vs :153), andrestore_failed_preparationremoves the entry before restoringschema. Both a rejected re-Parse and a Proxy-refused Parse (validate_settingfailure) restore the prior intent. - Portal copy:
bind_statement_effectsruns on bothOkarms of the Bind handler (frontend.rs:281, :286), so every bound portal carries its statement's intent. Error arms refuse the Bind and need no intent. - Release: the intent lives in the prepared-statement and portal entries, so Close,
DEALLOCATEandDISCARD ALLrelease it with the entry. The deletedprepared/portalsmaps have no remaining callers. DISCARD ALL:local_unmodelledis set only by temporary-table paths (middleware.rs:840, :971), so clearing it on a confirmedDISCARD ALLmatches what PostgreSQL drops. PostgreSQL refuses it inside a transaction block, so no savepoint or rollback restore can resurrect the flag. The catalog-levelunmodelledflag is untouched (pinned bydiscard_all_keeps_unmodelled_catalog_ddl_fail_closed).- Existing feedback: none on the PR, so nothing to cross-reference.
Not run: the test suite (needs CipherStash credentials and a built workspace), so the claimed 519 passing lib tests are unverified here.
Nit, non-blocking: execute_schema_portal still clones the full SchemaIntent AST on every Execute, as the old portals lookup did. Passing Arc<SchemaIntent> through execute would avoid it in a later step of the refactor.
Generated by Claude Code
This branch has not been deployed
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.
Requested by Toby
Schema middleware kept its own map of prepared statements and portals, recording whether each one runs DDL. That map sat outside Context's rollback journal and outside the statement lifecycle, so it could get out of step with PostgreSQL.
This PR is the first step of the operation-ledger refactor from the architecture review. It fixes the three bugs that review found before the larger restructuring starts.
Bugs
All three are reproduced by failing tests in the first commit (3d934b7) and fixed in the second.
dasCREATE TABLE …and let PostgreSQL acknowledge it, then re-ParsedasSELECT 1. PostgreSQL rejects the re-Parse and keeps the DDL.d, but Schema middleware's map now saiddwas the SELECT.dran DDL without the injected Flush, without updating the transaction schema overlay, and without schema publication.DEALLOCATEandDISCARD ALLleft every intent in Schema middleware's map, each holding a full AST clone.ProtocolStateSizesdidn't count the map, so the bounded-state tests couldn't see it.DISCARD ALLdidn't end temporary-table shadowing.DISCARD ALLdrops that table, but the fail-closed state stayed set.DISCARD ALLis PgBouncer's default reset query, so the refusal carried over to every later client of that pooled connection.Fix
SchemaMiddleware::intent) and applies outcomes. The intent is stored in Context's prepared statement entry, andbind_statement_effectscopies it onto each portal bound from the statement, as it already does for a deallocation.MetadataSnapshotnow records the intent, so the rollback journal restores it with the rest of the statement. Close and deallocation release it along with everything else in the entry.preparedandportalsmaps are deleted.executenow takes the intent from Context instead of looking it up by portal name.DISCARD ALLclears the connection-local fail-closed state, because PostgreSQL refuses it inside a transaction block, so no rollback can restore the dropped tables.DISCARD TEMPstill keeps the connection fail-closed, because a full rollback can bring the temporary table back.This follows the
Prepared statementdefinition in CONTEXT.md and ADR-0001's "prepared DDL records intent with the prepared statement". The CONTEXT.md entries for Prepared statement and Schema middleware are updated to match.Tests
DISCARD ALLends temporary shadowing,DISCARD TEMPkeeps it, andDISCARD ALLkeeps catalog-level unmodelled DDL fail-closed.*_retains_nothingtests also count Schema middleware's maps, which made them fail. The fix deletes those maps, so the counters go too.cargo test -p cipherstash-proxy --libpasses (519 tests).cargo clippy --all-targets -D warningsandcargo fmt --checkare clean. I couldn't run the integration tests locally because they need CipherStash credentials.Acknowledgment
By submitting this pull request, I confirm that CipherStash can use, modify, copy, and redistribute this contribution, under the terms of CipherStash's choice.
Generated by Claude Code