pg_multitenant_schemas 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.
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :pg_multitenant_schemas do
4
+ desc "List all tenant schemas"
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
19
+
20
+ desc "Create schema for a tenant"
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
38
+
39
+ desc "Drop schema for a tenant"
40
+ task :drop_schema, [:schema_name] => :environment do |task, args|
41
+ schema_name = args[:schema_name]
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
61
+ end
62
+
63
+ desc "Run migrations for all tenant schemas"
64
+ task :migrate_all => :environment do
65
+ puts "Running migrations for all tenant schemas..."
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"
97
+ end
98
+
99
+ desc "Setup multitenancy - create tenant schemas for existing tenants"
100
+ task :setup => :environment do
101
+ puts "Setting up multitenancy schemas..."
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
127
+ end
128
+
129
+ desc "Run migrations for a specific tenant schema"
130
+ task :migrate_tenant, [:schema_name] => :environment do |task, args|
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
154
+ end
155
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Tenant resolution from HTTP requests
5
+ class TenantResolver
6
+ class << self
7
+ # Extract subdomain from host
8
+ def extract_subdomain(host)
9
+ return nil if invalid_host?(host)
10
+
11
+ host = clean_host(host)
12
+ parts = host.split(".")
13
+ return nil if parts.length < 2
14
+
15
+ subdomain = parts.first.downcase
16
+ return nil if excluded_subdomain?(subdomain, parts)
17
+
18
+ subdomain
19
+ end
20
+
21
+ private
22
+
23
+ # Check if host is invalid for subdomain extraction
24
+ def invalid_host?(host)
25
+ host.blank? || host == "localhost" || host.match?(/\A\d+\.\d+\.\d+\.\d+/)
26
+ end
27
+
28
+ # Clean host by removing port
29
+ def clean_host(host)
30
+ host.split(":").first
31
+ end
32
+
33
+ # Check if subdomain should be excluded
34
+ def excluded_subdomain?(subdomain, parts)
35
+ # Exclude common subdomains for standard domains (3 parts or fewer)
36
+ # Allow them for complex subdomains (4+ parts like api.company.example.com)
37
+ if parts.length <= 3
38
+ excluded_subdomains = PgMultitenantSchemas.configuration.excluded_subdomains
39
+ return true if excluded_subdomains.include?(subdomain)
40
+ end
41
+
42
+ # For exactly 2 parts, also check if second part is a common TLD
43
+ if parts.length == 2
44
+ common_tlds = PgMultitenantSchemas.configuration.common_tlds
45
+ return true if common_tlds.include?(parts.last.downcase)
46
+ end
47
+
48
+ false
49
+ end
50
+
51
+ public
52
+
53
+ # Find tenant by subdomain (only active tenants)
54
+ def find_tenant_by_subdomain(subdomain)
55
+ return nil if subdomain.blank?
56
+
57
+ tenant_model = PgMultitenantSchemas.configuration.tenant_model
58
+
59
+ # Attempt to find active tenant
60
+ if tenant_model.respond_to?(:active)
61
+ tenant_model.active.find_by(subdomain: subdomain)
62
+ else
63
+ tenant_model.find_by(subdomain: subdomain, status: "active")
64
+ end
65
+ rescue StandardError => e
66
+ Rails.logger.error "PgMultitenantSchemas: Error finding tenant '#{subdomain}': #{e.message}"
67
+ nil
68
+ end
69
+
70
+ # Resolve tenant from request
71
+ def resolve_tenant_from_request(request)
72
+ subdomain = extract_subdomain(request.host)
73
+ return nil if subdomain.blank?
74
+
75
+ find_tenant_by_subdomain(subdomain)
76
+ end
77
+
78
+ # Resolve tenant with fallback options
79
+ def resolve_tenant_with_fallback(request)
80
+ tenant = resolve_tenant_from_request(request)
81
+
82
+ # If no tenant found and development fallback is enabled
83
+ if tenant.nil? && PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
84
+ Rails.logger.info "PgMultitenantSchemas: No tenant found, using development fallback"
85
+ return nil # This will cause switch to default schema
86
+ end
87
+
88
+ tenant
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ VERSION = "0.1.3"
5
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/delegation"
4
+ require "active_support/core_ext/object/blank"
5
+ require_relative "pg_multitenant_schemas/version"
6
+ require_relative "pg_multitenant_schemas/errors"
7
+ require_relative "pg_multitenant_schemas/configuration"
8
+ require_relative "pg_multitenant_schemas/schema_switcher"
9
+ require_relative "pg_multitenant_schemas/context"
10
+ require_relative "pg_multitenant_schemas/tenant_resolver"
11
+
12
+ # Rails integration (only load if Rails is available)
13
+ if defined?(Rails)
14
+ require_relative "pg_multitenant_schemas/rails/controller_concern"
15
+ require_relative "pg_multitenant_schemas/rails/model_concern"
16
+ require_relative "pg_multitenant_schemas/rails/railtie"
17
+ end
18
+
19
+ # PgMultitenantSchemas provides PostgreSQL schema-based multitenancy functionality.
20
+ # It allows switching between different PostgreSQL schemas to achieve tenant isolation
21
+ # while sharing the same database connection. Includes Rails integration support.
22
+ module PgMultitenantSchemas
23
+ class << self
24
+ # Delegate common methods to Context for convenience
25
+ delegate :current_tenant, :current_tenant=, :current_schema, :current_schema=,
26
+ :reset!, :switch_to_schema, :switch_to_tenant, :with_tenant,
27
+ :create_tenant_schema, :drop_tenant_schema, to: :"PgMultitenantSchemas::Context"
28
+
29
+ # Delegate tenant resolution to TenantResolver
30
+ delegate :extract_subdomain, :find_tenant_by_subdomain, :resolve_tenant_from_request,
31
+ to: :"PgMultitenantSchemas::TenantResolver"
32
+ end
33
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/pg_multitenant_schemas/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "pg_multitenant_schemas"
7
+ spec.version = PgMultitenantSchemas::VERSION
8
+ spec.authors = ["Ruben Paz"]
9
+ spec.email = ["rubenpazchuspe@outlook.com"]
10
+
11
+ spec.summary = "PostgreSQL schema-based multitenancy for Rails applications"
12
+ spec.description = "A Ruby gem that provides PostgreSQL schema-based multitenancy with automatic tenant " \
13
+ "resolution, schema switching, Rails 8 compatibility, and comprehensive Rails integration. " \
14
+ "Perfect for SaaS applications requiring secure tenant isolation."
15
+ spec.homepage = "https://github.com/rubenpazch/pg_multitenant_schemas"
16
+ spec.license = "MIT"
17
+ spec.required_ruby_version = ">= 3.1.0"
18
+
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
23
+
24
+ # Specify which files should be added to the gem when it is released.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile]) ||
29
+ f.end_with?(".gem")
30
+ end
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ # Runtime dependencies - Rails 8 compatible
37
+ spec.add_dependency "activerecord", ">= 7.0", "< 9.0"
38
+ spec.add_dependency "activesupport", ">= 7.0", "< 9.0"
39
+ spec.add_dependency "pg", "~> 1.0"
40
+ end
File without changes
@@ -0,0 +1,4 @@
1
+ module PgMultitenantSchemas
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pg_multitenant_schemas
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ platform: ruby
6
+ authors:
7
+ - Ruben Paz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-09-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '9.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '7.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '9.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '9.0'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '7.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '9.0'
53
+ - !ruby/object:Gem::Dependency
54
+ name: pg
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.0'
67
+ description: A Ruby gem that provides PostgreSQL schema-based multitenancy with automatic
68
+ tenant resolution, schema switching, Rails 8 compatibility, and comprehensive Rails
69
+ integration. Perfect for SaaS applications requiring secure tenant isolation.
70
+ email:
71
+ - rubenpazchuspe@outlook.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rspec"
77
+ - ".rubocop.yml"
78
+ - ".rubocop_simple.yml"
79
+ - CHANGELOG.md
80
+ - CODE_OF_CONDUCT.md
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - lib/pg_multitenant_schemas.rb
85
+ - lib/pg_multitenant_schemas/configuration.rb
86
+ - lib/pg_multitenant_schemas/context.rb
87
+ - lib/pg_multitenant_schemas/errors.rb
88
+ - lib/pg_multitenant_schemas/rails/controller_concern.rb
89
+ - lib/pg_multitenant_schemas/rails/model_concern.rb
90
+ - lib/pg_multitenant_schemas/rails/railtie.rb
91
+ - lib/pg_multitenant_schemas/schema_switcher.rb
92
+ - lib/pg_multitenant_schemas/tasks/pg_multitenant_schemas.rake
93
+ - lib/pg_multitenant_schemas/tenant_resolver.rb
94
+ - lib/pg_multitenant_schemas/version.rb
95
+ - pg_multitenant_schemas.gemspec
96
+ - rails_integration/app/controllers/application_controller.rb
97
+ - rails_integration/app/models/tenant.rb
98
+ - sig/pg_multitenant_schemas.rbs
99
+ homepage: https://github.com/rubenpazch/pg_multitenant_schemas
100
+ licenses:
101
+ - MIT
102
+ metadata:
103
+ allowed_push_host: https://rubygems.org
104
+ homepage_uri: https://github.com/rubenpazch/pg_multitenant_schemas
105
+ source_code_uri: https://github.com/rubenpazch/pg_multitenant_schemas
106
+ changelog_uri: https://github.com/rubenpazch/pg_multitenant_schemas/blob/main/CHANGELOG.md
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 3.1.0
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 3.3.27
123
+ signing_key:
124
+ specification_version: 4
125
+ summary: PostgreSQL schema-based multitenancy for Rails applications
126
+ test_files: []