Class: Apartment::Adapters::AbstractAdapter
- Inherits:
-
Object
- Object
- Apartment::Adapters::AbstractAdapter
- Includes:
- ActiveSupport::Callbacks
- Defined in:
- lib/apartment/adapters/abstract_adapter.rb
Overview
rubocop:disable Metrics/ClassLength
Direct Known Subclasses
Mysql2Adapter, PostgresqlDatabaseAdapter, PostgresqlSchemaAdapter, Sqlite3Adapter
Instance Attribute Summary collapse
-
#connection_config ⇒ Object
readonly
The raw database connection configuration hash (from ActiveRecord).
Instance Method Summary collapse
-
#aborted_transaction?(_conn) ⇒ Boolean
Whether
connsits in an aborted-transaction state that every subsequent statement will fail against until the transaction ends. - #apply_pinned_qualification(klass) ⇒ Object
-
#awaiting_own_qualification?(klass) ⇒ Boolean
A descendant that is registered but not yet processed gets qualified on its own turn, by direct assignment, which reaches names the ancestor's broadcast cannot.
-
#check_pinned_subclass(klass, sub, registered) ⇒ Object
Advisory only: this runs from process_pinned_models, which Tenant.init calls in after_initialize, so it must never be able to fail a boot.
-
#create(tenant) ⇒ Object
Create a new tenant (schema or database).
-
#current_db_role(_connection) ⇒ Object
The executing database role, for policies that need to name it explicitly (PostgreSQL's ALTER DEFAULT PRIVILEGES FOR ROLE).
-
#default_tenant ⇒ Object
Default tenant from config.
-
#drop(tenant) ⇒ Object
Drop a tenant.
-
#environmentify(tenant) ⇒ Object
Environmentify a tenant name based on config.
-
#failsafe_error_classes ⇒ Object
Request-path fail-safe contract.
- #inheriting_descendants(klass) ⇒ Object
-
#inherits_pinned_table?(klass) ⇒ Boolean
Whether
klassreaches its table through an already-pinned base class and so needs no qualification of its own. -
#initialize(connection_config) ⇒ AbstractAdapter
constructor
A new instance of AbstractAdapter.
-
#migrate(tenant, version = nil) ⇒ Object
Run migrations for a tenant.
-
#nearest_pinned_ancestor(klass, registered) ⇒ Object
The closest registered ancestor above
klass, i.e. -
#physical_tenant_name(tenant) ⇒ Object
The physical identifier used to address this tenant at connection time: the database name for database-per-tenant strategies (environmentified).
-
#pinned_qualification_mutates?(klass) ⇒ Boolean
Whether qualifying
klasswill actually change its naming. -
#pinned_table_name_for(model) ⇒ Object
A model whose table_name raises cannot be verified.
-
#pinned_table_qualifier ⇒ Object
The namespace that makes the default tenant's tables reachable from any tenant connection — a schema (PostgreSQL) or a database (MySQL).
-
#process_excluded_models ⇒ Object
Deprecated: use process_pinned_models instead.
-
#process_pinned_model(klass) ⇒ Object
Process a single pinned model.
-
#process_pinned_models ⇒ Object
Process all pinned models.
-
#qualify_pinned_table_name(klass) ⇒ Object
Qualify a pinned model's table_name so it targets the default tenant's tables from any tenant connection.
-
#qualify_pinned_table_name_prefix(klass) ⇒ Object
An abstract class has no table of its own — table_name is nil — so there is nothing to assign.
-
#resolve_connection_config(tenant, base_config: nil) ⇒ Object
Resolve a tenant-specific connection config hash.
-
#seed(tenant) ⇒ Object
Run seeds for a tenant.
-
#shared_pinned_connection? ⇒ Boolean
Whether pinned models can share the tenant's connection pool using qualified table names instead of establish_connection.
-
#standard_privilege_statements(_ctx, grant_to:, include_functions: true) ⇒ Object
The statements Privileges.standard should execute for ctx.phase, or [] when this engine needs none in that phase.
-
#tenant_container_gone?(error, tenant) ⇒ Boolean
Whether
error, raised while servingtenant, means the tenant's container (schema/database/file) no longer exists — so the validator should evict the name and the request should 404 instead of surfacing a 500. - #unqualified_pinned_message(model, name, prefix) ⇒ Object
- #unregistered_pinned_subclass?(sub, registered) ⇒ Boolean
-
#validated_connection_config(tenant, base_config_override: nil) ⇒ Object
Template method: validates tenant name then delegates to resolve_connection_config.
-
#verified_pinned_qualifier ⇒ Object
A nil or empty qualifier produces a bare ".table", which start_with?(".") would happily accept — the check proving nothing in exactly the case it exists for.
-
#verify_pinned_qualification!(klass) ⇒ Object
Prove the qualification took effect rather than assuming it did.
- #warn_unqualified_descendants(klass, prefix) ⇒ Object
- #warn_unqualified_subclass(klass, sub) ⇒ Object
-
#warn_unregistered_pinned_subclasses ⇒ Object
Warn about subclasses of a pinned model that declare their own table and were never registered.
Constructor Details
#initialize(connection_config) ⇒ AbstractAdapter
Returns a new instance of AbstractAdapter.
18 19 20 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 18 def initialize(connection_config) @connection_config = connection_config end |
Instance Attribute Details
#connection_config ⇒ Object (readonly)
The raw database connection configuration hash (from ActiveRecord). Not to be confused with Apartment.config (the Apartment::Config object).
16 17 18 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 16 def connection_config @connection_config end |
Instance Method Details
#aborted_transaction?(_conn) ⇒ Boolean
Whether conn sits in an aborted-transaction state that every subsequent
statement will fail against until the transaction ends. PostgreSQL is the
only supported engine with such a state (PQTRANS_INERROR); MySQL fails the
statement and leaves the transaction usable, and its raw connection has no
transaction_status at all. Base is conservative: never reclassify.
See docs/designs/transaction-taint-detection.md (Evidence E).
129 130 131 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 129 def aborted_transaction?(_conn) false end |
#apply_pinned_qualification(klass) ⇒ Object
227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 227 def apply_pinned_qualification(klass) return qualify_pinned_table_name_prefix(klass) if klass.abstract_class? return klass.apartment_mark_processed! if inherits_pinned_table?(klass) # Captured before the assignment below, which would otherwise make # every model look explicitly named. path = klass.apartment_explicit_table_name? ? :explicit : :computed original = klass.table_name klass.table_name = "#{pinned_table_qualifier}.#{original.sub(/\A[^.]+\./, '')}" klass.apartment_mark_processed!(path, (original if path == :explicit)) end |
#awaiting_own_qualification?(klass) ⇒ Boolean
A descendant that is registered but not yet processed gets qualified on its own turn, by direct assignment, which reaches names the ancestor's broadcast cannot. Registry order is always parent-first — defining a subclass loads its parent, and pin_tenant registers during class-body execution — so verifying the base's descendants would otherwise raise on a model that is about to become correct, aborting the very iteration that would have fixed it. That is the remedy this check's own error message prescribes, so it has to keep working.
338 339 340 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 338 def awaiting_own_qualification?(klass) Apartment.pinned_models.include?(klass) && !klass.apartment_pinned_processed? end |
#check_pinned_subclass(klass, sub, registered) ⇒ Object
Advisory only: this runs from process_pinned_models, which Tenant.init calls in after_initialize, so it must never be able to fail a boot. Rails' naming machinery raises on shapes we do not control — an anonymous descendant has no model_name, and Class.new(SomeBase) is everywhere in test suites.
451 452 453 454 455 456 457 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 451 def check_pinned_subclass(klass, sub, registered) return unless unregistered_pinned_subclass?(sub, registered) warn_unqualified_subclass(nearest_pinned_ancestor(sub, registered) || klass, sub) rescue StandardError => e warn "[Apartment] could not check pinned subclass #{sub.inspect}: #{e.class}: #{e.}" end |
#create(tenant) ⇒ Object
Create a new tenant (schema or database). Validates the physical identifier create_tenant actually addresses (raw schema name for :schema, environmentified database name otherwise), so create and the pool-resolution path validate the same name.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 54 def create(tenant) validate_pool_key_safety!(tenant) TenantNameValidator.validate!( physical_tenant_name(tenant), strategy: Apartment.config.tenant_strategy, adapter_name: base_config['adapter'] ) suppressing_pending_migration_check do run_callbacks(:create) do run_tenant_ddl(tenant) seed(tenant) if Apartment.config.seed_after_create Instrumentation.instrument(:create, tenant: tenant) end end end |
#current_db_role(_connection) ⇒ Object
The executing database role, for policies that need to name it explicitly (PostgreSQL's ALTER DEFAULT PRIVILEGES FOR ROLE). nil where the engine has no role system. Token shape differs by engine, so each adapter answers.
174 175 176 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 174 def current_db_role(_connection) nil end |
#default_tenant ⇒ Object
Default tenant from config.
555 556 557 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 555 def default_tenant Apartment.config.default_tenant end |
#drop(tenant) ⇒ Object
Drop a tenant.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 71 def drop(tenant) # Wrapped for the same reason create is: the container is owned by ddl_role, # and DROP SCHEMA requires ownership, so the writing role generally cannot drop # what the gem created. Only the engine call — the pool removal and shard # deregistration below are local bookkeeping and need no role. MigrationRole.wrap { drop_tenant(tenant) } removed_pools = Apartment.pool_manager&.remove_tenant(tenant) || [] removed_pools.each do |pool_key, pool| # remove_tenant already took these out of the manager, so deregister_shard's # own removal returns nil and cannot disconnect them — we close them here. Apartment.disconnect_removed_pool(pool, pool_key) begin deregister_shard_from_ar_handler(pool_key) rescue StandardError => e warn "[Apartment] Shard deregistration failed for '#{pool_key}': #{e.class}: #{e.}" end end Instrumentation.instrument(:drop, tenant: tenant) end |
#environmentify(tenant) ⇒ Object
Environmentify a tenant name based on config. :prepend/:append require Rails to be defined (for Rails.env).
531 532 533 534 535 536 537 538 539 540 541 542 543 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 531 def environmentify(tenant) case Apartment.config.environmentify_strategy when :prepend "#{rails_env}_#{tenant}" when :append "#{tenant}_#{rails_env}" when nil tenant.to_s else # Callable Apartment.config.environmentify_strategy.call(tenant) end end |
#failsafe_error_classes ⇒ Object
Request-path fail-safe contract. The elevator wraps the tenant switch; on one of these error classes it asks #tenant_container_gone? whether the tenant's storage actually vanished (a cross-process drop) rather than an app-level failure. An empty list disables the rescue, so an adapter that does not implement the seams never converts an error into a 404.
139 140 141 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 139 def failsafe_error_classes [] end |
#inheriting_descendants(klass) ⇒ Object
316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 316 def inheriting_descendants(klass) return [] unless klass.respond_to?(:descendants) klass.descendants.select do |sub| sub.respond_to?(:apartment_inherited_table_name) && !awaiting_own_qualification?(sub) && sub.table_name == sub.apartment_inherited_table_name rescue StandardError => e warn "[Apartment] could not classify pinned descendant #{sub.name || sub.inspect}: " \ "#{e.class}: #{e.}" false end end |
#inherits_pinned_table?(klass) ⇒ Boolean
Whether klass reaches its table through an already-pinned base class
and so needs no qualification of its own. Rails resolves a subclass's
table through base_class.table_name, which the base's qualification
already covers; assigning here would freeze a copy of the base's
qualified name onto the child and desynchronise the two on teardown.
Scoped narrowly on purpose. A subclass that declares its own table — the transitional shape when migrating an STI child off a pinned parent's table — is NOT covered and qualifies normally, because the parent's qualification cannot reach a different table. And a subclass whose base class is not pinned (e.g. an app model extending a gem's model) is not covered either, which is the case that motivated qualifying by assignment in the first place.
387 388 389 390 391 392 393 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 387 def inherits_pinned_table?(klass) return false if klass.base_class? return false if klass.apartment_explicit_table_name? base = klass.base_class base.respond_to?(:apartment_pinned?) && base.apartment_pinned? end |
#migrate(tenant, version = nil) ⇒ Object
Run migrations for a tenant.
92 93 94 95 96 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 92 def migrate(tenant, version = nil) Apartment::Tenant.switch(tenant) do ActiveRecord::Base.connection_pool.migration_context.migrate(version) end end |
#nearest_pinned_ancestor(klass, registered) ⇒ Object
The closest registered ancestor above klass, i.e. the pin it inherits.
460 461 462 463 464 465 466 467 468 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 460 def nearest_pinned_ancestor(klass, registered) ancestor = klass.superclass while ancestor.is_a?(Class) && ancestor < ActiveRecord::Base return ancestor if registered.include?(ancestor) ancestor = ancestor.superclass end nil end |
#physical_tenant_name(tenant) ⇒ Object
The physical identifier used to address this tenant at connection time: the database name for database-per-tenant strategies (environmentified). validated_connection_config validates THIS name so the pool-resolution path agrees with what the connection actually targets. Schema-per-tenant overrides this to the raw tenant (schemas are named directly).
550 551 552 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 550 def physical_tenant_name(tenant) environmentify(tenant) end |
#pinned_qualification_mutates?(klass) ⇒ Boolean
Whether qualifying klass will actually change its naming. False for
the branch that deliberately mutates nothing, which must not be
verified: a subclass sharing an already-pinned base's table is correct
by construction, and if its base has not been qualified yet (registry
order) it is about to become so.
223 224 225 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 223 def pinned_qualification_mutates?(klass) klass.abstract_class? || !inherits_pinned_table?(klass) end |
#pinned_table_name_for(model) ⇒ Object
A model whose table_name raises cannot be verified. Say so rather than skipping silently: the thesis of this check is proving rather than assuming, and an unverifiable model is exactly what it must not wave through unremarked.
295 296 297 298 299 300 301 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 295 def pinned_table_name_for(model) model.table_name rescue StandardError => e warn '[Apartment] could not verify the pinned table name for ' \ "#{model.name || model.inspect}: #{e.class}: #{e.}" nil end |
#pinned_table_qualifier ⇒ Object
The namespace that makes the default tenant's tables reachable from any tenant connection — a schema (PostgreSQL) or a database (MySQL). Subclasses must implement when shared_pinned_connection? returns true.
181 182 183 184 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 181 def pinned_table_qualifier raise(NotImplementedError, "#{self.class}#pinned_table_qualifier must be implemented when shared_pinned_connection? is true") end |
#process_excluded_models ⇒ Object
Deprecated: use process_pinned_models instead.
523 524 525 526 527 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 523 def process_excluded_models warn '[Apartment] DEPRECATION: process_excluded_models is deprecated. ' \ 'Use Apartment::Model with pin_tenant instead.' process_pinned_models end |
#process_pinned_model(klass) ⇒ Object
Process a single pinned model. Called by process_pinned_models (batch) and by Apartment::Model.pin_tenant (when activated? is true).
When shared_pinned_connection? is true, qualifies the table name so the model uses the tenant's pool (preserving transactional integrity). Otherwise, establishes a separate connection pool (required when cross-database queries are impossible).
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 495 def process_pinned_model(klass) # Ensure the concern is included — models registered via the # excluded_models shim may not have it yet. Uses apartment_mark_pinned! # (not pin_tenant) to avoid recursion back into process_pinned_model. unless klass.respond_to?(:apartment_pinned_processed?) klass.include(Apartment::Model) klass.apartment_mark_pinned! end return if klass.apartment_pinned_processed? # A subclass that reaches its table through an already-pinned base needs # nothing on either path. Qualifying would freeze a copy of the base's # name onto it; establishing a connection would hand it a *different* # pool from its parent, splitting two classes that share one physical # table across connections and breaking transactional integrity between # them. Mark processed with a nil path so teardown skips it too. return klass.apartment_mark_processed! if inherits_pinned_table?(klass) if shared_pinned_connection? qualify_pinned_table_name(klass) else klass.establish_connection(pinned_model_config) klass.apartment_mark_processed! end end |
#process_pinned_models ⇒ Object
Process all pinned models. When shared_pinned_connection? is true, qualifies table names for shared pool routing. Otherwise, establishes separate connections.
397 398 399 400 401 402 403 404 405 406 407 408 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 397 def process_pinned_models return if Apartment.pinned_models.empty? Apartment.pinned_models.each do |klass| process_pinned_model(klass) rescue StandardError => e raise(Apartment::ConfigurationError, "Failed to process pinned model #{klass.name}: #{e.class}: #{e.}") end warn_unregistered_pinned_subclasses end |
#qualify_pinned_table_name(klass) ⇒ Object
Qualify a pinned model's table_name so it targets the default tenant's tables from any tenant connection.
Always assigns table_name directly. The tempting alternative — set table_name_prefix and let Rails recompose — is unsound, because compute_table_name only consults full_table_name_prefix on its base_class? branch:
* a class that is not its own base_class gets base_class.table_name
verbatim, so the prefix is discarded outright;
* full_table_name_prefix prefers the first module parent that responds
to table_name_prefix, so an engine-namespaced model ignores the
prefix set on the class itself;
* overwriting the prefix drops one the app set, silently retargeting
the model at a different table.
In each case the model resolves to the tenant's table and nothing raises. Reading table_name first lets Rails compute the conventional name — honouring any prefix, suffix, or nesting the app declared — before we qualify the result.
206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 206 def qualify_pinned_table_name(klass) # Captured before the mutation below: afterwards there is no way to # tell a descendant's stale memo from a table it declared itself. inheriting = klass.apartment_descendants_inheriting_table_name # Evaluated before the mutation, which is what inherits_pinned_table? # inspects. mutates = pinned_qualification_mutates?(klass) apply_pinned_qualification(klass) klass.apartment_resync_descendant_table_names!(inheriting) verify_pinned_qualification!(klass) if mutates end |
#qualify_pinned_table_name_prefix(klass) ⇒ Object
An abstract class has no table of its own — table_name is nil — so
there is nothing to assign. Pinning one is a supported pattern (an
abstract connects_to base is pinned so Apartment does not build
tenant pools for it), and its qualifier still has to reach the concrete
descendants that inherit the pin.
Those descendants are never qualified directly: pin_tenant early-returns
once any superclass is pinned (apartment_pinned? walks the chain), so
they are never registered and process_pinned_models never sees them.
table_name_prefix is a class_attribute, so setting it here broadcasts
down the inheritance chain and each descendant composes it in its own
compute_table_name. Any prefix the app set is preserved rather than
overwritten, so myapp_ becomes <qualifier>.myapp_.
This is the one place the prefix mechanism is still correct, because here it is a broadcast to other classes rather than an attempt to qualify this class's own name.
368 369 370 371 372 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 368 def qualify_pinned_table_name_prefix(klass) original_prefix = klass.table_name_prefix klass.table_name_prefix = "#{pinned_table_qualifier}.#{original_prefix}" klass.apartment_mark_processed!(:prefix, original_prefix) end |
#resolve_connection_config(tenant, base_config: nil) ⇒ Object
Resolve a tenant-specific connection config hash. Subclasses override to set strategy-specific keys.
46 47 48 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 46 def resolve_connection_config(tenant, base_config: nil) raise(NotImplementedError) end |
#seed(tenant) ⇒ Object
Run seeds for a tenant.
99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 99 def seed(tenant) Apartment::Tenant.switch(tenant) do seed_file = Apartment.config.seed_data_file return unless seed_file unless File.exist?(seed_file) raise(Apartment::ConfigurationError, "Seed file '#{seed_file}' does not exist") end load(seed_file) end end |
#shared_pinned_connection? ⇒ Boolean
Whether pinned models can share the tenant's connection pool using qualified table names instead of establish_connection.
Returns false by default (separate pool). Subclasses override to return true when the engine supports cross-schema/database queries, gated by config.force_separate_pinned_pool.
119 120 121 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 119 def shared_pinned_connection? false end |
#standard_privilege_statements(_ctx, grant_to:, include_functions: true) ⇒ Object
The statements Privileges.standard should execute for ctx.phase, or [] when this engine needs none in that phase. A pure function of its inputs: build, do not execute, so the SQL is unit-testable without a database.
ConfigurationError rather than NotImplementedError. An adopter who configured
the standard policy on a strategy that has none made a configuration mistake,
and NotImplementedError descends from ScriptError, so rescue StandardError
around Tenant.create would not catch it. The NotImplementedError raises
elsewhere in this class mean something different: a subclass owes an
implementation.
165 166 167 168 169 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 165 def standard_privilege_statements(_ctx, grant_to:, include_functions: true) # rubocop:disable Lint/UnusedMethodArgument raise(Apartment::ConfigurationError, "Apartment::Privileges.standard does not support #{self.class.name}. " \ 'Write a tenant_privilege_policy for this strategy; see docs/rbac.md.') end |
#tenant_container_gone?(error, tenant) ⇒ Boolean
Whether error, raised while serving tenant, means the tenant's
container (schema/database/file) no longer exists — so the validator
should evict the name and the request should 404 instead of surfacing a
500. Composed from a cheap error-shape check and an authoritative
existence probe, both conservative by default so the base adapter never
reclassifies. Subclasses override the seams.
149 150 151 152 153 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 149 def tenant_container_gone?(error, tenant) return false unless container_error?(unwrap_db_error(error)) !tenant_container_exists?(tenant) end |
#unqualified_pinned_message(model, name, prefix) ⇒ Object
342 343 344 345 346 347 348 349 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 342 def (model, name, prefix) "[Apartment] #{model.name || model.inspect} is pinned but its table name " \ "(#{name.inspect}) is not qualified with #{prefix.inspect}, so it would read the " \ 'current tenant instead of the default tenant. This usually means Rails composed ' \ "the name from something the qualifier cannot reach — a module parent's " \ 'table_name_prefix, or a base class outside the pinned hierarchy. Call pin_tenant ' \ 'on the model directly, or set self.table_name to an already-qualified name.' end |
#unregistered_pinned_subclass?(sub, registered) ⇒ Boolean
470 471 472 473 474 475 476 477 478 479 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 470 def unregistered_pinned_subclass?(sub, registered) return false if registered.include?(sub) # Anonymous classes have no model_name for Rails to compute a table # from, and nothing actionable to name in a warning. return false if sub.name.nil? return false unless sub.respond_to?(:apartment_explicit_table_name?) return false if sub.abstract_class? sub.apartment_explicit_table_name? end |
#validated_connection_config(tenant, base_config_override: nil) ⇒ Object
Template method: validates tenant name then delegates to resolve_connection_config. Called by ConnectionHandling — subclasses should NOT override this. base_config_override: when supplied (e.g. a role-specific config from ConnectionHandling), the adapter builds the tenant config on top of it instead of its own base_config.
This validates only the PHYSICAL identifier (engine rules). Raw pool-key safety (colon/whitespace/NUL that would corrupt "tenant:role") is enforced by the sole production caller, ConnectionHandling#connection_pool, before it builds the pool key — and independently by #create. A future caller that invokes this directly, bypassing connection_pool, must validate the raw tenant itself (TenantNameValidator.validate_common!).
33 34 35 36 37 38 39 40 41 42 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 33 def validated_connection_config(tenant, base_config_override: nil) effective_base = base_config_override || base_config TenantNameValidator.validate!( physical_tenant_name(tenant), strategy: Apartment.config.tenant_strategy, adapter_name: effective_base['adapter'] ) config = resolve_connection_config(tenant, base_config: effective_base) apply_tenant_pool_size(config) end |
#verified_pinned_qualifier ⇒ Object
A nil or empty qualifier produces a bare ".table", which start_with?(".") would happily accept — the check proving nothing in exactly the case it exists for. On MySQL this is a connection config with no 'database' key.
306 307 308 309 310 311 312 313 314 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 306 def verified_pinned_qualifier qualifier = pinned_table_qualifier return qualifier unless qualifier.nil? || qualifier.to_s.empty? raise(Apartment::ConfigurationError, "[Apartment] #{self.class}#pinned_table_qualifier is #{qualifier.inspect}, so pinned " \ 'models were qualified to a bare ".table" and would not resolve. On MySQL this ' \ "usually means the connection config carries no 'database' key.") end |
#verify_pinned_qualification!(klass) ⇒ Object
Prove the qualification took effect rather than assuming it did.
A failed qualification is otherwise indistinguishable from a successful one: the model is marked processed, nothing raises, and it serves the current tenant's rows. Checking the post-condition surfaces that class of failure at the point it happens — including one introduced by a future Rails change to the naming internals, since compute_table_name is not public API.
An abstract base has no table of its own, so it is proven through the
descendants its prefix was meant to reach; a nil name is skipped.
The descendant set is computed here rather than reused from the resync pass: that one is deliberately limited to descendants holding a memo, while a descendant with no memo inherits its name lazily and can be just as wrong. Descendants that declare their own table are excluded — an ancestor's qualification was never meant to reach them, and warn_unregistered_pinned_subclasses already reports that shape. The model itself RAISES; its descendants only WARN. The asymmetry is the point, and it is the same rule warn_unregistered_pinned_subclasses follows: raise only on what this pass can actually prove.
For the registered model, the check is complete and unambiguous — it was just qualified, so if the name is not qualified something is genuinely broken, every time, on every boot.
For descendants it is neither. Detection walks descendants, so it is
complete under eager loading and partial under Zeitwerk lazy loading:
raising would fail production boots for a condition that dev never
reports, while still missing the descendants that load later. A warning
carries the same signal without making detection completeness a
boot-time dependency.
271 272 273 274 275 276 277 278 279 280 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 271 def verify_pinned_qualification!(klass) prefix = "#{verified_pinned_qualifier}." name = pinned_table_name_for(klass) if name && !name.start_with?(prefix) raise(Apartment::ConfigurationError, (klass, name, prefix)) end warn_unqualified_descendants(klass, prefix) end |
#warn_unqualified_descendants(klass, prefix) ⇒ Object
282 283 284 285 286 287 288 289 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 282 def warn_unqualified_descendants(klass, prefix) inheriting_descendants(klass).each do |sub| sub_name = pinned_table_name_for(sub) next if sub_name.nil? || sub_name.start_with?(prefix) warn((sub, sub_name, prefix)) end end |
#warn_unqualified_subclass(klass, sub) ⇒ Object
481 482 483 484 485 486 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 481 def warn_unqualified_subclass(klass, sub) warn "[Apartment] #{sub.name || sub.inspect} inherits a pin from " \ "#{klass.name || klass.inspect} but declares its own table " \ "(#{sub.table_name}) and was never registered, so it is not qualified. " \ "Call pin_tenant on it if it should read the default tenant's data." end |
#warn_unregistered_pinned_subclasses ⇒ Object
Warn about subclasses of a pinned model that declare their own table and were never registered. Such a class inherits apartment_pinned? through the superclass walk but gets no qualification, so on a shared-connection adapter it silently reads the tenant's table — and on a separate-pool adapter a genuinely tenant-scoped one silently reads the default's. The shape is transitional (migrating an STI child off a pinned parent's table), which is exactly when a silent read is most costly: the symptom looks like a botched backfill.
Detection walks descendants, so it is complete under eager loading (production boot, CI) and partial under Zeitwerk lazy loading. That is tolerable for a warning and would not be for a raise — which is why this warns rather than raising. descendants is transitive, so every pinned class in one inheritance chain sees the same unregistered descendant. Deduplicate, and attribute each warning to the nearest pinned ancestor — the one whose pin the subclass actually inherits — so the message is deterministic rather than dependent on registry iteration order.
428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 |
# File 'lib/apartment/adapters/abstract_adapter.rb', line 428 def warn_unregistered_pinned_subclasses # Snapshot the registry and walk it outside its own lock. Concurrent::Set # synchronizes every method on CRuby, #each included, so iterating in # place would hold a process-wide monitor across descendant walking and # stderr I/O. Same leaf-lock discipline as Patches::ConnectionRegistry. pinned = Apartment.pinned_models.to_a registered = Set.new(pinned) seen = Set.new pinned.each do |klass| next unless klass.respond_to?(:descendants) klass.descendants.each do |sub| check_pinned_subclass(klass, sub, registered) if seen.add?(sub) end end end |