diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb index 3b63fbe8c..3991aa19a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/collection.rb @@ -18,7 +18,10 @@ def initialize(datasource, model, support_polymorphic_relations: false) end def native_driver - ActiveRecord::Base.connection + connection = ActiveRecord::Base.connection_pool.lease_connection + yield connection + ensure + ActiveRecord::Base.connection_pool.release_connection end def list(_caller, filter, projection) @@ -224,16 +227,15 @@ def resolve_habtm_through_collection(association) through_model_name = association.join_table.classify # Check if the join table exists and has an 'id' column - if ActiveRecord::Base.connection.table_exists?(join_table) - columns = ActiveRecord::Base.connection.columns(join_table) - has_id_column = columns.any? { |col| col.name == 'id' } - - if has_id_column - begin - through_model_name.constantize - rescue NameError - create_virtual_habtm_model(association, through_model_name) - end + has_id_column = ActiveRecord::Base.connection_pool.with_connection do |conn| + conn.table_exists?(join_table) && conn.columns(join_table).any? { |col| col.name == 'id' } + end + + if has_id_column + begin + through_model_name.constantize + rescue NameError + create_virtual_habtm_model(association, through_model_name) end end diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb index ac3010126..886b47502 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/datasource.rb @@ -34,8 +34,10 @@ def execute_native_query(connection_name, query, binds) end begin - connection = @live_query_connections[connection_name] - pool = ActiveRecord::Base.connects_to(database: { reading: connection.to_sym }).first + connection_spec = @live_query_connections[connection_name] + pool = @native_query_pools[connection_spec] + + raise "No connection pool found for '#{connection_spec}'" unless pool result = pool.with_connection do |conn| conn.exec_query(query, "SQL Native Query on '#{connection_name}'", binds) @@ -87,7 +89,7 @@ def fetch_model(model) def init_orm(db_config) ActiveRecord::Base.establish_connection(db_config) - current_config = ActiveRecord::Base.connection_db_config.env_name + current_config = ActiveRecord::Base.connection_pool.db_config.env_name configurations = ActiveRecord::Base.configurations .configurations .group_by(&:env_name) @@ -98,6 +100,25 @@ def init_orm(db_config) end.to_h @connection_drivers = configurations[current_config] + init_native_query_pools(current_config) + end + + def init_native_query_pools(env_name) + @native_query_pools = {} + @live_query_connections.each_value do |spec_name| + next if @native_query_pools.key?(spec_name) + + db_config = ActiveRecord::Base.configurations.configs_for( + env_name: env_name, + name: spec_name + ) + next unless db_config + + @native_query_pools[spec_name] = ActiveRecord::Base.connection_handler.establish_connection( + db_config, + owner_name: "ForestAdminNativeQuery::#{spec_name}" + ) + end end def build_habtm(model) diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb index 72e307588..33034fd6a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query.rb @@ -149,7 +149,7 @@ def compute_main_operator(condition_tree, aggregator) end # Use database-specific regex syntax - adapter_name = @collection.model.connection.adapter_name.downcase + adapter_name = @collection.model.connection_pool.db_config.adapter.downcase table_and_column = "#{arel_attr.relation.name}.#{arel_attr.name}" regex_clause = case adapter_name when 'postgresql' diff --git a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb index 333ed4e6a..6ea1a700a 100644 --- a/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb +++ b/packages/forest_admin_datasource_active_record/lib/forest_admin_datasource_active_record/utils/query_aggregate.rb @@ -72,7 +72,7 @@ def add_join_relation(relation_name) private def date_trunc_sql(operation, field, original_field_name = nil) - adapter_name = @collection.model.connection.adapter_name.downcase + adapter_name = @collection.model.connection_pool.db_config.adapter.downcase operation = operation.to_s.downcase unless VALID_DATE_OPERATIONS.include?(operation) diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb index 0db48f88b..4c78cae47 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/collection_spec.rb @@ -366,5 +366,16 @@ module ForestAdminDatasourceActiveRecord end end end + + describe '#native_driver' do + let(:datasource) { Datasource.new({ adapter: 'sqlite3', database: 'db/database.db' }) } + let(:collection) { described_class.new(datasource, Car) } + + it 'yields a connection and releases it after the block' do + collection.native_driver do |conn| + expect(conn).to be_a(ActiveRecord::ConnectionAdapters::AbstractAdapter) + end + end + end end end diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb index eb35c3335..57b575140 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/datasource_spec.rb @@ -15,10 +15,34 @@ module ForestAdminDatasourceActiveRecord expect(collections).to all(be_a(String)) end + describe '#init_orm' do + it 'uses connection_pool.db_config instead of deprecated connection_db_config' do + mock_db_config = instance_double(ActiveRecord::DatabaseConfigurations::HashConfig, env_name: 'test') + mock_pool = instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool, db_config: mock_db_config) + allow(ActiveRecord::Base).to receive_messages( + establish_connection: nil, + connection_pool: mock_pool, + configurations: instance_double(ActiveRecord::DatabaseConfigurations, configurations: []) + ) + + ds = described_class.allocate + ds.instance_variable_set(:@live_query_connections, {}) + ds.send(:init_orm, {}) + + expect(ActiveRecord::Base).to have_received(:connection_pool) + expect(mock_pool).to have_received(:db_config) + end + end + describe '#execute_native_query' do + let(:mock_pool) do + instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) + end + let(:datasource) do ds = described_class.allocate ds.instance_variable_set(:@live_query_connections, { 'main' => 'primary' }) + ds.instance_variable_set(:@native_query_pools, { 'primary' => mock_pool }) ds end @@ -38,12 +62,8 @@ module ForestAdminDatasourceActiveRecord mock_result = ActiveRecord::Result.new(['test'], [[1]]) mock_conn = instance_double(ActiveRecord::ConnectionAdapters::AbstractAdapter) allow(mock_conn).to receive(:exec_query).and_return(mock_result) - - mock_pool = instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) allow(mock_pool).to receive(:with_connection).and_yield(mock_conn) - allow(ActiveRecord::Base).to receive(:connects_to).and_return([mock_pool]) - result = datasource.execute_native_query('main', 'SELECT 1 as test', []) expect(mock_pool).to have_received(:with_connection) @@ -54,11 +74,8 @@ module ForestAdminDatasourceActiveRecord context 'when the query raises an error' do it 'wraps the error in a ForestException' do - mock_pool = instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool) allow(mock_pool).to receive(:with_connection).and_raise(StandardError, 'syntax error') - allow(ActiveRecord::Base).to receive(:connects_to).and_return([mock_pool]) - expect do datasource.execute_native_query('main', 'INVALID SQL', []) end.to raise_error( @@ -68,5 +85,34 @@ module ForestAdminDatasourceActiveRecord end end end + + describe '#init_native_query_pools' do + it 'establishes connection pools using configs_for and custom owner_name' do + mock_db_config = instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) + mock_pool = instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool, + db_config: mock_db_config) + mock_handler = instance_double(ActiveRecord::ConnectionAdapters::ConnectionHandler) + + allow(ActiveRecord::Base).to receive_messages( + configurations: instance_double( + ActiveRecord::DatabaseConfigurations, + configs_for: mock_db_config, + configurations: [] + ), + connection_handler: mock_handler + ) + allow(mock_handler).to receive(:establish_connection).and_return(mock_pool) + + ds = described_class.allocate + ds.instance_variable_set(:@live_query_connections, { 'main' => 'primary' }) + ds.send(:init_native_query_pools, 'test') + + expect(mock_handler).to have_received(:establish_connection).with( + mock_db_config, + owner_name: 'ForestAdminNativeQuery::primary' + ) + expect(ds.instance_variable_get(:@native_query_pools)).to eq({ 'primary' => mock_pool }) + end + end end end diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_aggregate_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_aggregate_spec.rb index 57bfecf79..219f933e0 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_aggregate_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_aggregate_spec.rb @@ -90,6 +90,22 @@ module Utils expect(result.size).to eq(1) end + it 'uses DATE_TRUNC when adapter is postgresql' do + db_config = collection.model.connection_pool.db_config + allow(db_config).to receive(:adapter).and_return('postgresql') + + aggregation = Aggregation.new( + operation: 'Count', + field: nil, + groups: [{ field: 'created_at', operation: 'month' }] + ) + + query_aggregate = described_class.new(collection, aggregation) + sql = query_aggregate.send(:date_trunc_sql, 'month', '"cars"."created_at"', 'created_at') + + expect(sql).to eq("DATE_TRUNC('month', \"cars\".\"created_at\")") + end + it 'raises an error when given an unsupported date truncation operation' do aggregation = Aggregation.new( operation: 'Sum', diff --git a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_spec.rb b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_spec.rb index ebe25f025..3ee10cfc9 100644 --- a/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_spec.rb +++ b/packages/forest_admin_datasource_active_record/spec/lib/forest_admin_datasource_active_record/utils/query_spec.rb @@ -279,6 +279,20 @@ module Utils expect(sql).to include('(test)') expect(sql).not_to include('/gi') end + + it 'uses ~ operator when adapter is postgresql' do + db_config = collection.model.connection_pool.db_config + allow(db_config).to receive(:adapter).and_return('postgresql') + + condition_tree = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf.new('brand', ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators::MATCH, 'Toyota|Honda') + filter = Filter.new(condition_tree: condition_tree) + query_builder = described_class.new(collection, nil, filter) + query_builder.build + + sql = query_builder.query.to_sql + expect(sql).to include('~ ') + expect(sql).not_to include('REGEXP') + end end context 'when filtering on related fields' do diff --git a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb index e43d2b2cd..e2bc7d836 100644 --- a/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb +++ b/packages/forest_admin_datasource_customizer/lib/forest_admin_datasource_customizer/context/relaxed_wrappers/relaxed_collection.rb @@ -9,8 +9,8 @@ def initialize(collection, caller) @caller = caller end - def native_driver - @collection.native_driver + def native_driver(&block) + @collection.native_driver(&block) end def schema diff --git a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb index 2e3993ab2..63937f2fa 100644 --- a/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb +++ b/packages/forest_admin_datasource_toolkit/lib/forest_admin_datasource_toolkit/decorators/collection_decorator.rb @@ -14,8 +14,8 @@ def initialize(child_collection, datasource) child_collection.parent = self if child_collection.is_a?(CollectionDecorator) end - def native_driver - child_collection.native_driver + def native_driver(&block) + child_collection.native_driver(&block) end def schema