pg_multitenant_schemas 0.1.3 → 0.2.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 +4 -4
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/README.md +269 -16
- data/docs/README.md +77 -0
- data/docs/configuration.md +340 -0
- data/docs/context.md +292 -0
- data/docs/errors.md +498 -0
- data/docs/integration_testing.md +454 -0
- data/docs/migrator.md +291 -0
- data/docs/rails_integration.md +468 -0
- data/docs/schema_switcher.md +182 -0
- data/docs/tenant_resolver.md +394 -0
- data/docs/testing.md +358 -0
- data/examples/context_management.rb +198 -0
- data/examples/migration_workflow.rb +50 -0
- data/examples/rails_integration/controller_examples.rb +368 -0
- data/examples/schema_operations.rb +124 -0
- data/lib/pg_multitenant_schemas/configuration.rb +4 -4
- data/lib/pg_multitenant_schemas/migration_display_reporter.rb +30 -0
- data/lib/pg_multitenant_schemas/migration_executor.rb +81 -0
- data/lib/pg_multitenant_schemas/migration_schema_operations.rb +54 -0
- data/lib/pg_multitenant_schemas/migration_status_reporter.rb +65 -0
- data/lib/pg_multitenant_schemas/migrator.rb +89 -0
- data/lib/pg_multitenant_schemas/schema_switcher.rb +40 -66
- data/lib/pg_multitenant_schemas/tasks/advanced_tasks.rake +21 -0
- data/lib/pg_multitenant_schemas/tasks/basic_tasks.rake +20 -0
- data/lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake +53 -143
- data/lib/pg_multitenant_schemas/tasks/tenant_tasks.rake +65 -0
- data/lib/pg_multitenant_schemas/tenant_task_helpers.rb +102 -0
- data/lib/pg_multitenant_schemas/version.rb +1 -1
- data/lib/pg_multitenant_schemas.rb +10 -5
- data/pg_multitenant_schemas.gemspec +10 -9
- data/rails_integration/app/controllers/application_controller.rb +6 -0
- data/rails_integration/app/models/tenant.rb +6 -0
- metadata +39 -17
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PgMultitenantSchemas
|
|
4
|
+
# Migration status reporting functionality
|
|
5
|
+
module MigrationStatusReporter
|
|
6
|
+
# Check migration status across all tenants
|
|
7
|
+
def migration_status(verbose: true)
|
|
8
|
+
schemas = tenant_schemas
|
|
9
|
+
results = collect_migration_status_for_schemas(schemas)
|
|
10
|
+
display_status_report(results, verbose)
|
|
11
|
+
results
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def collect_migration_status_for_schemas(schemas)
|
|
17
|
+
schemas.map do |schema|
|
|
18
|
+
collect_status_for_schema(schema)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def collect_status_for_schema(schema)
|
|
23
|
+
original_schema = current_schema
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
switch_to_schema(schema)
|
|
27
|
+
build_schema_status(schema)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
{ schema: schema, error: e.message, status: :error }
|
|
30
|
+
ensure
|
|
31
|
+
switch_to_schema(original_schema) if original_schema
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def build_schema_status(schema)
|
|
36
|
+
pending = pending_migrations
|
|
37
|
+
applied = applied_migrations
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
schema: schema,
|
|
41
|
+
pending_count: pending.count,
|
|
42
|
+
applied_count: applied.count,
|
|
43
|
+
status: pending.any? ? :pending : :up_to_date
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def display_status_report(results, verbose)
|
|
48
|
+
return unless verbose
|
|
49
|
+
|
|
50
|
+
puts "📋 Migration Status Report:"
|
|
51
|
+
results.each { |result| display_schema_status(result) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def display_schema_status(result)
|
|
55
|
+
case result[:status]
|
|
56
|
+
when :up_to_date
|
|
57
|
+
puts " ✅ #{result[:schema]}: Up to date (#{result[:applied_count]} applied)"
|
|
58
|
+
when :pending
|
|
59
|
+
puts " ⏳ #{result[:schema]}: #{result[:pending_count]} pending, #{result[:applied_count]} applied"
|
|
60
|
+
when :error
|
|
61
|
+
puts " ❌ #{result[:schema]}: Error - #{result[:error]}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "migration_status_reporter"
|
|
4
|
+
require_relative "migration_schema_operations"
|
|
5
|
+
require_relative "migration_executor"
|
|
6
|
+
require_relative "migration_display_reporter"
|
|
7
|
+
|
|
8
|
+
module PgMultitenantSchemas
|
|
9
|
+
# Enhanced migration management for multi-tenant schemas
|
|
10
|
+
# Provides automated migration operations across tenant schemas
|
|
11
|
+
class Migrator
|
|
12
|
+
extend MigrationStatusReporter
|
|
13
|
+
extend MigrationSchemaOperations
|
|
14
|
+
extend MigrationExecutor
|
|
15
|
+
extend MigrationDisplayReporter
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
# Run migrations on all tenant schemas
|
|
19
|
+
def migrate_all(verbose: true, ignore_errors: false)
|
|
20
|
+
schemas = tenant_schemas
|
|
21
|
+
results = process_schemas_migration(schemas, verbose, ignore_errors)
|
|
22
|
+
|
|
23
|
+
display_migration_summary(results, verbose)
|
|
24
|
+
results
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Run migrations on a specific tenant schema
|
|
28
|
+
def migrate_tenant(schema_name, verbose: true, raise_on_error: true)
|
|
29
|
+
return handle_missing_schema(schema_name, verbose, raise_on_error) unless schema_exists?(schema_name)
|
|
30
|
+
|
|
31
|
+
execute_tenant_migration(schema_name, verbose, raise_on_error)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Setup tenant with schema creation and migrations
|
|
35
|
+
def setup_tenant(schema_name, verbose: true)
|
|
36
|
+
puts "🏗️ Setting up tenant: #{schema_name}" if verbose
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
create_tenant_schema_if_needed(schema_name, verbose)
|
|
40
|
+
result = migrate_tenant(schema_name, verbose: verbose)
|
|
41
|
+
puts " 🎉 Tenant setup completed!" if verbose
|
|
42
|
+
result
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
puts " ❌ Setup failed: #{e.message}" if verbose
|
|
45
|
+
raise
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Setup all tenants from Tenant model
|
|
50
|
+
def setup_all_tenants(verbose: true)
|
|
51
|
+
validate_tenant_model_exists
|
|
52
|
+
tenants = Tenant.all
|
|
53
|
+
puts "🏗️ Setting up #{tenants.count} tenants..." if verbose
|
|
54
|
+
|
|
55
|
+
results = process_setup_for_tenants(tenants, verbose)
|
|
56
|
+
display_setup_summary(results, verbose)
|
|
57
|
+
results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Create a new tenant with schema and migrations
|
|
61
|
+
def create_tenant_with_schema(attributes, verbose: true)
|
|
62
|
+
validate_tenant_model_exists
|
|
63
|
+
tenant = Tenant.create!(attributes)
|
|
64
|
+
schema_name = extract_schema_name(tenant)
|
|
65
|
+
|
|
66
|
+
puts "🆕 Creating new tenant: #{schema_name}" if verbose
|
|
67
|
+
setup_tenant(schema_name, verbose: verbose)
|
|
68
|
+
tenant
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Rollback migrations for a specific tenant
|
|
72
|
+
def rollback_tenant(schema_name, steps: 1, verbose: true)
|
|
73
|
+
puts "⏪ Rolling back #{steps} steps for #{schema_name}" if verbose
|
|
74
|
+
original_schema = current_schema
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
switch_to_schema(schema_name)
|
|
78
|
+
ActiveRecord::Base.connection.migration_context.rollback(migration_paths, steps)
|
|
79
|
+
puts " ✅ Rollback completed" if verbose
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
puts " ❌ Rollback failed: #{e.message}" if verbose
|
|
82
|
+
raise
|
|
83
|
+
ensure
|
|
84
|
+
switch_to_schema(original_schema) if original_schema
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -22,118 +22,92 @@ module PgMultitenantSchemas
|
|
|
22
22
|
raise ConnectionError, "Failed to get database connection: #{e.message}"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
# Switches the search_path
|
|
26
|
-
def switch_schema(
|
|
27
|
-
if
|
|
28
|
-
# New API: switch_schema('schema_name')
|
|
29
|
-
schema = conn_or_schema
|
|
30
|
-
conn = connection
|
|
31
|
-
else
|
|
32
|
-
# Old API: switch_schema(conn, 'schema_name')
|
|
33
|
-
conn = conn_or_schema
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
raise ArgumentError, "Schema name cannot be empty" if schema.nil? || schema.strip.empty?
|
|
25
|
+
# Switches the search_path to the specified schema
|
|
26
|
+
def switch_schema(schema_name)
|
|
27
|
+
raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
quoted_schema = "\"#{
|
|
29
|
+
conn = connection
|
|
30
|
+
quoted_schema = "\"#{schema_name.gsub('"', '""')}\""
|
|
40
31
|
execute_sql(conn, "SET search_path TO #{quoted_schema};")
|
|
41
32
|
end
|
|
42
33
|
|
|
43
|
-
# Reset to default schema
|
|
44
|
-
def reset_schema
|
|
45
|
-
conn
|
|
34
|
+
# Reset to default schema
|
|
35
|
+
def reset_schema
|
|
36
|
+
conn = connection
|
|
46
37
|
execute_sql(conn, "SET search_path TO public;")
|
|
47
38
|
end
|
|
48
39
|
|
|
49
|
-
# Create a new schema
|
|
50
|
-
def create_schema(
|
|
51
|
-
if conn_or_schema.is_a?(String)
|
|
52
|
-
# New API: create_schema('schema_name')
|
|
53
|
-
schema_name = conn_or_schema
|
|
54
|
-
conn = connection
|
|
55
|
-
else
|
|
56
|
-
# Old API: create_schema(conn, 'schema_name')
|
|
57
|
-
conn = conn_or_schema
|
|
58
|
-
end
|
|
59
|
-
|
|
40
|
+
# Create a new schema
|
|
41
|
+
def create_schema(schema_name)
|
|
60
42
|
raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
|
|
61
43
|
|
|
44
|
+
conn = connection
|
|
62
45
|
quoted_schema = "\"#{schema_name.gsub('"', '""')}\""
|
|
63
46
|
execute_sql(conn, "CREATE SCHEMA IF NOT EXISTS #{quoted_schema};")
|
|
64
47
|
end
|
|
65
48
|
|
|
66
|
-
# Drop a schema
|
|
67
|
-
def drop_schema(
|
|
68
|
-
if conn_or_schema.is_a?(String)
|
|
69
|
-
# New API: drop_schema('schema_name', cascade: true)
|
|
70
|
-
schema_name = conn_or_schema
|
|
71
|
-
options = schema_name_or_options || {}
|
|
72
|
-
cascade = options.fetch(:cascade, cascade)
|
|
73
|
-
conn = connection
|
|
74
|
-
else
|
|
75
|
-
# Old API: drop_schema(conn, 'schema_name', cascade: true)
|
|
76
|
-
conn = conn_or_schema
|
|
77
|
-
schema_name = schema_name_or_options
|
|
78
|
-
end
|
|
79
|
-
|
|
49
|
+
# Drop a schema
|
|
50
|
+
def drop_schema(schema_name, cascade: true)
|
|
80
51
|
raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
|
|
81
52
|
|
|
53
|
+
conn = connection
|
|
82
54
|
cascade_option = cascade ? "CASCADE" : "RESTRICT"
|
|
83
55
|
quoted_schema = "\"#{schema_name.gsub('"', '""')}\""
|
|
84
56
|
execute_sql(conn, "DROP SCHEMA IF EXISTS #{quoted_schema} #{cascade_option};")
|
|
85
57
|
end
|
|
86
58
|
|
|
87
|
-
# Check if schema exists
|
|
88
|
-
def schema_exists?(
|
|
89
|
-
if conn_or_schema.is_a?(String)
|
|
90
|
-
# New API: schema_exists?('schema_name')
|
|
91
|
-
schema_name = conn_or_schema
|
|
92
|
-
conn = connection
|
|
93
|
-
else
|
|
94
|
-
# Old API: schema_exists?(conn, 'schema_name')
|
|
95
|
-
conn = conn_or_schema
|
|
96
|
-
end
|
|
97
|
-
|
|
59
|
+
# Check if schema exists
|
|
60
|
+
def schema_exists?(schema_name)
|
|
98
61
|
return false if schema_name.nil? || schema_name.strip.empty?
|
|
99
62
|
|
|
63
|
+
conn = connection
|
|
100
64
|
result = execute_sql(conn, <<~SQL)
|
|
101
65
|
SELECT EXISTS(
|
|
102
66
|
SELECT 1 FROM information_schema.schemata#{" "}
|
|
103
67
|
WHERE schema_name = '#{schema_name}'
|
|
104
68
|
) AS schema_exists
|
|
105
69
|
SQL
|
|
106
|
-
|
|
70
|
+
|
|
71
|
+
value = get_result_value(result, 0, 0)
|
|
72
|
+
# Handle both boolean values and string representations
|
|
73
|
+
[true, "t", "true"].include?(value)
|
|
107
74
|
end
|
|
108
75
|
|
|
109
|
-
# Get current schema
|
|
110
|
-
def current_schema
|
|
111
|
-
conn
|
|
76
|
+
# Get current schema
|
|
77
|
+
def current_schema
|
|
78
|
+
conn = connection
|
|
112
79
|
result = execute_sql(conn, "SELECT current_schema()")
|
|
113
80
|
get_result_value(result, 0, 0)
|
|
114
81
|
end
|
|
115
82
|
|
|
116
83
|
private
|
|
117
84
|
|
|
118
|
-
# Execute SQL
|
|
85
|
+
# Execute SQL - handles both Rails connections and raw PG connections
|
|
119
86
|
def execute_sql(conn, sql)
|
|
120
87
|
if conn.respond_to?(:execute)
|
|
121
|
-
# Rails connection
|
|
88
|
+
# Rails/ActiveRecord connection
|
|
122
89
|
conn.execute(sql)
|
|
123
|
-
|
|
124
|
-
# Raw PG
|
|
90
|
+
elsif conn.respond_to?(:exec)
|
|
91
|
+
# Raw PG::Connection
|
|
125
92
|
conn.exec(sql)
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError, "Connection must respond to either :execute or :exec"
|
|
126
95
|
end
|
|
127
96
|
end
|
|
128
97
|
|
|
129
|
-
# Get value from result
|
|
98
|
+
# Get value from result - handles both Rails and PG results
|
|
130
99
|
def get_result_value(result, row, col)
|
|
131
|
-
if result.respond_to?(:
|
|
132
|
-
#
|
|
100
|
+
if result.respond_to?(:rows)
|
|
101
|
+
# Rails ActiveRecord::Result
|
|
102
|
+
result.rows[row][col]
|
|
103
|
+
elsif result.respond_to?(:getvalue)
|
|
104
|
+
# Raw PG::Result
|
|
133
105
|
result.getvalue(row, col)
|
|
106
|
+
elsif result.respond_to?(:[])
|
|
107
|
+
# Alternative PG::Result access
|
|
108
|
+
result[row][col]
|
|
134
109
|
else
|
|
135
|
-
|
|
136
|
-
result.rows[row][col]
|
|
110
|
+
raise ArgumentError, "Result must respond to either :rows, :getvalue, or :[]"
|
|
137
111
|
end
|
|
138
112
|
end
|
|
139
113
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Advanced tenant management tasks
|
|
4
|
+
namespace :tenants do
|
|
5
|
+
desc "Run migrations for a specific tenant"
|
|
6
|
+
task :migrate_tenant, [:schema_name] => :environment do |_task, args|
|
|
7
|
+
require_relative "../tenant_task_helpers"
|
|
8
|
+
TenantTaskHelpers.migrate_specific_tenant(args[:schema_name])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Setup new tenant with schema and migrations"
|
|
12
|
+
task :create, [:schema_name] => :environment do |_task, args|
|
|
13
|
+
require_relative "../tenant_task_helpers"
|
|
14
|
+
TenantTaskHelpers.create_tenant_with_schema(args[:schema_name])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
desc "Setup schemas and run migrations for all existing tenants"
|
|
18
|
+
task setup: :environment do
|
|
19
|
+
PgMultitenantSchemas::Migrator.setup_all_tenants
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Basic tenant management tasks
|
|
4
|
+
namespace :tenants do
|
|
5
|
+
desc "List all tenant schemas"
|
|
6
|
+
task list: :environment do
|
|
7
|
+
require_relative "../tenant_task_helpers"
|
|
8
|
+
TenantTaskHelpers.list_tenant_schemas
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
desc "Show migration status for all tenants"
|
|
12
|
+
task status: :environment do
|
|
13
|
+
PgMultitenantSchemas::Migrator.migration_status
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc "Run migrations for all tenant schemas"
|
|
17
|
+
task migrate: :environment do
|
|
18
|
+
PgMultitenantSchemas::Migrator.migrate_all
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -1,155 +1,65 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
task :list_schemas => :environment do
|
|
6
|
-
puts "Available tenant schemas:"
|
|
7
|
-
|
|
8
|
-
connection = ActiveRecord::Base.connection
|
|
9
|
-
schemas = connection.execute(
|
|
10
|
-
"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast_temp_1', 'pg_temp_1', 'public') AND schema_name NOT LIKE 'pg_toast%'"
|
|
11
|
-
).map { |row| row['schema_name'] }
|
|
12
|
-
|
|
13
|
-
if schemas.any?
|
|
14
|
-
schemas.each { |schema| puts " - #{schema}" }
|
|
15
|
-
else
|
|
16
|
-
puts " No tenant schemas found"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
3
|
+
# Load individual task files
|
|
4
|
+
Dir[File.join(File.dirname(__FILE__), "*.rake")].each { |file| load file unless file == __FILE__ }
|
|
19
5
|
|
|
20
|
-
|
|
21
|
-
task :create_schema, [:schema_name] => :environment do |task, args|
|
|
22
|
-
schema_name = args[:schema_name]
|
|
23
|
-
|
|
24
|
-
if schema_name.blank?
|
|
25
|
-
puts "Usage: rails pg_multitenant_schemas:create_schema[schema_name]"
|
|
26
|
-
exit 1
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
begin
|
|
30
|
-
connection = ActiveRecord::Base.connection
|
|
31
|
-
PgMultitenantSchemas::SchemaSwitcher.create_schema(connection, schema_name)
|
|
32
|
-
puts "Created schema: #{schema_name}"
|
|
33
|
-
rescue => e
|
|
34
|
-
puts "Error creating schema #{schema_name}: #{e.message}"
|
|
35
|
-
exit 1
|
|
36
|
-
end
|
|
37
|
-
end
|
|
6
|
+
require_relative "../tenant_task_helpers"
|
|
38
7
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if schema_name.blank?
|
|
44
|
-
puts "Usage: rails pg_multitenant_schemas:drop_schema[schema_name]"
|
|
45
|
-
exit 1
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
if schema_name == 'public'
|
|
49
|
-
puts "Cannot drop public schema"
|
|
50
|
-
exit 1
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
begin
|
|
54
|
-
connection = ActiveRecord::Base.connection
|
|
55
|
-
PgMultitenantSchemas::SchemaSwitcher.drop_schema(connection, schema_name)
|
|
56
|
-
puts "Dropped schema: #{schema_name}"
|
|
57
|
-
rescue => e
|
|
58
|
-
puts "Error dropping schema #{schema_name}: #{e.message}"
|
|
59
|
-
exit 1
|
|
60
|
-
end
|
|
8
|
+
namespace :tenants do
|
|
9
|
+
desc "Create new tenant with attributes (JSON format)"
|
|
10
|
+
task :new, [:attributes] => :environment do |_task, args|
|
|
11
|
+
TenantTaskHelpers.create_tenant_with_attributes(args[:attributes])
|
|
61
12
|
end
|
|
62
13
|
|
|
63
|
-
desc "
|
|
64
|
-
task :
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
connection = ActiveRecord::Base.connection
|
|
68
|
-
schemas = connection.execute(
|
|
69
|
-
"SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast_temp_1', 'pg_temp_1', 'public') AND schema_name NOT LIKE 'pg_toast%'"
|
|
70
|
-
).map { |row| row['schema_name'] }
|
|
71
|
-
|
|
72
|
-
original_schema = PgMultitenantSchemas.current_schema rescue 'public'
|
|
73
|
-
|
|
74
|
-
schemas.each do |schema|
|
|
75
|
-
puts "Migrating schema: #{schema}"
|
|
76
|
-
begin
|
|
77
|
-
PgMultitenantSchemas.with_tenant(schema) do
|
|
78
|
-
# Rails 8 compatible migration execution
|
|
79
|
-
if Rails.version >= "8.0"
|
|
80
|
-
# Use Rails 8 migration API
|
|
81
|
-
ActiveRecord::Tasks::DatabaseTasks.migrate
|
|
82
|
-
else
|
|
83
|
-
# Fallback for older Rails versions
|
|
84
|
-
ActiveRecord::Migrator.migrate(Rails.application.paths["db/migrate"])
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
puts " ✓ Completed migration for #{schema}"
|
|
88
|
-
rescue => e
|
|
89
|
-
puts " ✗ Error migrating #{schema}: #{e.message}"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Restore original schema
|
|
94
|
-
PgMultitenantSchemas.current_schema = original_schema if defined?(PgMultitenantSchemas)
|
|
95
|
-
|
|
96
|
-
puts "Completed migrations for #{schemas.count} schemas"
|
|
14
|
+
desc "Drop tenant schema (DANGEROUS)"
|
|
15
|
+
task :drop, [:schema_name] => :environment do |_task, args|
|
|
16
|
+
TenantTaskHelpers.drop_tenant_schema(args[:schema_name])
|
|
97
17
|
end
|
|
98
18
|
|
|
99
|
-
desc "
|
|
100
|
-
task :
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if defined?(Tenant)
|
|
104
|
-
tenants = Tenant.all
|
|
105
|
-
|
|
106
|
-
tenants.each do |tenant|
|
|
107
|
-
begin
|
|
108
|
-
puts "Creating schema for tenant: #{tenant.subdomain}"
|
|
109
|
-
PgMultitenantSchemas::SchemaSwitcher.create_schema(
|
|
110
|
-
ActiveRecord::Base.connection,
|
|
111
|
-
tenant.subdomain
|
|
112
|
-
)
|
|
113
|
-
puts " ✓ Schema created for #{tenant.subdomain}"
|
|
114
|
-
rescue => e
|
|
115
|
-
if e.message.include?("already exists")
|
|
116
|
-
puts " - Schema #{tenant.subdomain} already exists"
|
|
117
|
-
else
|
|
118
|
-
puts " ✗ Could not create schema for #{tenant.subdomain}: #{e.message}"
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
puts "Setup completed for #{tenants.count} tenants"
|
|
124
|
-
else
|
|
125
|
-
puts "Tenant model not found. Make sure your Tenant model is defined."
|
|
126
|
-
end
|
|
19
|
+
desc "Rollback migrations for a tenant"
|
|
20
|
+
task :rollback, %i[schema_name steps] => :environment do |_task, args|
|
|
21
|
+
steps = (args[:steps] || 1).to_i
|
|
22
|
+
TenantTaskHelpers.rollback_tenant_migrations(args[:schema_name], steps)
|
|
127
23
|
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Namespace aliases for convenience
|
|
27
|
+
namespace :tenants do
|
|
28
|
+
namespace :db do
|
|
29
|
+
desc "Create tenant schema (alias for tenants:create)"
|
|
30
|
+
task :create, [:schema_name] => "tenants:create"
|
|
31
|
+
|
|
32
|
+
desc "Run tenant migrations (alias for tenants:migrate)"
|
|
33
|
+
task migrate: "tenants:migrate"
|
|
34
|
+
|
|
35
|
+
desc "Check tenant migration status (alias for tenants:status)"
|
|
36
|
+
task status: "tenants:status"
|
|
37
|
+
|
|
38
|
+
desc "Setup all tenants (alias for tenants:setup)"
|
|
39
|
+
task setup: "tenants:setup"
|
|
128
40
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
schema_name = args[:schema_name]
|
|
132
|
-
|
|
133
|
-
if schema_name.blank?
|
|
134
|
-
puts "Usage: rails pg_multitenant_schemas:migrate_tenant[schema_name]"
|
|
135
|
-
exit 1
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
puts "Running migrations for tenant: #{schema_name}"
|
|
139
|
-
|
|
140
|
-
begin
|
|
141
|
-
PgMultitenantSchemas.with_tenant(schema_name) do
|
|
142
|
-
# Rails 8 compatible migration execution
|
|
143
|
-
if Rails.version >= "8.0"
|
|
144
|
-
ActiveRecord::Tasks::DatabaseTasks.migrate
|
|
145
|
-
else
|
|
146
|
-
ActiveRecord::Migrator.migrate(Rails.application.paths["db/migrate"])
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
puts "✓ Completed migration for #{schema_name}"
|
|
150
|
-
rescue => e
|
|
151
|
-
puts "✗ Error migrating #{schema_name}: #{e.message}"
|
|
152
|
-
exit 1
|
|
153
|
-
end
|
|
41
|
+
desc "Rollback tenant migrations (alias for tenants:rollback)"
|
|
42
|
+
task :rollback, %i[schema_name steps] => "tenants:rollback"
|
|
154
43
|
end
|
|
155
44
|
end
|
|
45
|
+
|
|
46
|
+
# Legacy namespace for backward compatibility
|
|
47
|
+
namespace :pg_multitenant_schemas do
|
|
48
|
+
desc "DEPRECATED: Use 'tenants:list' instead"
|
|
49
|
+
task list_schemas: "tenants:list"
|
|
50
|
+
|
|
51
|
+
desc "DEPRECATED: Use 'tenants:migrate' instead"
|
|
52
|
+
task migrate_all: "tenants:migrate"
|
|
53
|
+
|
|
54
|
+
desc "DEPRECATED: Use 'tenants:setup' instead"
|
|
55
|
+
task setup: "tenants:setup"
|
|
56
|
+
|
|
57
|
+
desc "DEPRECATED: Use 'tenants:migrate_tenant[schema_name]' instead"
|
|
58
|
+
task :migrate_tenant, [:schema_name] => "tenants:migrate_tenant"
|
|
59
|
+
|
|
60
|
+
desc "DEPRECATED: Use 'tenants:create[schema_name]' instead"
|
|
61
|
+
task :create_schema, [:schema_name] => "tenants:create"
|
|
62
|
+
|
|
63
|
+
desc "DEPRECATED: Use 'tenants:drop[schema_name]' instead"
|
|
64
|
+
task :drop_schema, [:schema_name] => "tenants:drop"
|
|
65
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Load individual task files
|
|
4
|
+
Dir[File.join(File.dirname(__FILE__), "*.rake")].each { |file| load file unless file == __FILE__ }
|
|
5
|
+
|
|
6
|
+
require_relative "../tenant_task_helpers"
|
|
7
|
+
|
|
8
|
+
namespace :tenants do
|
|
9
|
+
desc "Create new tenant with attributes (JSON format)"
|
|
10
|
+
task :new, [:attributes] => :environment do |_task, args|
|
|
11
|
+
TenantTaskHelpers.create_tenant_with_attributes(args[:attributes])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc "Drop tenant schema (DANGEROUS)"
|
|
15
|
+
task :drop, [:schema_name] => :environment do |_task, args|
|
|
16
|
+
TenantTaskHelpers.drop_tenant_schema(args[:schema_name])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
desc "Rollback migrations for a tenant"
|
|
20
|
+
task :rollback, %i[schema_name steps] => :environment do |_task, args|
|
|
21
|
+
steps = (args[:steps] || 1).to_i
|
|
22
|
+
TenantTaskHelpers.rollback_tenant_migrations(args[:schema_name], steps)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Namespace aliases for convenience
|
|
27
|
+
namespace :tenants do
|
|
28
|
+
namespace :db do
|
|
29
|
+
desc "Create tenant schema (alias for tenants:create)"
|
|
30
|
+
task :create, [:schema_name] => "tenants:create"
|
|
31
|
+
|
|
32
|
+
desc "Run tenant migrations (alias for tenants:migrate)"
|
|
33
|
+
task migrate: "tenants:migrate"
|
|
34
|
+
|
|
35
|
+
desc "Check tenant migration status (alias for tenants:status)"
|
|
36
|
+
task status: "tenants:status"
|
|
37
|
+
|
|
38
|
+
desc "Setup all tenants (alias for tenants:setup)"
|
|
39
|
+
task setup: "tenants:setup"
|
|
40
|
+
|
|
41
|
+
desc "Rollback tenant migrations (alias for tenants:rollback)"
|
|
42
|
+
task :rollback, %i[schema_name steps] => "tenants:rollback"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Legacy namespace for backward compatibility
|
|
47
|
+
namespace :pg_multitenant_schemas do
|
|
48
|
+
desc "DEPRECATED: Use 'tenants:list' instead"
|
|
49
|
+
task list_schemas: "tenants:list"
|
|
50
|
+
|
|
51
|
+
desc "DEPRECATED: Use 'tenants:migrate' instead"
|
|
52
|
+
task migrate_all: "tenants:migrate"
|
|
53
|
+
|
|
54
|
+
desc "DEPRECATED: Use 'tenants:setup' instead"
|
|
55
|
+
task setup: "tenants:setup"
|
|
56
|
+
|
|
57
|
+
desc "DEPRECATED: Use 'tenants:migrate_tenant[schema_name]' instead"
|
|
58
|
+
task :migrate_tenant, [:schema_name] => "tenants:migrate_tenant"
|
|
59
|
+
|
|
60
|
+
desc "DEPRECATED: Use 'tenants:create[schema_name]' instead"
|
|
61
|
+
task :create_schema, [:schema_name] => "tenants:create"
|
|
62
|
+
|
|
63
|
+
desc "DEPRECATED: Use 'tenants:drop[schema_name]' instead"
|
|
64
|
+
task :drop_schema, [:schema_name] => "tenants:drop"
|
|
65
|
+
end
|