ec-pg 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +254 -0
- data/Rakefile +8 -0
- data/config/locales/en.yml +2 -0
- data/lib/ec/pg/configuration.rb +47 -0
- data/lib/ec/pg/context.rb +76 -0
- data/lib/ec/pg/middleware/context_switcher.rb +34 -0
- data/lib/ec/pg/railtie.rb +37 -0
- data/lib/ec/pg/rls_manager.rb +121 -0
- data/lib/ec/pg/rls_mixin.rb +80 -0
- data/lib/ec/pg/schema_manager.rb +126 -0
- data/lib/ec/pg/schema_mixin.rb +52 -0
- data/lib/ec/pg/shard_manager.rb +94 -0
- data/lib/ec/pg/shard_mixin.rb +74 -0
- data/lib/ec/pg/tenant_context.rb +73 -0
- data/lib/ec/pg/version.rb +7 -0
- data/lib/ec/pg.rb +53 -0
- data/sig/ec/pg.rbs +6 -0
- metadata +160 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ec
|
|
4
|
+
module Pg
|
|
5
|
+
# Mixin for ActiveRecord models that participate in multi-tenancy.
|
|
6
|
+
#
|
|
7
|
+
# Include via +acts_as_rls+ in your model or add it to ApplicationRecord.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
12
|
+
# include Ec::Pg::RlsMixin
|
|
13
|
+
# acts_as_rls
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# == What it does
|
|
17
|
+
#
|
|
18
|
+
# * Adds an +around_action+-compatible callback that applies RLS before
|
|
19
|
+
# every query on the model when a tenant is active in the thread context.
|
|
20
|
+
# * Provides a +with_rls+ class method for explicit scoping.
|
|
21
|
+
# * Optionally raises TenantNotSet when required: true and no tenant is set.
|
|
22
|
+
#
|
|
23
|
+
module RlsMixin
|
|
24
|
+
class RlsError < StandardError; end
|
|
25
|
+
|
|
26
|
+
extend ActiveSupport::Concern
|
|
27
|
+
RlsModes = %i(local session)
|
|
28
|
+
|
|
29
|
+
included do
|
|
30
|
+
class_attribute :is_rls, default: false
|
|
31
|
+
class_attribute :registered_variables, default: {}
|
|
32
|
+
class_attribute :rls_mode, default: :local
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class_methods do
|
|
36
|
+
def acts_as_rls(mode: nil, variables: {})
|
|
37
|
+
validate_variables!(variables)
|
|
38
|
+
|
|
39
|
+
self.is_rls = true
|
|
40
|
+
self.rls_mode = mode || Ec::Pg.configuration.rls_mode || :local
|
|
41
|
+
self.registered_variables = variables
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Executes +block+ with the AR connection switched to +shard_name+.
|
|
45
|
+
#
|
|
46
|
+
# @param shard_name [Symbol] the registered shard key
|
|
47
|
+
# @param role [Symbol] :writing (default) or :reading
|
|
48
|
+
# @param klass [Class] the AR base class whose connection pool to switch
|
|
49
|
+
# (default: ActiveRecord::Base)
|
|
50
|
+
# @yield block to run in shard context
|
|
51
|
+
# @return the return value of +block+
|
|
52
|
+
def with_rls(variables: {}, &block)
|
|
53
|
+
RlsManager.with_rls(
|
|
54
|
+
rls_mode: self.rls_mode,
|
|
55
|
+
registered_variables: self.registered_variables,
|
|
56
|
+
variables: variables,
|
|
57
|
+
connection: self.connection,
|
|
58
|
+
&block
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear!
|
|
63
|
+
self.registered_variables = {}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
# Prevents variable name injection. Only allows dot-separated identifiers.
|
|
68
|
+
ValidVariablesRegexp = /\A[a-zA-Z_][a-zA-Z0-9_.]*\z/
|
|
69
|
+
|
|
70
|
+
def validate_variables!(variables)
|
|
71
|
+
variables.each do |key, variable|
|
|
72
|
+
unless ValidVariablesRegexp.match?(variable)
|
|
73
|
+
raise RlsError, "Invalid RLS variable name: #{key}: #{variable.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ec
|
|
4
|
+
module Pg
|
|
5
|
+
# Manages PostgreSQL schema switching via the +search_path+ session variable.
|
|
6
|
+
#
|
|
7
|
+
# When you have one Postgres database with a schema-per-tenant layout:
|
|
8
|
+
#
|
|
9
|
+
# CREATE SCHEMA tenant_abc;
|
|
10
|
+
# CREATE TABLE tenant_abc.records (...);
|
|
11
|
+
#
|
|
12
|
+
# This manager updates the connection's search_path so that ActiveRecord's
|
|
13
|
+
# unqualified table references resolve to the correct tenant schema.
|
|
14
|
+
#
|
|
15
|
+
# == Usage
|
|
16
|
+
#
|
|
17
|
+
# SchemaManager.with_schema("tenant_abc") { Record.all }
|
|
18
|
+
#
|
|
19
|
+
# The search_path is reset to the previous value (or the configured default)
|
|
20
|
+
# when the block exits, even if an exception is raised.
|
|
21
|
+
#
|
|
22
|
+
# == Thread safety
|
|
23
|
+
#
|
|
24
|
+
# +with_schema+ stores the schema name in the thread context AND updates the
|
|
25
|
+
# underlying database connection. When using connection pooling, each thread
|
|
26
|
+
# checks out its own connection, so search_path changes are thread-safe.
|
|
27
|
+
#
|
|
28
|
+
module SchemaManager
|
|
29
|
+
class InvalidSchema < StandardError; end
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Executes +block+ with the Postgres search_path set to +schema_name+,
|
|
34
|
+
# followed by any +shared_schemas+ from the configuration.
|
|
35
|
+
#
|
|
36
|
+
# @param schema_name [String] tenant schema (e.g. "tenant_abc")
|
|
37
|
+
# @yield block to execute within the schema context
|
|
38
|
+
# @return the return value of the block
|
|
39
|
+
def with_schema(schema_name, &block)
|
|
40
|
+
schema_name = schema_name.to_s.strip
|
|
41
|
+
validate_schema_name!(schema_name)
|
|
42
|
+
|
|
43
|
+
Context.with(schema: schema_name) do
|
|
44
|
+
apply_schema(schema_name)
|
|
45
|
+
block.call
|
|
46
|
+
end
|
|
47
|
+
ensure
|
|
48
|
+
# Restore previous schema on the same connection if we changed it.
|
|
49
|
+
# Guard against the case where the connection was already returned to
|
|
50
|
+
# the pool during the block.
|
|
51
|
+
restore_schema
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the schema currently active in thread context.
|
|
55
|
+
def current_schema
|
|
56
|
+
Context.schema || Ec::Pg.configuration.default_schema
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns true if a non-default schema is active.
|
|
60
|
+
def active?
|
|
61
|
+
!Context.schema.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Applies the search_path on an existing connection without block scoping.
|
|
65
|
+
# Useful in middleware or Rack apps that manage connection lifecycle manually.
|
|
66
|
+
def apply!(schema_name)
|
|
67
|
+
schema_name = schema_name.to_s.strip
|
|
68
|
+
validate_schema_name!(schema_name)
|
|
69
|
+
|
|
70
|
+
Context.set(schema: schema_name)
|
|
71
|
+
apply_schema(schema_name)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Resets search_path to the configured default schema on +connection+.
|
|
75
|
+
def reset!
|
|
76
|
+
Context.delete(:schema)
|
|
77
|
+
restore_schema
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Builds the search_path string and executes SET search_path on the connection.
|
|
83
|
+
def apply_schema(schema_name)
|
|
84
|
+
registered_connections.each do |connection|
|
|
85
|
+
shared = Ec::Pg.configuration.shared_schemas
|
|
86
|
+
path = ([schema_name] + shared).uniq.join(", ")
|
|
87
|
+
connection.schema_search_path = path
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Restores the search_path to the configured default schema(s).
|
|
92
|
+
def restore_schema
|
|
93
|
+
default = Ec::Pg.configuration.default_schema
|
|
94
|
+
shared = Ec::Pg.configuration.shared_schemas
|
|
95
|
+
path = ([default] + shared).uniq.join(", ")
|
|
96
|
+
|
|
97
|
+
registered_connections.each do |connection|
|
|
98
|
+
if connection && connection.pool.present?
|
|
99
|
+
connection.schema_search_path = path
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
rescue StandardError
|
|
103
|
+
# Swallow errors during cleanup (connection may have been returned to pool)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def registered_connections
|
|
107
|
+
Ec::Pg::SchemaMixin
|
|
108
|
+
.registered_models
|
|
109
|
+
.map(&:connection)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Guards against SQL injection via schema names.
|
|
113
|
+
ValidSchemaRegex = /\A[a-zA-Z][a-zA-Z0-9_$]*\z/
|
|
114
|
+
|
|
115
|
+
def validate_schema_name!(name)
|
|
116
|
+
return if ValidSchemaRegex.match?(name)
|
|
117
|
+
|
|
118
|
+
raise InvalidSchema,
|
|
119
|
+
"Invalid PostgreSQL schema name: #{name.inspect}. " \
|
|
120
|
+
"Only alphanumeric characters and underscores are allowed."
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
module_function :apply_schema, :restore_schema, :validate_schema_name!, :registered_connections
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ec
|
|
4
|
+
module Pg
|
|
5
|
+
# Mixin for ActiveRecord models that participate in multi-tenancy.
|
|
6
|
+
#
|
|
7
|
+
# Include via +acts_as_sharded+ in your model or add it to ApplicationRecord.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
12
|
+
# include Ec::Pg::SchemaMixin
|
|
13
|
+
# acts_as_namespaced
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# == What it does
|
|
17
|
+
#
|
|
18
|
+
# * Adds an +around_action+-compatible callback that applies RLS before
|
|
19
|
+
# every query on the model when a tenant is active in the thread context.
|
|
20
|
+
# * Provides a +with_tenant+ class method for explicit scoping.
|
|
21
|
+
# * Optionally raises TenantNotSet when required: true and no tenant is set.
|
|
22
|
+
#
|
|
23
|
+
module SchemaMixin
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
mattr_accessor :registered_models, default: []
|
|
26
|
+
included do
|
|
27
|
+
class_attribute :is_namespaced, default: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class_methods do
|
|
31
|
+
def acts_as_namespaced
|
|
32
|
+
self.is_namespaced = true
|
|
33
|
+
|
|
34
|
+
register(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def registered_models
|
|
38
|
+
Ec::Pg::SchemaMixin.registered_models
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear!
|
|
42
|
+
Ec::Pg::SchemaMixin.registered_models = []
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
def register(klass)
|
|
47
|
+
Ec::Pg::SchemaMixin.registered_models += [klass]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module Ec
|
|
3
|
+
module Pg
|
|
4
|
+
# Manages ActiveRecord database shard switching.
|
|
5
|
+
#
|
|
6
|
+
# Leverages ActiveRecord's native multi-database support (AR 6.1+) via
|
|
7
|
+
# +connected_to(shard:)+. Falls back to manual connection swapping for
|
|
8
|
+
# connection configurations that are not registered as named shards.
|
|
9
|
+
#
|
|
10
|
+
# == Configuration in database.yml (Rails multi-db style)
|
|
11
|
+
#
|
|
12
|
+
# production:
|
|
13
|
+
# primary:
|
|
14
|
+
# <<: *default
|
|
15
|
+
# database: app_primary
|
|
16
|
+
# db_sharded_shard_1:
|
|
17
|
+
# <<: *default
|
|
18
|
+
# database: app_shard_one
|
|
19
|
+
# migrations_paths: db/migrate_shards
|
|
20
|
+
#
|
|
21
|
+
# == Usage
|
|
22
|
+
#
|
|
23
|
+
# ShardManager.with_shard(:shard_one) { User.all }
|
|
24
|
+
# ShardManager.with_shard(:shard_one, role: :reading) { User.all }
|
|
25
|
+
#
|
|
26
|
+
module ShardManager
|
|
27
|
+
class ShardNotFound < StandardError; end
|
|
28
|
+
class UnsupportedActiveRecordVersion < StandardError; end
|
|
29
|
+
|
|
30
|
+
MinimumARVersion = Gem::Version.new("7.1")
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Executes +block+ with the AR connection switched to +shard_name+.
|
|
35
|
+
#
|
|
36
|
+
# @param shard_name [Symbol] the registered shard key
|
|
37
|
+
# @param role [Symbol] :writing (default) or :reading
|
|
38
|
+
# @param klass [Class] the AR base class whose connection pool to switch
|
|
39
|
+
# (default: ActiveRecord::Base)
|
|
40
|
+
# @yield block to run in shard context
|
|
41
|
+
# @return the return value of +block+
|
|
42
|
+
def with_shard(shard_name, role: :writing, klass: ActiveRecord::Base, &block)
|
|
43
|
+
shard_name = shard_name.to_sym
|
|
44
|
+
assert_ar_version!
|
|
45
|
+
|
|
46
|
+
Context.with(shard: shard_name) do
|
|
47
|
+
klass.prohibit_shard_swapping(true) do
|
|
48
|
+
klass.connected_to(shard: shard_name, role: role, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
rescue ActiveRecord::ConnectionNotEstablished => e
|
|
52
|
+
raise ShardNotFound,
|
|
53
|
+
"Could not connect to shard #{shard_name.inspect}. " \
|
|
54
|
+
"Make sure it is registered via connects_to. Original error: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns true when the thread is operating inside a +with_shard+ block
|
|
58
|
+
# (or when the shard was set via thread-local assignment).
|
|
59
|
+
def active?
|
|
60
|
+
!Context.shard.nil?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the shard currently set in thread context, or +:default+ when none
|
|
64
|
+
# is set (mirrors how AR treats the default shard).
|
|
65
|
+
def current_shard
|
|
66
|
+
Context.shard || :default
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns an array of shard names registered on +klass+.
|
|
70
|
+
# def registered_shards(klass: ActiveRecord::Base)
|
|
71
|
+
# klass
|
|
72
|
+
# .connection_handler
|
|
73
|
+
# .connection_pools
|
|
74
|
+
# .map(&:db_config)
|
|
75
|
+
# .map(&:name)
|
|
76
|
+
# rescue StandardError
|
|
77
|
+
# []
|
|
78
|
+
# end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def assert_ar_version!
|
|
83
|
+
ar_version = Gem::Version.new(ActiveRecord.version.to_s)
|
|
84
|
+
return if ar_version >= MinimumARVersion
|
|
85
|
+
|
|
86
|
+
raise UnsupportedActiveRecordVersion,
|
|
87
|
+
"activerecord-multi-tenant shard switching requires ActiveRecord >= 6.1. " \
|
|
88
|
+
"Current version: #{ar_version}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
module_function :assert_ar_version!
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ec
|
|
4
|
+
module Pg
|
|
5
|
+
# Mixin for ActiveRecord models that participate in multi-tenancy.
|
|
6
|
+
#
|
|
7
|
+
# Include via +acts_as_sharded+ in your model or add it to ApplicationRecord.
|
|
8
|
+
#
|
|
9
|
+
# == Usage
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationRecord < ActiveRecord::Base
|
|
12
|
+
# include Ec::Pg::ShardMixin
|
|
13
|
+
# acts_as_sharded
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# == What it does
|
|
17
|
+
#
|
|
18
|
+
# * Adds an +around_action+-compatible callback that applies RLS before
|
|
19
|
+
# every query on the model when a tenant is active in the thread context.
|
|
20
|
+
# * Provides a +with_tenant+ class method for explicit scoping.
|
|
21
|
+
# * Optionally raises TenantNotSet when required: true and no tenant is set.
|
|
22
|
+
#
|
|
23
|
+
module ShardMixin
|
|
24
|
+
extend ActiveSupport::Concern
|
|
25
|
+
|
|
26
|
+
included do
|
|
27
|
+
class_attribute :is_sharded, default: false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class_methods do
|
|
31
|
+
# Declares this model as sharded
|
|
32
|
+
#
|
|
33
|
+
# @param mode [Symbol] :solo, or :sharded
|
|
34
|
+
# @param writing_database_identifier [Symbol or String] database used for writing
|
|
35
|
+
# @param reading_database_identifier [Symbol or String] database used for reading (optional)
|
|
36
|
+
def acts_as_sharded(mode:, writing_database_identifier:, reading_database_identifier: nil)
|
|
37
|
+
class_attribute :tenant_mode, default: mode
|
|
38
|
+
class_attribute :writing_database_identifier, default: writing_database_identifier
|
|
39
|
+
class_attribute :reading_database_identifier, default: reading_database_identifier
|
|
40
|
+
|
|
41
|
+
# include InstanceMethods
|
|
42
|
+
# extend QueryMethods
|
|
43
|
+
|
|
44
|
+
self.is_sharded = true
|
|
45
|
+
self.abstract_class = true
|
|
46
|
+
self.connects_to(shards: shards_hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
def shards_hash
|
|
51
|
+
{}.tap do |hash|
|
|
52
|
+
Ec::Pg.configuration.number_of_shards.times do |index|
|
|
53
|
+
shard_key = "shard_#{index + 1}"
|
|
54
|
+
hash[shard_key] = {
|
|
55
|
+
writing: database_identifier_for(writing_database_identifier, index),
|
|
56
|
+
reading: database_identifier_for(reading_database_identifier, index)
|
|
57
|
+
}.compact
|
|
58
|
+
end
|
|
59
|
+
end.symbolize_keys
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def database_identifier_for(database_identifier, index)
|
|
63
|
+
return unless database_identifier.present?
|
|
64
|
+
|
|
65
|
+
if tenant_mode.to_sym == :solo
|
|
66
|
+
database_identifier
|
|
67
|
+
else
|
|
68
|
+
[database_identifier, "shard", index + 1].join('_')
|
|
69
|
+
end.to_sym
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ec
|
|
4
|
+
module Pg
|
|
5
|
+
module TenantContext
|
|
6
|
+
# Raised when require_tenant is true and a query is attempted without a tenant.
|
|
7
|
+
class TenantNotSet < StandardError; end
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# -------------------------------------------------------------------------
|
|
12
|
+
# Block-scoped (preferred)
|
|
13
|
+
# -------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
# Executes +block+ within the given tenant context.
|
|
16
|
+
#
|
|
17
|
+
# @param shard [Symbol, nil] shard to connect to (skipped when nil)
|
|
18
|
+
# @param schema [String, nil] Postgres schema to set (skipped when nil)
|
|
19
|
+
def switch(shard: nil, schema: nil, &block)
|
|
20
|
+
|
|
21
|
+
# Layer composition: innermost layer first so that the outermost wrapper
|
|
22
|
+
# controls the connection that the inner layers receive.
|
|
23
|
+
|
|
24
|
+
run = block
|
|
25
|
+
|
|
26
|
+
# 1. Schema (innermost)
|
|
27
|
+
if schema
|
|
28
|
+
inner_run = run
|
|
29
|
+
run = -> {SchemaManager.with_schema(schema, &inner_run)}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 2. Shard (outermost — switches the connection first)
|
|
33
|
+
if shard
|
|
34
|
+
inner_run = run
|
|
35
|
+
run = -> {ShardManager.with_shard(shard, &inner_run)}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if run.present?
|
|
39
|
+
run.call
|
|
40
|
+
else
|
|
41
|
+
raise TenantNotSet, "No schema or shard specified"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Convenience alias that shard and only switches schema.
|
|
46
|
+
def with_schema(schema_name, &block)
|
|
47
|
+
SchemaManager.with_schema(schema_name, &block)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convenience alias that skips schema and only switches shard.
|
|
51
|
+
def with_shard(shard_name, role: :writing, &block)
|
|
52
|
+
ShardManager.with_shard(shard_name, role: role, &block)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# -------------------------------------------------------------------------
|
|
56
|
+
# Thread-local (stateful)
|
|
57
|
+
# -------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def current_shard
|
|
60
|
+
ShardManager.current_shard
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def current_schema
|
|
64
|
+
SchemaManager.current_schema
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Clears all multi-tenant context for the current thread.
|
|
68
|
+
def clear_context!
|
|
69
|
+
Context.clear!
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/ec/pg.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
require_relative "pg/version"
|
|
6
|
+
require_relative "pg/context"
|
|
7
|
+
require_relative "pg/tenant_context"
|
|
8
|
+
require_relative "pg/configuration"
|
|
9
|
+
|
|
10
|
+
if ENV['RAILS_ENV'] == 'test'
|
|
11
|
+
require 'debug'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
if defined?(Rails::Railtie)
|
|
15
|
+
require_relative "pg/railtie"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
I18n.load_path += Dir.glob( File.expand_path("../../../config/locales/*.yml", __FILE__) )
|
|
19
|
+
|
|
20
|
+
module Ec
|
|
21
|
+
module Pg
|
|
22
|
+
class Error < StandardError; end
|
|
23
|
+
class InvalidType < Error; end
|
|
24
|
+
|
|
25
|
+
autoload :SchemaMixin, 'ec/pg/schema_mixin.rb'
|
|
26
|
+
autoload :SchemaManager, 'ec/pg/schema_manager.rb'
|
|
27
|
+
autoload :ShardMixin, 'ec/pg/shard_mixin.rb'
|
|
28
|
+
autoload :ShardManager, 'ec/pg/shard_manager.rb'
|
|
29
|
+
autoload :RlsMixin, 'ec/pg/rls_mixin.rb'
|
|
30
|
+
autoload :RlsManager, 'ec/pg/rls_manager.rb'
|
|
31
|
+
autoload :ContextSwitcher, 'ec/pg/middleware/context_switcher.rb'
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
def configure
|
|
36
|
+
yield configuration
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def configuration
|
|
40
|
+
@configuration ||= Ec::Pg::Configuration.new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def switch(shard: nil, schema: nil, &block)
|
|
44
|
+
TenantContext.switch(shard: shard, schema: schema, &block)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def current_shard = TenantContext.current_shard
|
|
48
|
+
def current_schema = TenantContext.current_schema
|
|
49
|
+
|
|
50
|
+
# Clears all thread-local context.
|
|
51
|
+
def clear_context! = TenantContext.clear_context!
|
|
52
|
+
end
|
|
53
|
+
end
|