Module: Ec::Pg::SchemaManager

Defined in:
lib/ec/pg/schema_manager.rb

Overview

Manages PostgreSQL schema switching via the search_path session variable.

When you have one Postgres database with a schema-per-tenant layout:

CREATE SCHEMA tenant_abc;
CREATE TABLE tenant_abc.records (...);

This manager updates the connection’s search_path so that ActiveRecord’s unqualified table references resolve to the correct tenant schema.

Usage

SchemaManager.with_schema("tenant_abc") { Record.all }

The search_path is reset to the previous value (or the configured default) when the block exits, even if an exception is raised.

Thread safety

with_schema stores the schema name in the thread context AND updates the underlying database connection. When using connection pooling, each thread checks out its own connection, so search_path changes are thread-safe.

Defined Under Namespace

Classes: InvalidSchema

Class Method Summary collapse

Class Method Details

.active?Boolean

Returns true if a non-default schema is active.

Returns:

  • (Boolean)


60
61
62
# File 'lib/ec/pg/schema_manager.rb', line 60

def active?
  !Context.schema.nil?
end

.apply!(schema_name) ⇒ Object

Applies the search_path on an existing connection without block scoping. Useful in middleware or Rack apps that manage connection lifecycle manually.



66
67
68
69
70
71
72
# File 'lib/ec/pg/schema_manager.rb', line 66

def apply!(schema_name)
  schema_name = schema_name.to_s.strip
  validate_schema_name!(schema_name)

  Context.set(schema: schema_name)
  apply_schema(schema_name)
end

.apply_schema(schema_name) ⇒ Object

Builds the search_path string and executes SET search_path on the connection.



83
84
85
86
87
88
89
# File 'lib/ec/pg/schema_manager.rb', line 83

def apply_schema(schema_name)
  registered_connections.each do |connection|
    shared = Ec::Pg.configuration.shared_schemas
    path   = ([schema_name] + shared).uniq.join(", ")
    connection.schema_search_path = path
  end
end

.current_schemaObject

Returns the schema currently active in thread context.



55
56
57
# File 'lib/ec/pg/schema_manager.rb', line 55

def current_schema
  Context.schema || Ec::Pg.configuration.default_schema
end

.registered_connectionsObject



106
107
108
109
110
# File 'lib/ec/pg/schema_manager.rb', line 106

def registered_connections
  Ec::Pg::SchemaMixin
    .registered_models
    .map(&:connection)
end

.reset!Object

Resets search_path to the configured default schema on connection.



75
76
77
78
# File 'lib/ec/pg/schema_manager.rb', line 75

def reset!
  Context.delete(:schema)
  restore_schema
end

.restore_schemaObject

Restores the search_path to the configured default schema(s).



92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/ec/pg/schema_manager.rb', line 92

def restore_schema
  default = Ec::Pg.configuration.default_schema
  shared = Ec::Pg.configuration.shared_schemas
  path   = ([default] + shared).uniq.join(", ")

  registered_connections.each do |connection|
    if connection && connection.pool.present?
      connection.schema_search_path = path
    end
  end
rescue StandardError
  # Swallow errors during cleanup (connection may have been returned to pool)
end

.validate_schema_name!(name) ⇒ Object



115
116
117
118
119
120
121
# File 'lib/ec/pg/schema_manager.rb', line 115

def validate_schema_name!(name)
  return if ValidSchemaRegex.match?(name)

  raise InvalidSchema,
        "Invalid PostgreSQL schema name: #{name.inspect}. " \
        "Only alphanumeric characters and underscores are allowed."
end

.with_schema(schema_name) { ... } ⇒ Object

Executes block with the Postgres search_path set to schema_name, followed by any shared_schemas from the configuration.

Parameters:

  • schema_name (String)

    tenant schema (e.g. “tenant_abc”)

Yields:

  • block to execute within the schema context

Returns:

  • the return value of the block



39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/ec/pg/schema_manager.rb', line 39

def with_schema(schema_name, &block)
  schema_name = schema_name.to_s.strip
  validate_schema_name!(schema_name)

  Context.with(schema: schema_name) do
    apply_schema(schema_name)
    block.call
  end
ensure
  # Restore previous schema on the same connection if we changed it.
  # Guard against the case where the connection was already returned to
  # the pool during the block.
  restore_schema
end