Skip to content

Commit

Permalink
Add pg_eager_any_typed_array plugin, automatically transform eager lo…
Browse files Browse the repository at this point in the history
…ads to use = ANY(array_expr::type[]) instead of IN (value_list)

This makes it easier to use the pg_auto_parameterize_in_array
extension with the :treat_string_list_as_text_array option, by
automatically using = ANY with the correct array type (instead
of Sequel assuming the text[] type, which may not be correct).
  • Loading branch information
jeremyevans committed Feb 3, 2025
1 parent 1d94f19 commit 5336fb6
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
=== master

* Add pg_eager_any_typed_array plugin, automatically transform eager loads to use = ANY(array_expr::type[]) instead of IN (value_list) (jeremyevans)

* Support :eager_loading_predicate_transform association option (jeremyevans)

* Support explicitly specifying :use_placeholder_loader association option (jeremyevans)
Expand Down
1 change: 1 addition & 0 deletions doc/testing.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ SEQUEL_MODEL_THROW_FAILURES :: Use the throw_failures plugin when running the sp
SEQUEL_NO_CACHE_ASSOCIATIONS :: Don't cache association metadata when running the specs
SEQUEL_NO_PENDING :: Don't skip any specs, try running all specs (note, can cause lockups for some adapters)
SEQUEL_PG_AUTO_PARAMETERIZE :: Use the pg_auto_parameterize extension when running the postgres specs. Value can be +in_array+ to test the pg_auto_parameterize_in_array extension, and +in_array_string+ to test the pg_auto_parameterize_in_array extension with the +:treat_in_string_list_as_text_array+ Database option set.
SEQUEL_PG_EAGER_ANY_TYPED_ARRAY :: Use the pg_eager_any_typed_array extension when running the postgres specs.
SEQUEL_PG_TIMESTAMPTZ :: Use the pg_timestamptz extension when running the postgres specs
SEQUEL_PRIMARY_KEY_LOOKUP_CHECK_VALUES :: Use the primary_key_lookup_check_values extension when running the adapter or integration specs
SEQUEL_QUERY_PER_ASSOCIATION_DB_0_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
Expand Down
66 changes: 66 additions & 0 deletions lib/sequel/plugins/pg_eager_any_typed_array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# frozen-string-literal: true

module Sequel
module Plugins
# The pg_eager_any_typed_array plugin automatically converts
# the predicate expressions used for eager loading from:
#
# table.column IN (value_list)
#
# to:
#
# table.column = ANY(array_expr::type[])
#
# This makes it easier to use the pg_auto_parameterize_in_array
# extension with the :treat_string_list_as_text_array option,
# when using foreign keys with non-text database types that are represented
# by Ruby strings, such as enum and uuid types.
#
# To avoid this behavior for particular associations, set the
# :eager_loading_predicate_transform association option to nil/false.
#
# This plugin loads the pg_array extension into the model's Database.
module PgEagerAnyTypedArray
# Add the pg_array extension to the database
def self.apply(model)
model.db.extension(:pg_array)
end

module ClassMethods
TRANSFORM = proc do |values, ref|
type = ref.send(:cached_fetch, :_pg_eager_any_typed_array_type) do
key = ref.predicate_key
next if key.is_a?(Array)

while key.is_a?(SQL::QualifiedIdentifier)
key = key.column
end

if (sch = ref.associated_class.db_schema[key])
sch[:db_type]
end
end

if type
Sequel.function(:ANY, Sequel.pg_array(values, type))
else
values
end
end

# If the association does not use a composite predicate key,
# and does not already have the :eager_loading_predicate_transform
# option set, set the option so that eager loading
def associate(type, name, opts = OPTS, &block)
res = super

unless res.has_key?(:eager_loading_predicate_transform)
res[:eager_loading_predicate_transform] = TRANSFORM
end

res
end
end
end
end
end
1 change: 1 addition & 0 deletions spec/adapters/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

