Module: Hecks::Adapters::PostgresEra::Lineage::Provisioning
- Included in:
- Hecks::Adapters::PostgresEra::Lineage
- Defined in:
- lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb
Instance Method Summary collapse
-
#ensure_base! ⇒ Object
Provisioning is the OWNER's job, and a deployment's app role is deliberately not the owner — it may append and read, and it owns nothing.
-
#ensure_partition!(era) ⇒ Object
BUILD, THEN ATTACH — never CREATE ...
-
#partition_attached?(era) ⇒ Boolean
Attached, not merely present: a crash between the CREATE and the ATTACH leaves a table that is not yet part of the journal, and the next boot must finish the job rather than skip it.
-
#provisioner? ⇒ Boolean
Nothing provisioned yet — build it.
-
#rename_domain! ⇒ Object
A DOMAIN'S OWN IDENTITY CHANGED — bridge its history under the new name, before
provisioner?/the CREATE TABLE IF NOT EXISTS block below ever run.
Instance Method Details
#ensure_base! ⇒ Object
Provisioning is the OWNER's job, and a deployment's app role is deliberately not the owner — it may append and read, and it owns nothing. By the time such a role connects, the base is already built, so its boot verifies rather than builds.
Without this guard the per-era fence below is unreachable: the ALTER TABLE and REVOKE here are owner-only, so the very role grant_era! exists to constrain could never finish booting ("must be owner of table hecks_eras"). A fence nothing can reach is not a fence.
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
# File 'lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb', line 16 def ensure_base! rename_domain! if @formerly_known_as return unless provisioner? @db.exec(<<~SQL) CREATE TABLE IF NOT EXISTS hecks_eras ( domain text NOT NULL, ordinal int NOT NULL, hash text, label text, held_text text NOT NULL, watermark bigint, PRIMARY KEY (domain, ordinal) ) SQL @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text") @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb") # which canonical-form version minted this name — see # Runtime::StorageShape::FORM_VERSION; rows minted before the # column carry NULL, read as an implicit 1 @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int") # every frozen text version, archived where an edit cannot # reach it — the recovery the hard reattest refusal points at @db.exec(<<~SQL) CREATE TABLE IF NOT EXISTS hecks_era_texts ( domain text NOT NULL, ordinal int NOT NULL, digest text NOT NULL, held_text text NOT NULL, archived_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (domain, ordinal, digest) ) SQL @db.exec(<<~SQL) CREATE TABLE IF NOT EXISTS hecks_approvals ( domain text NOT NULL, from_label text NOT NULL, to_label text NOT NULL, edge_digest text NOT NULL, reviewed_ordinal bigint NOT NULL, approved_at timestamptz NOT NULL DEFAULT now() ) SQL @db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}") # GENERATED ALWAYS AS IDENTITY is the intent, but identity # columns on partitioned tables need Postgres 17 — an owned # sequence default is the same spanning ordinal on any # supported server. @db.exec(<<~SQL) CREATE TABLE IF NOT EXISTS #{quoted_journal} ( ordinal bigint NOT NULL DEFAULT nextval('#{sequence}'), era int NOT NULL, aggregate text NOT NULL, aggregate_id text NOT NULL, operation text NOT NULL DEFAULT 'save', state jsonb, mirrors jsonb ) PARTITION BY LIST (era) SQL ensure_partition!(1) # Immutability by privilege: nothing updates or deletes journal # rows. The owner's implicit rights remain (Postgres has no way # to revoke them from the owner itself); a deployment's app # role connects as a NON-owner and gets exactly INSERT, per # era, at mint time. @db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC") # RLS goes on AT PROVISIONING, never mid-life — enabling it # later would deny every role that has no policy yet, on # whatever the shape of the schema happened to be at that # moment. # # FORCE, not merely ENABLE: without it, the table OWNER is # exempt from every policy here, by Postgres default — which # would leave the schema writable forever to whoever holds # the owner's credentials, the one connection this whole # design cannot fence. Checked, not assumed: mint_era! never # inserts into the journal at all (only hecks_eras/ # hecks_era_texts, neither RLS-protected), and merge_tail!'s # one journal INSERT targets the CURRENT era, which the fence # already admits for anyone with base privileges — so FORCE # costs the owner nothing operations here actually need. # # This still exempts an actual Postgres SUPERUSER (or any # role granted BYPASSRLS) unconditionally — FORCE only # narrows what ENABLE already narrows for the owner # specifically, and superuser bypass sits above both. Running # migrations as a real superuser (self-hosted Postgres, most # commonly) leaves this gap open regardless; a managed # provider's admin account is typically NOT a superuser, and # is exactly what FORCE closes. # # GUARDED, not reissued unconditionally — measured, not # assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY` # takes AccessExclusiveLock EVEN WHEN THE SETTING IS ALREADY # CORRECT (Postgres does not skip the lock just because the # statement would be a no-op). ensure_base! runs on EVERY # boot by the owning role, not only the first — so an # unconditional reissue here would mean every ordinary # reboot of the deployment's own identity re-freezes every # concurrent writer, on any era, for as long as that ALTER # TABLE has to wait its turn. Read the current state first; # touch the catalog only on the boot that actually needs to. # pg_table_is_visible, NOT a bare relname match — a shared # instance (storehouse) can hold a same-named journal table # per schema; catalog lookups here must resolve the SAME way # search_path resolves an unqualified SQL reference, or a # sibling domain's table satisfies a query meant for this # domain's own. current = @db.exec_params( "SELECT relrowsecurity, relforcerowsecurity FROM pg_class " \ "WHERE relname = $1 AND pg_table_is_visible(oid)", [journal] )[0] @db.exec("ALTER TABLE #{quoted_journal} ENABLE ROW LEVEL SECURITY") unless current["relrowsecurity"] == "t" @db.exec("ALTER TABLE #{quoted_journal} FORCE ROW LEVEL SECURITY") unless current["relforcerowsecurity"] == "t" install_transforms! end |
#ensure_partition!(era) ⇒ Object
BUILD, THEN ATTACH — never CREATE ... PARTITION OF. The two produce the same partition; only the lock differs, and that difference is the whole availability story of a mint:
CREATE TABLE ... PARTITION OF → AccessExclusiveLock (parent)
CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
AccessExclusive conflicts with every insert in the hierarchy — routed through the parent OR addressed to an existing leaf — so attaching the new era inside the mint transaction stopped every writer for the WHOLE mint, tail materialization included. ShareUpdateExclusive conflicts with neither, so the old checkout keeps writing its own era straight through the build and only pauses for the head swap at the end.
That is what makes the fork real DURING a mint rather than merely before and after one. Measured, and pinned by the spec — which writes through a live mint rather than reading a lock mode out of the catalog.
255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb', line 255 def ensure_partition!(era) return if partition_attached?(era) @db.exec(<<~SQL) CREATE TABLE IF NOT EXISTS #{quote(partition(era))} ( LIKE #{quoted_journal} INCLUDING DEFAULTS ) SQL @db.exec(<<~SQL) ALTER TABLE #{quoted_journal} ATTACH PARTITION #{quote(partition(era))} FOR VALUES IN (#{era.to_i}) SQL end |
#partition_attached?(era) ⇒ Boolean
Attached, not merely present: a crash between the CREATE and the ATTACH leaves a table that is not yet part of the journal, and the next boot must finish the job rather than skip it.
272 273 274 275 276 277 278 279 280 281 |
# File 'lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb', line 272 def partition_attached?(era) @db.exec_params( "SELECT 1 FROM pg_inherits i " \ "JOIN pg_class child ON child.oid = i.inhrelid " \ "JOIN pg_class parent ON parent.oid = i.inhparent " \ "WHERE child.relname = $1 AND parent.relname = $2 " \ "AND pg_table_is_visible(child.oid) AND pg_table_is_visible(parent.oid)", [partition(era), journal] ).ntuples.positive? end |
#provisioner? ⇒ Boolean
Nothing provisioned yet — build it. Provisioned and owned — keep it current. Provisioned by SOMEONE ELSE — this is an app role, and the owner has already done this work.
227 228 229 230 231 232 233 234 |
# File 'lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb', line 227 def provisioner? rows = @db.exec_params( "SELECT pg_get_userbyid(relowner) = current_user AS owned FROM pg_class " \ "WHERE relname = $1 AND pg_table_is_visible(oid)", [journal] ) rows.ntuples.zero? || rows[0]["owned"] == "t" end |
#rename_domain! ⇒ Object
A DOMAIN'S OWN IDENTITY CHANGED — bridge its history under the
new name, before provisioner?/the CREATE TABLE IF NOT EXISTS
block below ever run. That ordering is load-bearing, not
tidiness: provisioner? and every statement in ensure_base!
test for the journal under journal — the NEW name — and a
freshly-renamed domain looks, to those checks, exactly like a
domain that has never been provisioned at all. Left where it
was written, ensure_base! would happily CREATE TABLE IF NOT
EXISTS a brand-new, empty journal under the new name before
this method ever got a chance to run — and the ALTER TABLE ...
RENAME below would then fail, renaming onto a name that
already exists.
Three-way precheck, cheapest first:
1. no hecks_eras table at all yet — a genuinely fresh
database; nothing to bridge, fall through to the
ordinary provisioning path.
2. hecks_eras already has rows under the NEW name — this
rename already ran (a prior boot, possibly this one on a
retry); idempotent no-op.
3. hecks_eras has rows under the OLD name — run the
migration.
Anything else (no rows under either name) falls through
harmlessly — formerly_known_as pointing at a name with no
held history is not an error, just inert.
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb', line 158 def rename_domain! return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"] return if @db.exec_params( "SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@domain] ).ntuples.positive? return if @db.exec_params( "SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@formerly_known_as] ).ntuples.zero? old_journal = "hecks_journal_#{Naming.snake(@formerly_known_as)}" old_sequence = "#{old_journal}_ordinal" ordinals = @db.exec_params( "SELECT ordinal FROM hecks_eras WHERE domain = $1 ORDER BY ordinal", [@formerly_known_as] ).map { |row| row["ordinal"].to_i } @db.exec("BEGIN") @db.exec("SET LOCAL lock_timeout = '10s'") # Fixed order — old before new, the `eras:` family before the # `ordinal:` family — so two rename attempts (or a rename # racing a mint/plain-write on either name) can only ever # queue behind one another, never deadlock. [ "hecks_eras:#{@formerly_known_as}", "hecks_eras:#{@domain}", "hecks_ordinal:#{@formerly_known_as}", "hecks_ordinal:#{@domain}" ].each do |key| @db.exec_params("SELECT pg_advisory_xact_lock(hashtext($1))", [key]) end @db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}") # An owned SERIAL/IDENTITY sequence moves automatically with # its table and errors if renamed explicitly — this one is a # plain CREATE SEQUENCE the journal's DEFAULT merely points # at (see ensure_base!), so it does need its own rename, and # the column default survives untouched: Postgres stores # nextval('...') as a regclass reference internally, not # literal text. @db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}") # ALTER TABLE ... RENAME on the parent does NOT cascade to # child partition names — each one needs its own explicit # rename, sourced from the ordinals held under the OLD name, # captured above before the UPDATE below flips the column. ordinals.each do |ordinal| old_partition = "#{old_journal}_era_#{ordinal}" @db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}") end %w[hecks_eras hecks_era_texts hecks_approvals].each do |table| @db.exec_params("UPDATE #{table} SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as]) end # Unlike its siblings, hecks_attestations is not created in # ensure_base! at all — only lazily, on first reattest! — so a # domain that never needed one must not be forced through an # UPDATE against a table that doesn't exist. @db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as]) if @db.exec_params("SELECT to_regclass($1)", ["hecks_attestations"])[0]["to_regclass"] @db.exec("COMMIT") rescue PG::LockNotAvailable @db.exec("ROLLBACK") rescue nil raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \ "one of the domain locks — waited 10s; try again shortly" rescue PG::Error => error @db.exec("ROLLBACK") rescue nil raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{error..strip}" end |