ec-pg 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64cd8465d41750ff6dfefa8f4e9d7a5c05ecb90c65f657ac2d0670f700e5d516
4
- data.tar.gz: abba235476bd2b3e624f0ba13a9c4bbe63fce55f7c410ae69b0537de0b6a0d08
3
+ metadata.gz: 615463774272fc429a2f2dfe75db88f133cdd4507b5d5d94147ff84da214a220
4
+ data.tar.gz: f23f77f305c261f719a37a79d9728aba485a7b214332b08abc87abcab2330421
5
5
  SHA512:
6
- metadata.gz: 7a2a8e48e04b460014c0a1603fcad4117d319dbecc53680f4c5238c342c57b087332849db7814c7dfff483ef11a2b7ae6e91179b2deec1ecdd8e37bf093d6c87
7
- data.tar.gz: d2c51f56fd6b3c69406ff3e363ec5f35c9344c8d96dedfd4eeb55303b70a907b3128a167ef5c0c510cc3bb199b523119547c4c54e779d9cdeea94209f60b6252
6
+ metadata.gz: 27e2de6ff3980f2ff75160147fcfc87dd1ad2b5f4f3965bde1601883c23bfa637fc0c38ce93d6b7a3b4f2d537eb8835252026781e162a7324e3951f2e216ae22
7
+ data.tar.gz: f8135d52f5fd32e97a6e992b6930b0a19b14a27d3e926d6ef03ae19df960baf72ca54557c8d8e5dad0184b6f4360b69cae9be961cac084bc452558362fb7e35f
data/README.md CHANGED
@@ -201,6 +201,97 @@ Ec::Pg.current_schema # => 'acme'
201
201
 
202
202
  ---
203
203
 