IDENTIFIER_MANGLING = !!ENV['SEQUEL_IDENTIFIER_MANGLING'] unless defined?(IDENTIFIER_MANGLING)
DB.extension(:identifier_mangling) if IDENTIFIER_MANGLING
Sequel::Model.plugin :pg_eager_any_typed_array if ENV['SEQUEL_PG_EAGER_ANY_TYPED_ARRAY']

if DB.adapter_scheme == :ibmdb || (DB.adapter_scheme == :ado && DB.database_type == :access)
def DB.drop_table(*tables)
Expand Down
48 changes: 48 additions & 0 deletions spec/extensions/pg_eager_any_typed_array_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")

describe "pg_eager_any_typed_array plugin" do
before do
@db = Sequel.connect('mock://postgres')
@c = Class.new(Sequel::Model(@db[:items]))
def @c.name; "C" end
@c.columns :id, :c_id
@c.plugin :pg_eager_any_typed_array
@c.dataset = @c.dataset.with_fetch([[{:id=>1, :c_id=>2}, {:id=>3, :c_id=>4}]])
@c.db_schema[:id][:db_type] = "int4"
@c.db_schema[:c_id][:db_type] = "int8"
@c.one_to_many :cs, :class=>@c
@c.many_to_one :c, :class=>@c
@c.many_to_one :cc, :class=>@c, :key=>[:id, :c_id], :primary_key=>[:id, :c_id]
@db.sqls
end

it "should automatically use column = ANY() for eager loads using scaler keys with known type" do
@c.eager(:c, :cs).all
@db.sqls.must_equal ['SELECT * FROM "items"',
'SELECT * FROM "items" WHERE ("items"."id" = ANY(ARRAY[2,4]::int4[]))',
'SELECT * FROM "items" WHERE ("items"."c_id" = ANY(ARRAY[1,3]::int8[]))']
end

it "should automatically use (column1, column2) IN (value_list) for eager loads using composite keys" do
@c.eager(:cc).all
@db.sqls.must_equal ['SELECT * FROM "items"', 'SELECT * FROM "items" WHERE (("items"."id", "items"."c_id") IN ((1, 2), (3, 4)))']
end

it "should automatically use column IN (value_list) for eager loads using scalar keys with unknown type" do
@c.db_schema[:id].delete(:db_type)
@c.db_schema[:c_id].delete(:db_type)
@c.eager(:c, :cs).all
@db.sqls.must_equal ['SELECT * FROM "items"',
'SELECT * FROM "items" WHERE ("items"."id" IN (2, 4))',
'SELECT * FROM "items" WHERE ("items"."c_id" IN (1, 3))']
end

it "should automatically use column IN (value_list) for eager loads when explicitly disabled" do
@c.one_to_many :cs, :class=>@c, :eager_loading_predicate_transform=>nil
@c.many_to_one :c, :class=>@c, :eager_loading_predicate_transform=>nil
@c.eager(:c, :cs).all
@db.sqls.must_equal ['SELECT * FROM "items"',
'SELECT * FROM "items" WHERE ("items"."id" IN (2, 4))',
'SELECT * FROM "items" WHERE ("items"."c_id" IN (1, 3))']
end
end
4 changes: 4 additions & 0 deletions www/pages/plugins.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@
<span class="ul__span">Adds associations types to handle the case where foreign keys are stored in a PostgreSQL array in one of the tables.</span>
</li>
<li class="ul__li ul__li--grid">
<a class="a" href="rdoc-plugins/classes/Sequel/Plugins/PgEagerAnyTypedArray.html">pg_eager_any_typed_array</a>
<span class="ul__span">Makes eager loading predicates use <code>key = ANY(array_expr::type[])</code> instead of <code>key IN (value_list)</code>.</span>
</li>
<li class="ul__li ul__li--grid">
<a class="a" href="rdoc-plugins/classes/Sequel/Plugins/RcteTree.html">rcte_tree </a>
<span class="ul__span">Supports retrieving all ancestors and descendants for tree structured data using recursive common table expressions.</span>
</li>
Expand Down

0 comments on commit 5336fb6

Please sign in to comment.