Module: Apartment::Patches::PostgresqlSequenceName

Defined in:
lib/apartment/patches/postgresql_sequence_name.rb

Overview

Keeps ActiveRecord's class-level Model.sequence_name memoization schema-agnostic under pool-per-tenant.

Rails resolves default_sequence_name via pg_get_serial_sequence with an unqualified table name, so PostgreSQL answers through the current connection's search_path and returns a schema-QUALIFIED name — the schema of whichever tenant's pool happened to resolve it first. ActiveRecord then memoizes that value once per model class, process-wide. Consumers that render it into SQL (activerecord-import prefetches primary keys as a literal nextval(Model.sequence_name)) would draw ids from the first-resolver tenant's sequence in every tenant: wrong-tenant ids, silent sequence drift, and eventual PG::UniqueViolation.

Stripping the connection's own current_schema prefix makes the memoized value schema-agnostic: nextval('widgets_id_seq') re-resolves through each pool's search_path, per tenant — the same invariant v4 relies on for table names (see docs/designs/v4-connection-model-rationale.md). A prefix naming any OTHER schema is preserved on purpose: persistent-schema tables and pinned models (whose table names are default_tenant-qualified and may execute on a shared tenant connection) are only correct BECAUSE they stay qualified.

v3 shipped this guarantee as PostgreSqlAdapterPatch; it was lost in the Phase 2.5 v3 deletion (PR #356). Reset-on-switch is not an alternative: v4 serves tenants concurrently in one process, so only a schema-agnostic memoized value can be correct.

Instance Method Summary collapse

Instance Method Details

#default_sequence_name(table_name, primary_key = 'id') ⇒ Object



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
# File 'lib/apartment/patches/postgresql_sequence_name.rb', line 32

def default_sequence_name(table_name, primary_key = 'id')
  res = super
  return res if res.nil?
  # A schema-qualified table name is a PINNED model: Apartment qualifies
  # those to the default tenant, and they resolve on whatever connection is
  # current (the schema strategy shares the tenant's connection). Its
  # sequence must stay qualified — stripping here is the mirror-image bug,
  # because an unqualified name would later re-resolve against a *tenant's*
  # search_path and draw ids from that tenant's stale copy of the pinned
  # table's sequence. That order is the common one, not a corner case:
  # pinned models are typically first touched at boot, on the default pool.
  # v3 guarded the same case by force-adding the default_tenant prefix.
  return res if table_name.to_s.include?('.')

  # Routed table: drop whatever schema PostgreSQL qualified the sequence
  # with — not just the connection's own. The two differ when the tenant
  # schema is missing the table and search_path falls through to a schema
  # that has it: the resolved sequence then names the FALLBACK schema, and
  # preserving that would memoize it process-wide, so every tenant —
  # including correctly-migrated ones — would draw from the fallback's
  # sequence forever. Unqualified is the only value that lets the sequence
  # re-resolve per pool exactly like the table name does.
  #
  # Parsed with AR's own splitter (rather than by hand) because it is what
  # produced `res`, so it round-trips quoting and dotted identifiers.
  ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
    .extract_schema_qualified_name(res).identifier
end