Class: Apartment::Adapters::PostgresqlSchemaAdapter

Inherits:
AbstractAdapter show all
Includes:
PostgresqlTransactionState
Defined in:
lib/apartment/adapters/postgresql_schema_adapter.rb

Overview

v4 PostgreSQL adapter using schema-based tenant isolation.

Resolves tenant-specific connection configs by setting schema_search_path to the raw tenant name (not environmentified — schemas are named directly, unlike database-per-tenant adapters) plus any persistent schemas from Apartment.config.postgres_config. Lifecycle operations (create/drop) execute DDL against the default connection.

Instance Attribute Summary

Attributes inherited from AbstractAdapter

#connection_config

Instance Method Summary collapse

Methods included from PostgresqlTransactionState

#aborted_transaction?

Methods inherited from AbstractAdapter

#aborted_transaction?, #apply_pinned_qualification, #awaiting_own_qualification?, #check_pinned_subclass, #create, #default_tenant, #drop, #environmentify, #inheriting_descendants, #inherits_pinned_table?, #initialize, #migrate, #nearest_pinned_ancestor, #pinned_qualification_mutates?, #pinned_table_name_for, #process_excluded_models, #process_pinned_model, #process_pinned_models, #qualify_pinned_table_name, #qualify_pinned_table_name_prefix, #seed, #tenant_container_gone?, #unqualified_pinned_message, #unregistered_pinned_subclass?, #validated_connection_config, #verified_pinned_qualifier, #verify_pinned_qualification!, #warn_unqualified_descendants, #warn_unqualified_subclass, #warn_unregistered_pinned_subclasses

Constructor Details

This class inherits a constructor from Apartment::Adapters::AbstractAdapter

Instance Method Details

#current_db_role(connection) ⇒ Object

pg_get_userbyid-free: current_user is what ALTER DEFAULT PRIVILEGES scopes to when FOR ROLE is omitted, so it is the role a policy must name to be explicit.



84
85
86
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 84

def current_db_role(connection)
  connection.select_value('SELECT current_user')
end

#failsafe_error_classesObject

The schema-strategy missing-tenant error: a dropped schema is not caught at switch time (search_path accepts a non-existent schema silently) — it surfaces on the first query as ActiveRecord::StatementInvalid (PG::UndefinedTable, 42P01). That is the same shape as a missing table in a live schema, so #tenant_container_exists? does the disambiguating to_regnamespace check.

ApartmentError is included because ConnectionHandling wraps errors raised during pool resolution (e.g. the dev-mode pending-migration check, which queries schema_migrations in the gone schema) as ApartmentError with the StatementInvalid as #cause; #container_error? then unwraps and classifies it the same as the query-time case, and re-raises any other ApartmentError.



53
54
55
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 53

def failsafe_error_classes
  [ActiveRecord::StatementInvalid, Apartment::ApartmentError]
end

#physical_tenant_name(tenant) ⇒ Object

Schemas are named directly (never environmentified), so the physical identifier validated at pool-resolution time is the raw tenant name.



37
38
39
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 37

def physical_tenant_name(tenant)
  tenant.to_s
end

#pinned_table_qualifierObject

Pinned tables live in the default tenant's schema; every tenant connection can reach them by schema-qualifying the name.



23
24
25
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 23

def pinned_table_qualifier
  default_tenant
end

#resolve_connection_config(tenant, base_config: nil) ⇒ Object



27
28
29
30
31
32
33
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 27

def resolve_connection_config(tenant, base_config: nil)
  config = base_config || send(:base_config)
  persistent = Apartment.config.postgres_config&.persistent_schemas || []
  search_path = [tenant, *persistent].map { |s| %("#{s}") }.join(',')

  config.merge('schema_search_path' => search_path)
end

#shared_pinned_connection?Boolean

Returns:

  • (Boolean)


17
18
19
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 17

def shared_pinned_connection?
  !Apartment.config.force_separate_pinned_pool
end

#standard_privilege_statements(ctx, grant_to:, include_functions: true) ⇒ Object

Role names are quoted with quote_column_name, not quote_table_name: the latter splits on dots, so a legal role like svc.migrator becomes "svc"."migrator" and a.b.c silently loses a segment. Neither is valid where one role identifier is required, and role names never pass TenantNameValidator. quoted_container stays as it is; container names do pass it, and it rejects dots.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/apartment/adapters/postgresql_schema_adapter.rb', line 62

def standard_privilege_statements(ctx, grant_to:, include_functions: true)
  roles = Array(grant_to).map { |role| ctx.connection.quote_column_name(role) }.join(', ')
  schema = ctx.quoted_container

  case ctx.phase
  when :before_schema_load
    before_schema_load_statements(ctx, schema, roles, include_functions)
  when :after_schema_load
    [
      "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA #{schema} TO #{roles}",
      "GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA #{schema} TO #{roles}",
    ]
  else
    # Contexts are gem-built, so an unknown phase is a gem bug. Raising beats
    # falling through to the after-phase statements, which would grant on
    # objects the caller may not have created yet.
    raise(Apartment::ConfigurationError, "Unknown privilege policy phase: #{ctx.phase.inspect}")
  end
end