204
+ ## Schema lifecycle (create / drop)
205
+
206
+ `SchemaRegistry` handles PostgreSQL schema DDL — separate from `SchemaManager` which only handles `search_path` switching.
207
+
208
+ ```ruby
209
+ # Create a schema (raises SchemaAlreadyExists if it exists)
210
+ Ec::Pg::SchemaRegistry.create!("tenant_abc")
211
+
212
+ # Drop a schema
213
+ Ec::Pg::SchemaRegistry.drop!("tenant_abc")
214
+ Ec::Pg::SchemaRegistry.drop!("tenant_abc", cascade: true) # drop with all objects
215
+
216
+ # Check existence
217
+ Ec::Pg::SchemaRegistry.exists?("tenant_abc") # => true / false
218
+
219
+ # List all non-system schemas
220
+ Ec::Pg::SchemaRegistry.all # => ["public", "tenant_abc", ...]
221
+ ```
222
+
223
+ ### Rake tasks
224
+
225
+ ```bash
226
+ rake 'ec_pg:schema:create[tenant_abc]'
227
+ rake 'ec_pg:schema:drop[tenant_abc]'
228
+ rake 'ec_pg:schema:drop[tenant_abc,cascade]'
229
+ rake ec_pg:schema:list
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Migrations
235
+
236
+ `Migrator` runs ActiveRecord migrations inside a specific schema or shard context. Schema migrations track `schema_migrations` in the tenant schema (not the public schema), so tenants can be at different versions.
237
+
238
+ ### Schema migrations
239
+
240
+ ```ruby
241
+ # Migrate to latest
242
+ Ec::Pg::Migrator.migrate_schema("tenant_abc")
243
+
244
+ # Migrate to a specific version
245
+ Ec::Pg::Migrator.migrate_schema("tenant_abc", version: 20240101120000)
246
+
247
+ # Migrate a list of schemas
248
+ Ec::Pg::Migrator.migrate_each_schema(["tenant_a", "tenant_b"])
249
+
250
+ # Roll back
251
+ Ec::Pg::Migrator.rollback_schema("tenant_abc") # 1 step
252
+ Ec::Pg::Migrator.rollback_schema("tenant_abc", steps: 3)
253
+ ```
254
+
255
+ ### Shard migrations
256
+
257
+ ```ruby
258
+ # Migrate a single shard
259
+ Ec::Pg::Migrator.migrate_shard(:shard_one)
260
+
261
+ # Migrate all shards (derives :shard_1..:shard_N from configuration.number_of_shards)
262
+ Ec::Pg::Migrator.migrate_each_shard
263
+
264
+ # Migrate an explicit list of shards
265
+ Ec::Pg::Migrator.migrate_each_shard([:shard_one, :shard_two])
266
+
267
+ # Roll back
268
+ Ec::Pg::Migrator.rollback_shard(:shard_one)
269
+ Ec::Pg::Migrator.rollback_shard(:shard_one, steps: 2)
270
+ ```
271
+
272
+ Custom migration paths can be passed to any method via `paths:`:
273
+
274
+ ```ruby
275
+ Ec::Pg::Migrator.migrate_schema("tenant_abc", paths: ["db/tenant_migrate"])
276
+ ```
277
+
278
+ ### Rake tasks
279
+
280
+ ```bash
281
+ rake 'ec_pg:migrate:schema[tenant_abc]'
282
+ rake 'ec_pg:migrate:schemas[tenant_a,tenant_b]'
283
+ rake 'ec_pg:migrate:rollback_schema[tenant_abc]'
284
+ rake 'ec_pg:migrate:rollback_schema[tenant_abc,3]'
285
+
286
+ rake 'ec_pg:migrate:shard[shard_one]'
287
+ rake ec_pg:migrate:shards
288
+ rake 'ec_pg:migrate:shards[shard_one,shard_two]'
289
+ rake 'ec_pg:migrate:rollback_shard[shard_one]'
290
+ rake 'ec_pg:migrate:rollback_shard[shard_one,2]'
291
+ ```
292
+
293
+ ---
294
+
204
295
  ## Rack middleware
205
296
 
206
297
  Add `ContextSwitcher` to automatically resolve tenant context from each request:
data/Rakefile CHANGED
@@ -6,3 +6,8 @@ require "rspec/core/rake_task"
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  task default: :spec
9
+
10
+ path = File.expand_path(__dir__)
11
+ Dir.glob("#{path}/lib/tasks/**/*.rake").each do |f|
12
+ load f
13
+ end
@@ -10,6 +10,7 @@ module Ec
10
10
  attr_reader :number_of_shards
11
11
  attr_reader :get_context_method
12
12
  attr_accessor :context_switch_exclude_paths
13
+ attr_accessor :pg_excluded_names
13
14
 
14
15
  def initialize
15
16
  self.logger = Logger.new($stdout)
@@ -17,6 +18,7 @@ module Ec
17
18
  self.number_of_shards = 1
18
19
  self.rls_mode = :local
19
20
  self.context_switch_exclude_paths = []
21
+ self.pg_excluded_names = []
20
22
  end
21
23
 
22
24
  def number_of_shards=(value)
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Pg
5
+ # Runs ActiveRecord migrations in the context of a specific schema or shard.
6
+ #
7
+ # == Schema migrations
8
+ #
9
+ # Sets search_path to the target schema before running migrations so that
10
+ # Rails creates (and tracks via schema_migrations) tables inside that schema.
11
+ #
12
+ # Migrator.migrate_schema("tenant_abc")
13
+ # Migrator.migrate_schema("tenant_abc", version: 20240101120000)
14
+ # Migrator.rollback_schema("tenant_abc", steps: 2)
15
+ #
16
+ # Migrator.migrate_each_schema(["tenant_a", "tenant_b"])
17
+ #
18
+ # == Shard migrations
19
+ #
20
+ # Connects to the target shard before running migrations.
21
+ #
22
+ # Migrator.migrate_shard(:shard_one)
23
+ # Migrator.rollback_shard(:shard_one, steps: 1)
24
+ #
25
+ # # Migrate all shards derived from configuration.number_of_shards
26
+ # Migrator.migrate_each_shard
27
+ #
28
+ # # Or supply an explicit list
29
+ # Migrator.migrate_each_shard([:shard_one, :shard_two])
30
+ #
31
+ class Migrator
32
+ class << self
33
+ # Runs pending migrations with search_path set to +schema_name+.
34
+ #
35
+ # @param schema_name [String]
36
+ # @param paths [Array<String>] migration paths (defaults to Rails configured paths)
37
+ # @param version [Integer, nil] target version; migrates to latest when nil
38
+ def migrate_schema(connection, schema_name, paths: nil, version: nil)
39
+ Ec::Pg.dprint("Running migration for: #{schema_name}".red)
40
+ connection.schema_search_path = schema_name
41
+ run_migrations(connection, paths: paths, version: version)
42
+ end
43
+
44
+ def migrate_all_schemas
45
+ Ec::Pg::SchemaMixin.registered_models.each do |model|
46
+ model.schemas.each do |shard, schemas|
47
+ Ec::Pg.switch(shard: shard) do
48
+ schemas.each do |schema|
49
+ Ec::Pg.dprint("migrating model: #{model}, shard: #{shard}, schema: #{schema}")
50
+ migrate_schema(model.connection, schema)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # Calls {migrate_schema} for each name in +schema_names+.
58
+ #
59
+ # @param schema_names [Array<String>]
60
+ def migrate_each_schema(connection, schema_names, paths: nil, version: nil)
61
+ schema_names.each { |name| migrate_schema(connection, name, paths: paths, version: version) }
62
+ end
63
+
64
+ # Rolls back +steps+ migrations with search_path set to +schema_name+.
65
+ #
66
+ # @param schema_name [String]
67
+ # @param steps [Integer] number of migrations to roll back (default 1)
68
+ def rollback_schema(schema_name, steps: 1, paths: nil)
69
+ SchemaManager.with_schema(schema_name) do
70
+ build_context(paths).rollback(steps)
71
+ end
72
+ end
73
+
74
+ # Runs pending migrations connected to +shard_name+.
75
+ #
76
+ # @param shard_name [Symbol]
77
+ # @param paths [Array<String>]
78
+ # @param version [Integer, nil]
79
+ def migrate_shard(shard_name, paths: nil, version: nil)
80
+ ShardManager.with_shard(shard_name) do
81
+ run_migrations(paths: paths, version: version)
82
+ end
83
+ end
84
+
85
+ # Calls {migrate_shard} for each name in +shard_names+.
86
+ # When +shard_names+ is omitted, derives +:shard_1+ through +:shard_N+
87
+ # from +configuration.number_of_shards+.
88
+ #
89
+ # @param shard_names [Array<Symbol>, nil]
90
+ def migrate_each_shard(shard_names = nil, paths: nil, version: nil)
91
+ (shard_names || default_shards).each do |name|
92
+ migrate_shard(name, paths: paths, version: version)
93
+ end
94
+ end
95
+
96
+ # Rolls back +steps+ migrations connected to +shard_name+.
97
+ #
98
+ # @param shard_name [Symbol]
99
+ # @param steps [Integer]
100
+ def rollback_shard(shard_name, steps: 1, paths: nil)
101
+ ShardManager.with_shard(shard_name) do
102
+ build_context(paths).rollback(steps)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def run_migrations(connection, paths: nil, version: nil)
109
+ Ec::Pg.dprint("migrations_path: #{paths}")
110
+
111
+ ActiveRecord::Base.establish_connection(connection.pool.db_config)
112
+ connection.pool.migration_context.migrate
113
+ end
114
+
115
+ def default_shards
116
+ n = Ec::Pg.configuration.number_of_shards
117
+ (1..n).map { |i| :"shard_#{i}" }
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Pg
5
+ module Mixin
6
+ extend ActiveSupport::Concern
7
+
8
+ include SchemaMixin
9
+ include ShardMixin
10
+ include RlsMixin
11
+ include SchemaRegistryMixin
12
+ end
13
+ end
14
+ end
data/lib/ec/pg/railtie.rb CHANGED
@@ -28,10 +28,15 @@ module Ec
28
28
  #
29
29
  # rake_tasks do
30
30
  # load 'tasks/ec-pg-ar.rake'
31
- # if DH::PG.configuration.db_migrate_tenants
31
+ # if Ec::Pg.configuration.db_migrate_tenants
32
32
  # require_relative '../support/migration_tasks_enhancer'
33
33
  # end
34
34
  # end
35
+
36
+ rake_tasks do
37
+ load File.expand_path("../../tasks/ec_pg_migrate.rake", __dir__)
38
+ load File.expand_path("../../tasks/ec_pg_schema.rake", __dir__)
39
+ end
35
40
  end
36
41
  end
37
42
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Pg
5
+ module SchemaRegistry
6
+ class Cloner < Struct.new(:connection, :schema, keyword_init: true)
7
+ include Mixin
8
+
9
+ PgDumpBlacklistedStatements = [
10
+ /SET search_path/i,
11
+ /SET lock_timeout/i,
12
+ /SET row_security/i,
13
+ /SET idle_in_transaction_session_timeout/i,
14
+ /CREATE SCHEMA #{Ec::Pg.configuration.default_schema}/i,
15
+ /COMMENT ON SCHEMA #{Ec::Pg.configuration.default_schema}/i
16
+ ].freeze
17
+
18
+ def call
19
+ connection.execute(
20
+ patched_search_path(default_schema_dump)
21
+ )
22
+ end
23
+
24
+ def patched_search_path(sql)
25
+ search_path = %[SET search_path = "#{schema}", #{Ec::Pg.configuration.default_schema};]
26
+
27
+ swap_schema_qualifier(sql)
28
+ .split("\n")
29
+ .reject{|line| line =~ /\\(un)?restrict/}
30
+ .reject{|line| matches_blacklist?(line, PgDumpBlacklistedStatements).present?}
31
+ .prepend(search_path)
32
+ .join("\n")
33
+ end
34
+
35
+ def default_schema_dump
36
+ # TODO: check exit code of pg_dump
37
+ with_pg_env {
38
+ shell_command(
39
+ %(pg_dump -s -x -O -n #{Ec::Pg.configuration.default_schema} #{configurations[:database]})
40
+ )
41
+ }
42
+ end
43
+
44
+ def swap_schema_qualifier(sql)
45
+ sql.gsub(/#{Ec::Pg.configuration.default_schema}\.\w*/) do |match|
46
+ if Ec::Pg.configuration.pg_excluded_names.any? {|name| match.include? name}
47
+ match
48
+ else
49
+ match.gsub("#{Ec::Pg.configuration.default_schema}.", %("#{schema}".))
50
+ end
51
+ end
52
+ end
53
+
54
+ def matches_blacklist?(input, regexps)
55
+ regexps.select {|c| input.match(c)}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Pg
5
+ module SchemaRegistry
6
+ module Mixin
7
+ extend ActiveSupport::Concern
8
+
9
+ def with_pg_env(&block)
10
+ pghost = ENV['PGHOST']
11
+ pgport = ENV['PGPORT']
12
+ pguser = ENV['PGUSER']
13
+ pgpassword = ENV['PGPASSWORD']
14
+
15
+ ENV['PGUSER'] = configurations[:superusername] if configurations[:superusername]
16
+ ENV['PGHOST'] = configurations[:host] if configurations[:host]
17
+ ENV['PGPORT'] = configurations[:port].to_s if configurations[:port]
18
+ ENV['PGPASSWORD'] = configurations[:password].to_s if configurations[:password]
19
+
20
+ block.call
21
+ ensure
22
+ ENV['PGHOST'] = pghost
23
+ ENV['PGPORT'] = pgport
24
+ ENV['PGUSER'] = pguser
25
+ ENV['PGPASSWORD'] = pgpassword
26
+ end
27
+
28
+ def shell_command(command)
29
+ puts command
30
+ %x(#{command})
31
+ end
32
+
33
+ def configurations
34
+ @configurations ||= connection.pool.db_config.configuration_hash
35
+ end
36
+
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ec
4
+ module Pg
5
+ module SchemaRegistry
6
+ class SchemaMigrationCloner < Cloner
7
+ def default_schema_dump
8
+ with_pg_env {
9
+ shell_command(
10
+ %(
11
+ pg_dump -a --inserts -t #{Ec::Pg.configuration.default_schema}.schema_migrations -t #{Ec::Pg.configuration.default_schema}.ar_internal_metadata #{configurations[:database]}
12
+ )
13
+ )
14
+ }
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema_registry/mixin'
4
+ require_relative 'schema_registry/cloner'
5
+ require_relative 'schema_registry/schema_migration_cloner'
6
+
7
+ module Ec
8
+ module Pg
9
+ module SchemaRegistryMixin
10
+ extend ActiveSupport::Concern
11
+ include SchemaRegistry::Mixin
12
+
13
+ class SchemaAlreadyExists < StandardError; end
14
+ class SchemaNotFound < StandardError; end
15
+
16
+ class_methods do
17
+ def create_schema!(shard: nil, schema: schema)
18
+ Ec::Pg.switch(shard: shard) do
19
+ validate_schema_name!(schema)
20
+
21
+ if connection.schema_exists?(schema)
22
+ raise SchemaAlreadyExists, "Cannot create existing schema '#{schema}'"
23
+ end
24
+ connection.execute(%{CREATE SCHEMA "#{schema}"})
25
+
26
+ Ec::Pg.switch(schema: schema) do
27
+ SchemaRegistry::Cloner.new(connection: connection, schema: schema).call
28
+ SchemaRegistry::SchemaMigrationCloner.new(connection: connection, schema: schema).call
29
+ end
30
+
31
+ rescue ::ActiveRecord::ActiveRecordError, PG::Error => exception
32
+ raise SchemaAlreadyExists, "Error while creating tenant #{tenant}: #{ exception.message }"
33
+ end
34
+ end
35
+
36
+ def drop_schema!(shard: nil, schema: schema)
37
+ Ec::Pg.switch(shard: shard) do
38
+ validate_schema_name!(schema)
39
+
40
+ if connection.schema_exists?(schema)
41
+ connection.execute(%{DROP SCHEMA "#{schema}" CASCADE})
42
+ end
43
+ end
44
+
45
+ rescue ::ActiveRecord::ActiveRecordError, PG::Error => exception
46
+ raise SchemaNotFound, "Error while dropping schema #{schema}: #{exception.message}"
47
+ end
48
+
49
+ # Returns all non-system PostgreSQL schema names, sorted alphabetically.
50
+ #
51
+ # @return {'shard_1': ['schema1', 'schema2']}
52
+ def schemas
53
+ {}.tap do |hash|
54
+ Ec::Pg.each_shard do |shard|
55
+ hash[shard] = connection.execute(<<~SQL).column_values(0)
56
+ SELECT schema_name
57
+ FROM information_schema.schemata
58
+ WHERE schema_name NOT IN ('information_schema')
59
+ AND schema_name NOT LIKE 'pg_%'
60
+ ORDER BY schema_name
61
+ SQL
62
+ end
63
+ end
64
+ end
65
+
66
+ def validate_schema_name!(schema_name)
67
+ SchemaManager.validate_schema_name!(schema_name)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -44,8 +44,8 @@ module Ec
44
44
  assert_ar_version!
45
45
 
46
46
  Context.with(shard: shard_name) do
47
- klass.prohibit_shard_swapping(true) do
48
- klass.connected_to(shard: shard_name, role: role, &block)
47
+ klass.connected_to(shard: shard_name, role: role) do
48
+ klass.prohibit_shard_swapping(true, &block)
49
49
  end
50
50
  end
51
51
  rescue ActiveRecord::ConnectionNotEstablished => e
@@ -38,9 +38,6 @@ module Ec
38
38
  class_attribute :writing_database_identifier, default: writing_database_identifier
39
39
  class_attribute :reading_database_identifier, default: reading_database_identifier
40
40
 
41
- # include InstanceMethods
42
- # extend QueryMethods
43
-
44
41
  self.is_sharded = true
45
42
  self.abstract_class = true
46
43
  self.connects_to(shards: shards_hash)
@@ -49,23 +46,22 @@ module Ec
49
46
  private
50
47
  def shards_hash
51
48
  {}.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)
49
+ Ec::Pg.shards.each do |shard|
50
+ hash[shard] = {
51
+ writing: database_identifier_for(writing_database_identifier, shard),
52
+ reading: database_identifier_for(reading_database_identifier, shard)
57
53
  }.compact
58
54
  end
59
55
  end.symbolize_keys
60
56
  end
61
57
 
62
- def database_identifier_for(database_identifier, index)
58
+ def database_identifier_for(database_identifier, shard)
63
59
  return unless database_identifier.present?
64
60
 
65
61
  if tenant_mode.to_sym == :solo
66
62
  database_identifier
67
63
  else
68
- [database_identifier, "shard", index + 1].join('_')
64
+ [database_identifier, shard].join('_')
69
65
  end.to_sym
70
66
  end
71
67
  end
data/lib/ec/pg/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ec
4
4
  module Pg
5
- VERSION = "0.1.2"
5
+ VERSION = "0.1.3"
6
6
  end
7
7
  end
data/lib/ec/pg.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_record"
4
4
  require "active_support/concern"
5
+ require "colorize"
5
6
  require_relative "pg/version"
6
7
  require_relative "pg/context"
7
8
  require_relative "pg/tenant_context"
@@ -22,16 +23,25 @@ module Ec
22
23
  class Error < StandardError; end
23
24
  class InvalidType < Error; end
24
25
 
26
+ autoload :Mixin, 'ec/pg/mixin.rb'
25
27
  autoload :SchemaMixin, 'ec/pg/schema_mixin.rb'
26
28
  autoload :SchemaManager, 'ec/pg/schema_manager.rb'
27
29
  autoload :ShardMixin, 'ec/pg/shard_mixin.rb'
28
30
  autoload :ShardManager, 'ec/pg/shard_manager.rb'
29
31
  autoload :RlsMixin, 'ec/pg/rls_mixin.rb'
30
32
  autoload :RlsManager, 'ec/pg/rls_manager.rb'
31
- autoload :ContextSwitcher, 'ec/pg/middleware/context_switcher.rb'
33
+ autoload :ContextSwitcher, 'ec/pg/middleware/context_switcher.rb'
34
+ autoload :SchemaRegistryMixin, 'ec/pg/schema_registry_mixin.rb'
35
+ autoload :Migrator, 'ec/pg/migrator.rb'
32
36
 
33
37
  module_function
34
38
 
39
+ def dprint(*args)
40
+ if ENV['ECPG_DEBUG']
41
+ puts args
42
+ end
43
+ end
44
+
35
45
  def configure
36
46
  yield configuration
37
47
  end
@@ -44,6 +54,20 @@ module Ec
44
54
  TenantContext.switch(shard: shard, schema: schema, &block)
45
55
  end
46
56
 
57
+ def shards
58
+ @shards ||= configuration.number_of_shards.times.map do |index|
59
+ "shard_#{index + 1}"
60
+ end
61
+ end
62
+
63
+ def each_shard(&block)
64
+ shards.each do |shard|
65
+ Ec::Pg.switch(shard: shard) do
66
+ yield(shard)
67
+ end
68
+ end
69
+ end
70
+
47
71
  def current_shard = TenantContext.current_shard
48
72
  def current_schema = TenantContext.current_schema
49
73
 
data/lib/tasks/db.rake ADDED
@@ -0,0 +1,8 @@
1
+ namespace :db do
2
+ namespace :rspec do
3
+ desc "Prepares test db for rspec"
4
+ task prepare: :environment do
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ec_pg do
4
+ # ---------------------------------------------------------------------------
5
+ # Migrations
6
+ # ---------------------------------------------------------------------------
7
+ namespace :migrate do
8
+ task preload_models: :environment do
9
+ if ENV['CPG_DEBUG']
10
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
11
+ end
12
+
13
+ Dir[Rails.root.join('app/models/**/*.rb')].each do |file|
14
+ require file
15
+ end
16
+ end
17
+
18
+ desc "Migrate a single schema. Usage: rake 'ec_pg:migrate:schema[name]'"
19
+ task :schema, [:name] => :environment do |_, args|
20
+ name = args[:name] or abort "Usage: rake 'ec_pg:migrate:schema[schema_name]'"
21
+ Ec::Pg::Migrator.migrate_schema(name)
22
+ puts "Schema '#{name}' migrated."
23
+ end
24
+
25
+ desc "Migrate comma-separated schemas. Usage: rake 'ec_pg:migrate:schemas[tenant_a,tenant_b]'"
26
+ task :schemas => :preload_models do
27
+ Ec::Pg::Migrator.migrate_all_schemas
28
+ end
29
+
30
+ desc "Roll back a schema. Usage: rake 'ec_pg:migrate:rollback_schema[name]' or '[name,2]'"
31
+ task :rollback_schema, [:name, :steps] => :environment do |_, args|
32
+ name = args[:name] or abort "Usage: rake 'ec_pg:migrate:rollback_schema[name]'"
33
+ steps = args[:steps]&.to_i || 1
34
+ Ec::Pg::Migrator.rollback_schema(name, steps: steps)
35
+ puts "Rolled back #{steps} migration(s) in schema '#{name}'."
36
+ end
37
+
38
+ desc "Migrate a single shard. Usage: rake 'ec_pg:migrate:shard[shard_one]'"
39
+ task :shard, [:name] => :environment do |_, args|
40
+ name = args[:name] or abort "Usage: rake 'ec_pg:migrate:shard[shard_name]'"
41
+ Ec::Pg::Migrator.migrate_shard(name.to_sym)
42
+ puts "Shard '#{name}' migrated."
43
+ end
44
+
45
+ desc "Migrate all shards (derived from configuration.number_of_shards, or explicit list)"
46
+ task :shards, [:names] => :environment do |_, args|
47
+ shards = args[:names]&.split(",")&.map { |s| s.strip.to_sym }
48
+ Ec::Pg::Migrator.migrate_each_shard(shards)
49
+ puts "All shards migrated."
50
+ end
51
+
52
+ desc "Roll back a shard. Usage: rake 'ec_pg:migrate:rollback_shard[shard_one]' or '[shard_one,2]'"
53
+ task :rollback_shard, [:name, :steps] => :environment do |_, args|
54
+ name = args[:name] or abort "Usage: rake 'ec_pg:migrate:rollback_shard[name]'"
55
+ steps = args[:steps]&.to_i || 1
56
+ Ec::Pg::Migrator.rollback_shard(name.to_sym, steps: steps)
57
+ puts "Rolled back #{steps} migration(s) in shard '#{name}'."
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ec_pg do
4
+ # ---------------------------------------------------------------------------
5
+ # Schema DDL
6
+ # ---------------------------------------------------------------------------
7
+ namespace :schema do
8
+ desc "Create a PostgreSQL schema. Usage: rake 'ec_pg:schema:create[name]'"
9
+ task :create, [:model_class, :shard, :schema_name] => :environment do |_, args|
10
+ model = args[:model_class].constantize
11
+ shard = args[:shard]
12
+ schema = args[:schema_name]
13
+
14
+ unless shard.present? && schema.present?
15
+ abort "Usage: rake 'ec_pg:schema:create[Some::ApplicationRecord,shard_1,schema_name]"
16
+ end
17
+ model.create_schema!(shard: shard, schema: schema)
18
+ end
19
+
20
+ desc "Drop a PostgreSQL schema. Usage: rake 'ec_pg:schema:drop[name]' or 'ec_pg:schema:drop[name,cascade]'"
21
+ task :drop, [:name, :cascade] => :environment do |_, args|
22
+ name = args[:name] or abort "Usage: rake 'ec_pg:schema:drop[schema_name]'"
23
+ cascade = args[:cascade]&.casecmp?("true") || false
24
+ Ec::Pg::SchemaRegistry.drop!(name, cascade: cascade)
25
+ puts "Schema '#{name}' dropped."
26
+ end
27
+
28
+ desc "List all non-system PostgreSQL schemas"
29
+ task list: :environment do
30
+ Ec::Pg::SchemaRegistry.all.each { |s| puts s }
31
+ end
32
+ end
33
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ec-pg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - gmhawash
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: colorize
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: rails
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +121,20 @@ dependencies:
107
121
  - - ">="
108
122
  - !ruby/object:Gem::Version
109
123
  version: '0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: database_cleaner-active_record
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
110
138
  description: Multitenant implemntations using shards, schemas and RLS
111
139
  email:
112
140
  - gmhawash@gmail.com
@@ -120,22 +148,29 @@ files:
120
148
  - README.md
121
149
  - Rakefile
122
150
  - config/locales/en.yml
123
- - ec-pg-0.1.0.gem
124
- - ec-pg-0.1.1.gem
125
151
  - images/stand-with-palestine.webp
126
152
  - lib/ec/pg.rb
127
153
  - lib/ec/pg/configuration.rb
128
154
  - lib/ec/pg/context.rb
129
155
  - lib/ec/pg/middleware/context_switcher.rb
156
+ - lib/ec/pg/migrator.rb
157
+ - lib/ec/pg/mixin.rb
130
158
  - lib/ec/pg/railtie.rb
131
159
  - lib/ec/pg/rls_manager.rb
132
160
  - lib/ec/pg/rls_mixin.rb
133
161
  - lib/ec/pg/schema_manager.rb
134
162
  - lib/ec/pg/schema_mixin.rb
163
+ - lib/ec/pg/schema_registry/cloner.rb
164
+ - lib/ec/pg/schema_registry/mixin.rb
165
+ - lib/ec/pg/schema_registry/schema_migration_cloner.rb
166
+ - lib/ec/pg/schema_registry_mixin.rb
135
167
  - lib/ec/pg/shard_manager.rb
136
168
  - lib/ec/pg/shard_mixin.rb
137
169
  - lib/ec/pg/tenant_context.rb
138
170
  - lib/ec/pg/version.rb
171
+ - lib/tasks/db.rake
172
+ - lib/tasks/ec_pg_migrate.rake
173
+ - lib/tasks/ec_pg_schema.rake
139
174
  - sig/ec/pg.rbs
140
175
  homepage: https://github.com/binnablus/ec-pg
141
176
  licenses:
data/ec-pg-0.1.0.gem DELETED
Binary file
data/ec-pg-0.1.1.gem DELETED
Binary file