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.
data/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # PgMultitenantSchemas
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/pg_multitenant_schemas.svg)](https://badge.fury.io/rb/pg_multitenant_schemas)
4
+ [![Ruby](https://github.com/yourusername/pg_multitenant_schemas/actions/workflows/main.yml/badge.svg)](https://github.com/yourusername/pg_multitenant_schemas/actions/workflows/main.yml)
5
+
6
+ A Ruby gem that provides PostgreSQL schema-based multitenancy with automatic tenant resolution, schema switching, and Rails integration. Perfect for SaaS applications that need secure tenant isolation.
7
+
8
+ ## Features
9
+
10
+ - ๐Ÿข **Schema-based multitenancy** - Complete tenant isolation using PostgreSQL schemas
11
+ - ๐Ÿ”„ **Automatic schema switching** - Seamlessly switch between tenant schemas
12
+ - ๐ŸŒ **Subdomain resolution** - Extract tenant from request subdomains
13
+ - ๐Ÿ›ก๏ธ **Rails 8 compatible** - Works with Rails 7.x and 8.x
14
+ - ๐Ÿš€ **Dual API support** - Backward compatible API design
15
+ - ๐Ÿงต **Thread-safe** - Safe for concurrent operations
16
+ - ๐Ÿ“ **Comprehensive logging** - Track schema operations
17
+ - โšก **High performance** - Minimal overhead
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'pg_multitenant_schemas'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ Or install it yourself as:
34
+
35
+ ```bash
36
+ gem install pg_multitenant_schemas
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ Configure the gem in your Rails initializer (`config/initializers/pg_multitenant_schemas.rb`):
42
+
43
+ ```ruby
44
+ PgMultitenantSchemas.configure do |config|
45
+ config.connection_class = 'ApplicationRecord' # or 'ActiveRecord::Base'
46
+ config.tenant_model_class = 'Tenant' # your tenant model
47
+ config.default_schema = 'public'
48
+ config.excluded_subdomains = ['www', 'api', 'admin']
49
+ config.development_fallback = true # for development
50
+ config.auto_create_schemas = true # automatically create missing schemas
51
+ end
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ ### Basic Schema Operations
57
+
58
+ ```ruby
59
+ # Switch to a tenant schema
60
+ PgMultitenantSchemas::SchemaSwitcher.switch_schema('tenant_123')
61
+
62
+ # Create a new schema
63
+ PgMultitenantSchemas::SchemaSwitcher.create_schema('tenant_456')
64
+
65
+ # Check if schema exists
66
+ PgMultitenantSchemas::SchemaSwitcher.schema_exists?('tenant_123')
67
+
68
+ # Drop a schema
69
+ PgMultitenantSchemas::SchemaSwitcher.drop_schema('tenant_456')
70
+ ```
71
+
72
+ ### Tenant Resolution
73
+
74
+ ```ruby
75
+ # Extract tenant from subdomain
76
+ subdomain = PgMultitenantSchemas.extract_subdomain('acme.myapp.com')
77
+ # => 'acme'
78
+
79
+ # Find tenant by subdomain
80
+ tenant = PgMultitenantSchemas.find_tenant_by_subdomain('acme')
81
+
82
+ # Resolve tenant from Rails request
83
+ tenant = PgMultitenantSchemas.resolve_tenant_from_request(request)
84
+ ```
85
+
86
+ ### Context Management
87
+
88
+ ```ruby
89
+ # Switch to tenant context
90
+ PgMultitenantSchemas.switch_to_tenant(tenant)
91
+
92
+ # Use block-based context switching
93
+ PgMultitenantSchemas.with_tenant(tenant) do
94
+ # All queries here use tenant's schema
95
+ User.all # Queries tenant_123.users
96
+ end
97
+
98
+ # Check current context
99
+ PgMultitenantSchemas.current_tenant
100
+ PgMultitenantSchemas.current_schema
101
+ ```
102
+
103
+ ### Rails Integration
104
+
105
+ In your ApplicationController:
106
+
107
+ ```ruby
108
+ class ApplicationController < ActionController::Base
109
+ include PgMultitenantSchemas::Rails::ControllerConcern
110
+
111
+ before_action :resolve_tenant
112
+
113
+ private
114
+
115
+ def resolve_tenant
116
+ tenant = resolve_tenant_from_subdomain
117
+ switch_to_tenant(tenant) if tenant
118
+ end
119
+ end
120
+ ```
121
+
122
+ In your models:
123
+
124
+ ```ruby
125
+ class Tenant < ApplicationRecord
126
+ include PgMultitenantSchemas::Rails::ModelConcern
127
+
128
+ after_create :create_tenant_schema
129
+ after_destroy :drop_tenant_schema
130
+ end
131
+ ```
132
+
133
+ ## Development
134
+
135
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
136
+
137
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
138
+
139
+ ## Contributing
140
+
141
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pg_multitenant_schemas. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pg_multitenant_schemas/blob/main/CODE_OF_CONDUCT.md).
142
+
143
+ ## License
144
+
145
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
146
+
147
+ ## Code of Conduct
148
+
149
+ Everyone interacting in the PgMultitenantSchemas project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pg_multitenant_schemas/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PgMultitenantSchemas provides PostgreSQL schema-based multitenancy functionality.
4
+ module PgMultitenantSchemas
5
+ # Configuration class for PgMultitenantSchemas gem settings.
6
+ # Manages tenant resolution, schema switching behavior, and Rails integration options.
7
+ class Configuration
8
+ attr_accessor :tenant_model_class, :default_schema, :development_fallback,
9
+ :excluded_subdomains, :common_tlds, :auto_create_schemas,
10
+ :connection_class, :logger
11
+
12
+ def initialize
13
+ @tenant_model_class = "Tenant"
14
+ @default_schema = "public"
15
+ @development_fallback = false
16
+ @excluded_subdomains = %w[www api admin mail ftp blog support help docs]
17
+ @common_tlds = %w[com org net edu gov mil int co uk ca au de fr jp cn]
18
+ @auto_create_schemas = true
19
+ @connection_class = "ActiveRecord::Base"
20
+ @logger = nil
21
+ end
22
+
23
+ def tenant_model
24
+ @tenant_model ||= tenant_model_class.constantize
25
+ end
26
+ end
27
+
28
+ class << self
29
+ def configuration
30
+ @configuration ||= Configuration.new
31
+ end
32
+
33
+ def configure
34
+ yield(configuration) if block_given?
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Thread-safe tenant context management
5
+ class Context
6
+ class << self
7
+ def current_tenant
8
+ Thread.current[:pg_multitenant_current_tenant]
9
+ end
10
+
11
+ def current_tenant=(tenant)
12
+ Thread.current[:pg_multitenant_current_tenant] = tenant
13
+ end
14
+
15
+ def current_schema
16
+ Thread.current[:pg_multitenant_current_schema] || PgMultitenantSchemas.configuration.default_schema
17
+ end
18
+
19
+ def current_schema=(schema_name)
20
+ Thread.current[:pg_multitenant_current_schema] = schema_name
21
+ end
22
+
23
+ def reset!
24
+ Thread.current[:pg_multitenant_current_tenant] = nil
25
+ Thread.current[:pg_multitenant_current_schema] = nil
26
+ switch_to_schema(PgMultitenantSchemas.configuration.default_schema)
27
+ end
28
+
29
+ def switch_to_schema(schema_name)
30
+ schema_name = PgMultitenantSchemas.configuration.default_schema if schema_name.blank?
31
+ SchemaSwitcher.switch_schema(schema_name)
32
+ self.current_schema = schema_name
33
+ end
34
+
35
+ def switch_to_tenant(tenant)
36
+ if tenant
37
+ schema_name = tenant.respond_to?(:subdomain) ? tenant.subdomain : tenant.to_s
38
+ switch_to_schema(schema_name)
39
+ self.current_tenant = tenant
40
+ else
41
+ switch_to_schema(PgMultitenantSchemas.configuration.default_schema)
42
+ self.current_tenant = nil
43
+ end
44
+ end
45
+
46
+ # Execute block within tenant context
47
+ def with_tenant(tenant_or_schema)
48
+ schema_name, tenant = extract_schema_and_tenant(tenant_or_schema)
49
+ previous_tenant = current_tenant
50
+ previous_schema = current_schema
51
+
52
+ begin
53
+ switch_to_schema(schema_name)
54
+ self.current_tenant = tenant
55
+ yield if block_given?
56
+ ensure
57
+ restore_previous_context(previous_tenant, previous_schema)
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Extract schema name and tenant object from input
64
+ def extract_schema_and_tenant(tenant_or_schema)
65
+ if tenant_or_schema.respond_to?(:subdomain)
66
+ [tenant_or_schema.subdomain, tenant_or_schema]
67
+ else
68
+ [tenant_or_schema.to_s, nil]
69
+ end
70
+ end
71
+
72
+ # Restore the previous tenant context
73
+ def restore_previous_context(previous_tenant, previous_schema)
74
+ restore_schema = if previous_tenant.respond_to?(:subdomain)
75
+ previous_tenant.subdomain
76
+ else
77
+ previous_schema
78
+ end
79
+ switch_to_schema(restore_schema)
80
+ self.current_tenant = previous_tenant
81
+ end
82
+
83
+ public
84
+
85
+ # Create new tenant schema
86
+ def create_tenant_schema(tenant_or_schema)
87
+ schema_name = if tenant_or_schema.respond_to?(:subdomain)
88
+ tenant_or_schema.subdomain
89
+ else
90
+ tenant_or_schema.to_s
91
+ end
92
+ SchemaSwitcher.create_schema(schema_name)
93
+ end
94
+
95
+ # Drop tenant schema
96
+ def drop_tenant_schema(tenant_or_schema, cascade: true)
97
+ schema_name = if tenant_or_schema.respond_to?(:subdomain)
98
+ tenant_or_schema.subdomain
99
+ else
100
+ tenant_or_schema.to_s
101
+ end
102
+ SchemaSwitcher.drop_schema(schema_name, cascade: cascade)
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Exception classes
5
+ class Error < StandardError; end
6
+ class ConnectionError < Error; end
7
+ class SchemaExists < Error; end
8
+ class SchemaNotFound < Error; end
9
+ class ConfigurationError < Error; end
10
+ class TenantNotFound < Error; end
11
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ module Rails
5
+ # Controller concern for Rails integration
6
+ module ControllerConcern
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Add callback hooks that can be overridden
11
+ before_action :resolve_tenant_context, unless: :skip_tenant_resolution?
12
+ after_action :reset_tenant_context, unless: :skip_tenant_resolution?
13
+
14
+ # Make current tenant available to views
15
+ helper_method :current_tenant, :current_schema if respond_to?(:helper_method)
16
+ end
17
+
18
+ # Instance methods
19
+ def current_tenant
20
+ @current_tenant || PgMultitenantSchemas::Context.current_tenant
21
+ end
22
+
23
+ def current_schema
24
+ PgMultitenantSchemas::Context.current_schema
25
+ end
26
+
27
+ def switch_to_tenant(tenant)
28
+ @current_tenant = tenant
29
+ PgMultitenantSchemas::Context.switch_to_tenant(tenant)
30
+ end
31
+
32
+ def resolve_tenant_context
33
+ tenant = PgMultitenantSchemas::TenantResolver.resolve_tenant_with_fallback(request)
34
+ switch_to_tenant(tenant)
35
+ rescue StandardError => e
36
+ handle_tenant_resolution_error(e)
37
+ end
38
+
39
+ def reset_tenant_context
40
+ @current_tenant = nil
41
+ PgMultitenantSchemas::Context.reset!
42
+ rescue StandardError => e
43
+ Rails.logger.error "PgMultitenantSchemas: Failed to reset tenant context: #{e.message}"
44
+ # Don't raise - this is cleanup code
45
+ end
46
+
47
+ protected
48
+
49
+ # Override this method in controllers that shouldn't use tenant resolution
50
+ def skip_tenant_resolution?
51
+ false
52
+ end
53
+
54
+ # Override this method to customize error handling
55
+ def handle_tenant_resolution_error(error)
56
+ Rails.logger.error "PgMultitenantSchemas: Tenant resolution failed: #{error.message}"
57
+
58
+ raise error unless PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
59
+
60
+ switch_to_tenant(nil) # Fall back to default schema in development
61
+ end
62
+
63
+ # Class methods
64
+ class_methods do
65
+ # DSL to skip tenant resolution for specific actions
66
+ def skip_tenant_resolution(options = {})
67
+ skip_before_action :resolve_tenant_context, options
68
+ skip_after_action :reset_tenant_context, options
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ module Rails
5
+ # Model concern for tenant callbacks
6
+ module ModelConcern
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Add callbacks for schema management
11
+ after_create :create_tenant_schema_callback, if: :should_manage_schema?
12
+ before_destroy :drop_tenant_schema_callback, if: :should_manage_schema?
13
+ end
14
+
15
+ def create_tenant_schema_callback
16
+ return unless valid_tenant_for_schema?
17
+
18
+ handle_schema_creation
19
+ end
20
+
21
+ def drop_tenant_schema_callback
22
+ return unless valid_tenant_for_schema?
23
+
24
+ handle_schema_deletion
25
+ end
26
+
27
+ protected
28
+
29
+ # Override this method to control when schema management should happen
30
+ def should_manage_schema?
31
+ PgMultitenantSchemas.configuration.auto_create_schemas &&
32
+ respond_to?(:subdomain) &&
33
+ subdomain.present?
34
+ end
35
+
36
+ private
37
+
38
+ # Check if tenant is valid for schema operations
39
+ def valid_tenant_for_schema?
40
+ respond_to?(:subdomain) && subdomain.present?
41
+ end
42
+
43
+ # Handle schema creation with error handling
44
+ def handle_schema_creation
45
+ PgMultitenantSchemas::Context.create_tenant_schema(self)
46
+ log_schema_operation("Created schema for tenant '#{subdomain}'")
47
+ rescue PgMultitenantSchemas::SchemaExists
48
+ log_schema_operation("Schema '#{subdomain}' already exists")
49
+ rescue StandardError => e
50
+ handle_schema_error(e, "create")
51
+ end
52
+
53
+ # Handle schema deletion with error handling
54
+ def handle_schema_deletion
55
+ PgMultitenantSchemas::Context.drop_tenant_schema(self)
56
+ log_schema_operation("Dropped schema for tenant '#{subdomain}'")
57
+ rescue PgMultitenantSchemas::SchemaNotFound
58
+ log_schema_operation("Schema '#{subdomain}' not found for deletion")
59
+ rescue StandardError => e
60
+ handle_schema_error(e, "drop")
61
+ end
62
+
63
+ # Log schema operations
64
+ def log_schema_operation(message)
65
+ Rails.logger.info "PgMultitenantSchemas: #{message}"
66
+ end
67
+
68
+ # Handle schema operation errors
69
+ def handle_schema_error(error, operation)
70
+ Rails.logger.error "PgMultitenantSchemas: Failed to #{operation} schema '#{subdomain}': #{error.message}"
71
+ raise error unless development_fallback_enabled?
72
+ end
73
+
74
+ # Check if development fallback is enabled
75
+ def development_fallback_enabled?
76
+ PgMultitenantSchemas.configuration.development_fallback && Rails.env.development?
77
+ end
78
+
79
+ class_methods do
80
+ # DSL to disable automatic schema management
81
+ def skip_schema_management
82
+ skip_callback :after_create, :create_tenant_schema_callback
83
+ skip_callback :before_destroy, :drop_tenant_schema_callback
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ module Rails
5
+ # Railtie for automatic initialization and integration with Rails applications.
6
+ # Handles schema switcher initialization when ActiveRecord loads.
7
+ class Railtie < ::Rails::Railtie
8
+ initializer "pg_multitenant_schemas.initialize" do |_app|
9
+ # Initialize connection when Rails starts
10
+ ActiveSupport.on_load(:active_record) do
11
+ PgMultitenantSchemas::SchemaSwitcher.initialize_connection
12
+ end
13
+ end
14
+
15
+ # Add rake tasks
16
+ rake_tasks do
17
+ load File.expand_path("../tasks/pg_multitenant_schemas.rake", __dir__)
18
+ end
19
+
20
+ # Add generators
21
+ generators do
22
+ require_relative "generators/install_generator"
23
+ require_relative "generators/tenant_migration_generator"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgMultitenantSchemas
4
+ # Core schema switching functionality
5
+ class SchemaSwitcher
6
+ class << self
7
+ # Initialize connection management
8
+ def initialize_connection
9
+ # This is called by the Railtie when Rails starts
10
+ # No special initialization needed as we use the configured connection class
11
+ end
12
+
13
+ # Get the database connection from the configured connection class
14
+ def connection
15
+ connection_class = PgMultitenantSchemas.configuration.connection_class
16
+ if connection_class.is_a?(String)
17
+ Object.const_get(connection_class).connection
18
+ else
19
+ connection_class.connection
20
+ end
21
+ rescue StandardError => e
22
+ raise ConnectionError, "Failed to get database connection: #{e.message}"
23
+ end
24
+
25
+ # Switches the search_path - supports both APIs for backward compatibility
26
+ def switch_schema(conn_or_schema, schema = nil)
27
+ if conn_or_schema.is_a?(String)
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?
37
+
38
+ # Use simple quoting for PostgreSQL identifiers
39
+ quoted_schema = "\"#{schema.gsub('"', '""')}\""
40
+ execute_sql(conn, "SET search_path TO #{quoted_schema};")
41
+ end
42
+
43
+ # Reset to default schema - supports both APIs
44
+ def reset_schema(conn = nil)
45
+ conn ||= connection
46
+ execute_sql(conn, "SET search_path TO public;")
47
+ end
48
+
49
+ # Create a new schema - supports both APIs
50
+ def create_schema(conn_or_schema, schema_name = nil)
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
+
60
+ raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
61
+
62
+ quoted_schema = "\"#{schema_name.gsub('"', '""')}\""
63
+ execute_sql(conn, "CREATE SCHEMA IF NOT EXISTS #{quoted_schema};")
64
+ end
65
+
66
+ # Drop a schema - supports both APIs
67
+ def drop_schema(conn_or_schema, schema_name_or_options = nil, cascade: true)
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
+
80
+ raise ArgumentError, "Schema name cannot be empty" if schema_name.nil? || schema_name.strip.empty?
81
+
82
+ cascade_option = cascade ? "CASCADE" : "RESTRICT"
83
+ quoted_schema = "\"#{schema_name.gsub('"', '""')}\""
84
+ execute_sql(conn, "DROP SCHEMA IF EXISTS #{quoted_schema} #{cascade_option};")
85
+ end
86
+
87
+ # Check if schema exists - supports both APIs
88
+ def schema_exists?(conn_or_schema, schema_name = nil)
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
+
98
+ return false if schema_name.nil? || schema_name.strip.empty?
99
+
100
+ result = execute_sql(conn, <<~SQL)
101
+ SELECT EXISTS(
102
+ SELECT 1 FROM information_schema.schemata#{" "}
103
+ WHERE schema_name = '#{schema_name}'
104
+ ) AS schema_exists
105
+ SQL
106
+ get_result_value(result, 0, 0) == "t"
107
+ end
108
+
109
+ # Get current schema - supports both APIs
110
+ def current_schema(conn = nil)
111
+ conn ||= connection
112
+ result = execute_sql(conn, "SELECT current_schema()")
113
+ get_result_value(result, 0, 0)
114
+ end
115
+
116
+ private
117
+
118
+ # Execute SQL with Rails 8 compatibility
119
+ def execute_sql(conn, sql)
120
+ if conn.respond_to?(:execute)
121
+ # Rails connection - use execute
122
+ conn.execute(sql)
123
+ else
124
+ # Raw PG connection - use exec
125
+ conn.exec(sql)
126
+ end
127
+ end
128
+
129
+ # Get value from result with compatibility for different result types
130
+ def get_result_value(result, row, col)
131
+ if result.respond_to?(:getvalue)
132
+ # PG::Result
133
+ result.getvalue(row, col)
134
+ else
135
+ # ActiveRecord::Result
136
+ result.rows[row][col]
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end