Class: Apartment::Adapters::Mysql2Adapter

Inherits:
AbstractAdapter show all
Defined in:
lib/apartment/adapters/mysql2_adapter.rb

Overview

v4 MySQL adapter using database-per-tenant isolation (mysql2 driver).

Resolves tenant-specific connection configs by setting the database key to the environmentified tenant name. Lifecycle operations (create/drop) execute DDL against the default connection.

Direct Known Subclasses

TrilogyAdapter

Instance Attribute Summary

Attributes inherited from AbstractAdapter

#connection_config

Instance Method Summary collapse

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, #physical_tenant_name, #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

Returns MySQL's role@host form, for a policy that needs to name the executing account. The statement builder above does not consume it: it assumes the % host, and MySQL's GRANT syntax wants the halves quoted separately ('role'@'host'), which a whole role@host token cannot express.



71
72
73
# File 'lib/apartment/adapters/mysql2_adapter.rb', line 71

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

#failsafe_error_classesObject

The database-per-tenant missing-tenant error: connecting to a dropped database raises ActiveRecord::NoDatabaseError (MySQL error 1049) — an unambiguous signal. It surfaces raw at query time, or wrapped in ApartmentError when ConnectionHandling resolves the pool (the dev-mode pending-migration check), so both are listed; #container_error? gates on the unwrapped NoDatabaseError. Inherited by TrilogyAdapter.



34
35
36
# File 'lib/apartment/adapters/mysql2_adapter.rb', line 34

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

#pinned_table_qualifierObject

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



19
20
21
# File 'lib/apartment/adapters/mysql2_adapter.rb', line 19

def pinned_table_qualifier
  base_config['database']
end

#resolve_connection_config(tenant, base_config: nil) ⇒ Object



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

def resolve_connection_config(tenant, base_config: nil)
  config = base_config || send(:base_config)
  config.merge('database' => environmentify(tenant))
end

#shared_pinned_connection?Boolean

Returns:

  • (Boolean)


13
14
15
# File 'lib/apartment/adapters/mysql2_adapter.rb', line 13

def shared_pinned_connection?
  !Apartment.config.force_separate_pinned_pool
end

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

MySQL has no ALTER DEFAULT PRIVILEGES. ON db.* is pattern-based and covers objects created later, so one statement in the first phase is the whole policy and include_functions has nothing to control here.

grant_to takes bare role names and every grant lands on role@'%'. Splitting an account on its last @ would be wrong, because me@localhost is itself a legal MySQL username, so a value carrying @ is refused rather than guessed at. A specific host is what a custom policy is for.

Branching on the phase by name, rather than falling out of a before_schema_load? guard, so the empty after-phase is a stated decision and an unrecognised phase raises. A silent nothing is the defect this whole design replaces: app_role's String form did nothing on two adapters and told nobody.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/apartment/adapters/mysql2_adapter.rb', line 51

def standard_privilege_statements(ctx, grant_to:, include_functions: true) # rubocop:disable Lint/UnusedMethodArgument
  case ctx.phase
  when :before_schema_load
    roles = Array(grant_to)
    validate_bare_role_names!(roles)
    accounts = roles.map { |role| "#{ctx.connection.quote(role)}@'%'" }.join(', ')
    ["GRANT SELECT, INSERT, UPDATE, DELETE ON #{ctx.quoted_container}.* TO #{accounts}"]
  when :after_schema_load
    # Nothing to do: the grant above already covers tables the import and later
    # migrations create.
    []
  else
    raise(Apartment::ConfigurationError, "Unknown privilege policy phase: #{ctx.phase.inspect}")
  end
end