Class: Apartment::Adapters::AbstractAdapter

Inherits:
Object
  • Object
show all
Includes:
ActiveSupport::Callbacks
Defined in:
lib/apartment/adapters/abstract_adapter.rb

Overview

rubocop:disable Metrics/ClassLength

Instance Attribute Summary collapse

Instance Method Summary collapse

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_configObject (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

#create(tenant) ⇒ Object

Create a new tenant (schema or database).



43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/apartment/adapters/abstract_adapter.rb', line 43

def create(tenant)
  TenantNameValidator.validate!(
    environmentify(tenant),
    strategy: Apartment.config.tenant_strategy,
    adapter_name: base_config['adapter']
  )
  run_callbacks(:create) do
    create_tenant(tenant)
    grant_tenant_privileges(tenant)
    import_schema(tenant) if Apartment.config.schema_load_strategy
    seed(tenant) if Apartment.config.seed_after_create
    Instrumentation.instrument(:create, tenant: tenant)
  end
end

#default_tenantObject

Default tenant from config.



156
157
158
# File 'lib/apartment/adapters/abstract_adapter.rb', line 156

def default_tenant
  Apartment.config.default_tenant
end

#drop(tenant) ⇒ Object

Drop a tenant.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/apartment/adapters/abstract_adapter.rb', line 59

def drop(tenant) # rubocop:disable Metrics/CyclomaticComplexity
  drop_tenant(tenant)
  removed_pools = Apartment.pool_manager&.remove_tenant(tenant) || []
  removed_pools.each do |pool_key, pool|
    begin
      pool&.disconnect! if pool.respond_to?(:disconnect!)
    rescue StandardError => e
      warn "[Apartment] Pool disconnect failed for '#{pool_key}': #{e.class}: #{e.message}"
    end
    begin
      deregister_shard_from_ar_handler(pool_key)
    rescue StandardError => e
      warn "[Apartment] Shard deregistration failed for '#{pool_key}': #{e.class}: #{e.message}"
    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).



141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/apartment/adapters/abstract_adapter.rb', line 141

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

#migrate(tenant, version = nil) ⇒ Object

Run migrations for a tenant.



78
79
80
81
82
# File 'lib/apartment/adapters/abstract_adapter.rb', line 78

def migrate(tenant, version = nil)
  Apartment::Tenant.switch(tenant) do
    ActiveRecord::Base.connection_pool.migration_context.migrate(version)
  end
end

#process_excluded_modelsObject

Deprecated: use process_pinned_models instead.



133
134
135
136
137
# File 'lib/apartment/adapters/abstract_adapter.rb', line 133

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).



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/apartment/adapters/abstract_adapter.rb', line 110

def process_pinned_model(klass)
  # Idempotent: skip if already processed. Uses a class-level flag rather
  # than connection_specification_name comparison — the spec name differs
  # from ActiveRecord::Base for ApplicationRecord subclasses even before
  # establish_connection, so it's not a reliable "already processed" signal.
  return if klass.instance_variable_get(:@apartment_connection_established)

  # Use base_config (the adapter's raw connection config) rather than
  # resolve_connection_config(default_tenant). For database-per-tenant
  # strategies (MySQL, SQLite), resolve_connection_config would set the
  # database key to the default tenant NAME (e.g. 'default'), not the
  # actual default database (e.g. 'apartment_v4_test'). base_config
  # points to the real default database.
  klass.establish_connection(base_config)
  klass.instance_variable_set(:@apartment_connection_established, true)

  return unless Apartment.config.tenant_strategy == :schema

  table = klass.table_name.split('.').last
  klass.table_name = "#{default_tenant}.#{table}"
end

#process_pinned_modelsObject

Process all pinned models — establish separate connections pinned to default tenant.



100
101
102
103
104
105
106
# File 'lib/apartment/adapters/abstract_adapter.rb', line 100

def process_pinned_models
  return if Apartment.pinned_models.empty?

  Apartment.pinned_models.each do |klass|
    process_pinned_model(klass)
  end
end

#resolve_connection_config(tenant, base_config: nil) ⇒ Object

Resolve a tenant-specific connection config hash. Subclasses override to set strategy-specific keys.

Raises:

  • (NotImplementedError)


38
39
40
# File 'lib/apartment/adapters/abstract_adapter.rb', line 38

def resolve_connection_config(tenant, base_config: nil)
  raise(NotImplementedError)
end

#seed(tenant) ⇒ Object

Run seeds for a tenant.



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/apartment/adapters/abstract_adapter.rb', line 85

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

#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.



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

def validated_connection_config(tenant, base_config_override: nil)
  effective_base = base_config_override || base_config
  TenantNameValidator.validate!(
    tenant,
    strategy: Apartment.config.tenant_strategy,
    adapter_name: effective_base['adapter']
  )
  resolve_connection_config(tenant, base_config: effective_base)
end