sequel 5.46.0 → 5.47.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8202d77fff48270013e8d06b75c5ab51933ff9e3fda72f99139211c9cb00de65
4
- data.tar.gz: 15393a6189c83eb324e243d81805da38deced274d3f3a1b64bbf3cee54a27fa6
3
+ metadata.gz: 191a57b6bd41c00e023891afaddbebffeb1f54017c663d2a9b32faf83584bf1f
4
+ data.tar.gz: d158aa702dfe28dbe624b551ed455615017942caf990c599f077a4fb53f6b533
5
5
  SHA512:
6
- metadata.gz: 35aa602f835100be3a01bec05ecfb3a0d53555774d1dff520963c254d79c8123328f61e2252c8cb9c69b935f015de7c53f5cbeaec378d425666e7c96bcd0dba7
7
- data.tar.gz: b52d0a0e2ea2fe6e388bd8fc694bac86d66f960cba0ed6c952213ba2414395862d3ba734b1763f13883ec7f7983cf69fe75a39a0f8e9406af46e2a2974f7210e
6
+ metadata.gz: 5c91ce33e0191d342f34dbeb4a402153bedc6d46a1df56d99b1393cf559bb9edd837e3eca8f259e1f7856990015e40098cbb269cbc9e915fa4dac9fc254c8e0c
7
+ data.tar.gz: 9d789484e942ac7380e6dbae64b3e0fb27aeaf9d3fc6764f2e6d6e86c17296cdab116cd6c8d53c429044604a429fdaf18536b6f68b728f5031a81aba81f4107b
data/CHANGELOG CHANGED
@@ -1,3 +1,17 @@
1
+ === 5.47.0 (2021-08-01)
2
+
3
+ * Make the unused_associations plugin track access to association reflections to determine whether associations are used (jeremyevans)
4
+
5
+ * Support :db option for join tables in {many,one}_through_many to use a separate query for each join table (jeremyevans)
6
+
7
+ * Support :join_table_db option for many_to_many/one_through_one associations, to use a separate query for the join table (jeremyevans)
8
+
9
+ * Support :allow_eager_graph and :allow_filtering_by association options (jeremyevans)
10
+
11
+ * Add Database#rename_tables on MySQL, for renaming multiple tables in a single call (nick96) (#1774)
12
+
13
+ * Support Dataset#returning on SQLite 3.35+ (jeremyevans)
14
+
1
15
  === 5.46.0 (2021-07-01)
2
16
 
3
17
  * Add unused_associations plugin, for determining which associations and association methods are not used (jeremyevans)
@@ -41,7 +41,7 @@ As is the code to add a related album to an artist:
41
41
 
42
42
  @artist.add_album(name: 'RF')
43
43
 
44
- It also makes it easier to creating queries that use joins based on the association:
44
+ It also makes it easier to create queries that use joins based on the association:
45
45
 
46
46
  Artist.association_join(:albums)
47
47
  # SELECT * FROM artists
@@ -63,8 +63,8 @@ It ships with additional association types via plugins.
63
63
 
64
64
  The many_to_one association is used when the table for the current class
65
65
  contains a foreign key that references the primary key in the table for the
66
- associated class. It is named because there can be many rows in the current
67
- table for each row in the associated table.
66
+ associated class. It is named 'many_to_one' because there can be many rows
67
+ in the current table for each row in the associated table.
68
68
 
69
69
  # Database schema:
70
70
  # albums artists
@@ -81,8 +81,8 @@ table for each row in the associated table.
81
81
 
82
82
  The one_to_many association is used when the table for the associated class
83
83
  contains a foreign key that references the primary key in the table for the
84
- current class. It is named because for each row in the current table there
85
- can be many rows in the associated table:
84
+ current class. It is named 'one_to_many' because for each row in the
85
+ current table there can be many rows in the associated table:
86
86
 
87
87
  The one_to_one association can be thought of as a subset of the one_to_many association,
88
88
  but where there can only be either 0 or 1 records in the associated table. This is
@@ -344,7 +344,7 @@ instance method:
344
344
 
345
345
  == Dataset Method
346
346
 
347
- In addition to the above methods, associations also add a instance method
347
+ In addition to the above methods, associations also add an instance method
348
348
  ending in +_dataset+ that returns a dataset representing the objects in the associated table:
349
349
 
350
350
  @album.artist_id
@@ -1107,7 +1107,7 @@ already applied, and the proc should return a modified copy of this dataset.
1107
1107
  Here's an example of an association of songs to artists through lyrics, where
1108
1108
  the artist can perform any one of four tasks for the lyric:
1109
1109
 
1110
- Album.one_to_many :songs, dataset: (lambda do |r|
1110
+ Artist.one_to_many :songs, dataset: (lambda do |r|
1111
1111
  r.associated_dataset.select_all(:songs).
1112
1112
  join(:lyrics, id: :lyricid, id=>[:composer_id, :arranger_id, :vocalist_id, :lyricist_id])
1113
1113
  end)
@@ -1166,6 +1166,23 @@ when deleting.
1166
1166
  ds.where(instrument_id: 5)
1167
1167
  end)
1168
1168
 
1169
+ ==== :join_table_db [+many_to_many+, +one_through_one+]
1170
+
1171
+ A Sequel::Database to use for the join table. Specifying this option switches the
1172
+ loading to use a separate query for the join table. This is useful if the
1173
+ join table is not located in the same database as the associated table, or
1174
+ if the database account with access to the associated table doesn't have
1175
+ access to the join table.
1176
+
1177
+ For example, if the Album class uses a different Sequel::Database than the Artist
1178
+ class, and the join table is in the database that the Artist class uses:
1179
+
1180
+ Artist.many_to_many :lead_guitar_albums, class: :Album, :join_table_db=>Artist.db
1181
+
1182
+ This option also affects the add/remove/remove_all methods, by changing
1183
+ which database is used for inserts/deletes from the join table (add/remove/remove_all
1184
+ defaults to use the current model's database instead of the associated model's database).
1185
+
1169
1186
  === Callback Options
1170
1187
 
1171
1188
  All callbacks can be specified as a Symbol, Proc, or array of both/either
@@ -1686,12 +1703,35 @@ If set to false, you cannot load the association eagerly via eager or
1686
1703
  eager_graph.
1687
1704
 
1688
1705
  Artist.one_to_many :albums, allow_eager: false
1689
- Artist.eager(:albums) # Raises Sequel::Error
1706
+ Artist.eager(:albums) # Raises Sequel::Error
1707
+ Artist.eager_graph(:albums) # Raises Sequel::Error
1690
1708
 
1691
1709
  This is usually used if the association dataset depends on specific values in
1692
1710
  model instance that would not be valid when eager loading for multiple
1693
1711
  instances.
1694
1712
 
1713
+ ==== :allow_eager_graph
1714
+
1715
+ If set to false, you cannot load the association eagerly via eager_graph.
1716
+
1717
+ Artist.one_to_many :albums, allow_eager_graph: false
1718
+ Artist.eager(:albums) # Allowed
1719
+ Artist.eager_graph(:albums) # Raises Sequel::Error
1720
+
1721
+ This is useful if you still want to allow loading via eager, but do not want
1722
+ to allow loading via eager graph, possibly because the association does not
1723
+ support joins.
1724
+
1725
+ ==== :allow_filtering_by
1726
+
1727
+ If set to false, you cannot use the association when filtering.
1728
+
1729
+ Artist.one_to_many :albums, allow_filtering_by: false
1730
+ Artist.where(:albums=>Album.where(:name=>'A')).all # Raises Sequel::Error
1731
+
1732
+ This is useful if such filtering cannot work, such as when a subquery cannot
1733
+ be used because the necessary tables are not in the same database.
1734
+
1695
1735
  ==== :instance_specific
1696
1736
 
1697
1737
  This allows you to override the setting of whether the dataset contains instance
data/doc/migration.rdoc CHANGED
@@ -543,16 +543,22 @@ The main difference between the two is that <tt>-d</tt> will use the type method
543
543
  with the database independent ruby class types, while <tt>-D</tt> will use
544
544
  the +column+ method with string types.
545
545
 
546
- Note that Sequel cannot dump constraints other than primary key and possibly
547
- foreign key constraints. If you are using database features such
548
- as constraints or triggers, you should use your database's dump and restore
549
- programs instead of Sequel's schema dumper.
550
-
551
546
  You can take the migration created by the schema dumper to another computer
552
547
  with an empty database, and attempt to recreate the schema using:
553
548
 
554
549
  sequel -m db/migrations postgres://host/database
555
550
 
551
+ The schema_dumper extension is quite limited in what types of
552
+ database objects it supports. In general, it only supports
553
+ dumping tables, columns, primary key and foreign key constraints,
554
+ and some indexes. It does not support most table options, CHECK
555
+ constraints, partial indexes, database functions, triggers,
556
+ security grants/revokes, and a wide variety of other useful
557
+ database properties. Be aware of the limitations when using the
558
+ schema_dumper extension. If you are dumping the schema to restore
559
+ to the same database type, it is recommended to use your database's
560
+ dump and restore programs instead of the schema_dumper extension.
561
+
556
562
  == Checking for Current Migrations
557
563
 
558
564
  In your application code, you may want to check that you are up to date in
@@ -0,0 +1,59 @@
1
+ = New Features
2
+
3
+ * Sequel now supports using separate queries for each table for both
4
+ lazy and eager loading of the following associations:
5
+
6
+ * many_to_many
7
+ * one_through_one
8
+ * many_through_many # many_through_many plugin
9
+ * one_through_many # many_through_many plugin
10
+
11
+ For many_to_many/one_through_one, you specify the :join_table_db
12
+ association option, which should be a Sequel::Database instance
13
+ containing the join table. It is possible for the current table,
14
+ join table, and associated table all to be in separate databases:
15
+
16
+ JOIN_TABLE_DB = Sequel.connect('...')
17
+ Album.many_to_many :artists, join_table_db: JOIN_TABLE_DB
18
+
19
+ For many_through_many/one_through_many, you can use the :db option
20
+ in each join table specification. All join tables can be in
21
+ separate databases:
22
+
23
+ JTDB1 = Sequel.connect('...')
24
+ JTDB2 = Sequel.connect('...')
25
+ # Tracks on all albums this artist appears on
26
+ Artist.many_through_many :album_tracks, [
27
+ {table: :albums_artists, left: :artist_id, right: :album_id, db: JTDB1},
28
+ {table: :artists, left: :id, right: :id, db: JTDB2}
29
+ ],
30
+ class: :Track, right_primary_key: :album_id
31
+
32
+ * The :allow_eager_graph association option has been added. Setting
33
+ this option to false will disallow eager loading via #eager_graph.
34
+ This is useful if you can eager load the association via #eager,
35
+ but not with #eager_graph.
36
+
37
+ * The :allow_filtering_by association option has been added. Setting
38
+ this option to false will disallow the use of filtering by
39
+ associations for the association.
40
+
41
+ * Dataset#returning is now supported on SQLite 3.35.0+. To work around
42
+ bugs in the SQLite implementation, identifiers used in the RETURNING
43
+ clause are automatically aliased. Additionally, prepared statements
44
+ that use the RETURNING clause on SQLite seem to have issues, so the
45
+ prepared_statements plugin does not automatically use prepared
46
+ statements on SQLite for queries that use the RETURNING clause.
47
+
48
+ * Database#rename_tables has been added on MySQL to support renaming
49
+ multiple tables in the same query.
50
+
51
+ = Other Improvements
52
+
53
+ * The unused_associations plugin now tracks access to the association
54
+ reflection for associations, so it will no longer show an
55
+ association as completely unused if something is accessing the
56
+ association reflection for it. This eliminates most of the false
57
+ positives, where the plugin would show an association as unused
58
+ even though something was using it without calling the association
59
+ methods.
data/doc/sql.rdoc CHANGED
@@ -428,6 +428,18 @@ As you can see, these literalize with ANDs by default. You can use the <tt>Sequ
428
428
 
429
429
  Sequel.or(column1: 1, column2: 2) # (("column1" = 1) OR ("column2" = 2))
430
430
 
431
+ As you can see in the above examples, <tt>Sequel.|</tt> and <tt>Sequel.or</tt> work differently.
432
+ <tt>Sequel.|</tt> is for combining an arbitrary number of expressions using OR. If you pass a single
433
+ argument, <tt>Sequel.|</tt> will just convert it to a Sequel expression, similar to <tt>Sequel.expr</tt>.
434
+ <tt>Sequel.or</tt> is for taking a single hash or array of two element arrays and combining the
435
+ elements of that single argument using OR instead of AND:
436
+
437
+ Sequel.|(column1: 1, column2: 2) # (("column1" = 1) AND ("column2" = 2))
438
+ Sequel.or(column1: 1, column2: 2) # (("column1" = 1) OR ("column2" = 2))
439
+
440
+ Sequel.|({column1: 1}, {column2: 2}) # (("column1" = 1) OR ("column2" = 2))
441
+ Sequel.or({column1: 1}, {column2: 2}) # ArgumentError
442
+
431
443
  You've already seen the <tt>Sequel.negate</tt> method, which will use ANDs if multiple entries are used:
432
444
 
433
445
  Sequel.negate(column1: 1, column2: 2) # (("column1" != 1) AND ("column2" != 2))
data/doc/testing.rdoc CHANGED
@@ -175,6 +175,10 @@ SEQUEL_NO_CACHE_ASSOCIATIONS :: Don't cache association metadata when running th
175
175
  SEQUEL_CHECK_PENDING :: Try running all specs (note, can cause lockups for some adapters), and raise errors for skipped specs that don't fail
176
176
  SEQUEL_NO_PENDING :: Don't skip any specs, try running all specs (note, can cause lockups for some adapters)
177
177
  SEQUEL_PG_TIMESTAMPTZ :: Use the pg_timestamptz extension when running the postgres specs
178
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_0_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
179
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_1_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
180
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_2_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
181
+ SEQUEL_QUERY_PER_ASSOCIATION_DB_3_URL :: Run query-per-association integration tests with multiple databases (all 4 must be set to run)
178
182
  SEQUEL_SPLIT_SYMBOLS :: Turn on symbol splitting when running the adapter and integration specs
179
183
  SEQUEL_SYNCHRONIZE_SQL :: Use the synchronize_sql extension when running the specs
180
184
  SEQUEL_TZINFO_VERSION :: Force the given tzinfo version when running the specs (e.g. '>=2')
@@ -187,6 +187,15 @@ module Sequel
187
187
  def views(opts=OPTS)
188
188
  full_tables('VIEW', opts)
189
189
  end
190
+
191
+ # Renames multiple tables in a single call.
192
+ #
193
+ # DB.rename_tables [:items, :old_items], [:other_items, :old_other_items]
194
+ # # RENAME TABLE items TO old_items, other_items TO old_other_items
195
+ def rename_tables(*renames)
196
+ execute_ddl(rename_tables_sql(renames))
197
+ renames.each{|from,| remove_cached_schema(from)}
198
+ end
190
199
 
191
200
  private
192
201
 
@@ -473,6 +482,14 @@ module Sequel
473
482
  schema(table).select{|a| a[1][:primary_key]}.map{|a| a[0]}
474
483
  end
475
484
 
485
+ # SQL statement for renaming multiple tables.
486
+ def rename_tables_sql(renames)
487
+ rename_tos = renames.map do |from, to|
488
+ "#{quote_schema_table(from)} TO #{quote_schema_table(to)}"
489
+ end.join(', ')
490
+ "RENAME TABLE #{rename_tos}"
491
+ end
492
+
476
493
  # Rollback the currently open XA transaction
477
494
  def rollback_transaction(conn, opts=OPTS)
478
495
  if (s = opts[:prepare]) && savepoint_level(conn) <= 1
@@ -562,10 +562,10 @@ module Sequel
562
562
  EXTRACT_MAP = {:year=>"'%Y'", :month=>"'%m'", :day=>"'%d'", :hour=>"'%H'", :minute=>"'%M'", :second=>"'%f'"}.freeze
563
563
  EXTRACT_MAP.each_value(&:freeze)
564
564
 
565
- Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
566
- Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
565
+ Dataset.def_sql_method(self, :delete, [['if db.sqlite_version >= 33500', %w'with delete from where returning'], ['elsif db.sqlite_version >= 30803', %w'with delete from where'], ["else", %w'delete from where']])
566
+ Dataset.def_sql_method(self, :insert, [['if db.sqlite_version >= 33500', %w'with insert conflict into columns values on_conflict returning'], ['elsif db.sqlite_version >= 30803', %w'with insert conflict into columns values on_conflict'], ["else", %w'insert conflict into columns values']])
567
567
  Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'with values compounds'], ['else', %w'with select distinct columns from join where group having window compounds order limit lock']])
568
- Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 33300', %w'with update table set from where'], ['elsif db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
568
+ Dataset.def_sql_method(self, :update, [['if db.sqlite_version >= 33500', %w'with update table set from where returning'], ['elsif db.sqlite_version >= 33300', %w'with update table set from where'], ['elsif db.sqlite_version >= 30803', %w'with update table set where'], ["else", %w'update table set where']])
569
569
 
570
570
  def cast_sql_append(sql, expr, type)
571
571
  if type == Time or type == DateTime
@@ -639,8 +639,8 @@ module Sequel
639
639
  # SQLite performs a TRUNCATE style DELETE if no filter is specified.
640
640
  # Since we want to always return the count of records, add a condition
641
641
  # that is always true and then delete.
642
- def delete
643
- @opts[:where] ? super : where(1=>1).delete
642
+ def delete(&block)
643
+ @opts[:where] ? super : where(1=>1).delete(&block)
644
644
  end
645
645
 
646
646
  # Return an array of strings specifying a query explanation for a SELECT of the
@@ -661,6 +661,21 @@ module Sequel
661
661
  super
662
662
  end
663
663
 
664
+ # Support insert select for associations, so that the model code can use
665
+ # returning instead of a separate query.
666
+ def insert_select(*values)
667
+ return unless supports_insert_select?
668
+ # Handle case where query does not return a row
669
+ server?(:default).with_sql_first(insert_select_sql(*values)) || false
670
+ end
671
+
672
+ # The SQL to use for an insert_select, adds a RETURNING clause to the insert
673
+ # unless the RETURNING clause is already present.
674
+ def insert_select_sql(*values)
675
+ ds = opts[:returning] ? self : returning
676
+ ds.insert_sql(*values)
677
+ end
678
+
664
679
  # SQLite uses the nonstandard ` (backtick) for quoting identifiers.
665
680
  def quoted_identifier_append(sql, c)
666
681
  sql << '`' << c.to_s.gsub('`', '``') << '`'
@@ -742,6 +757,13 @@ module Sequel
742
757
  insert_conflict(:ignore)
743
758
  end
744
759
 
760
+ # Automatically add aliases to RETURNING values to work around SQLite bug.
761
+ def returning(*values)
762
+ return super if values.empty?
763
+ raise Error, "RETURNING is not supported on #{db.database_type}" unless supports_returning?(:insert)
764
+ clone(:returning=>_returning_values(values).freeze)
765
+ end
766
+
745
767
  # SQLite 3.8.3+ supports common table expressions.
746
768
  def supports_cte?(type=:select)
747
769
  db.sqlite_version >= 30803
@@ -782,6 +804,11 @@ module Sequel
782
804
  false
783
805
  end
784
806
 
807
+ # SQLite 3.35.0 supports RETURNING on INSERT/UPDATE/DELETE.
808
+ def supports_returning?(_)
809
+ db.sqlite_version >= 33500
810
+ end
811
+
785
812
  # SQLite supports timezones in literal timestamps, since it stores them
786
813
  # as text. But using timezones in timestamps breaks SQLite datetime
787
814
  # functions, so we allow the user to override the default per database.
@@ -814,6 +841,21 @@ module Sequel
814
841
 
815
842
  private
816
843
 
844
+ # Add aliases to symbols and identifiers to work around SQLite bug.
845
+ def _returning_values(values)
846
+ values.map do |v|
847
+ case v
848
+ when Symbol
849
+ _, c, a = split_symbol(v)
850
+ a ? v : Sequel.as(v, c)
851
+ when SQL::Identifier, SQL::QualifiedIdentifier
852
+ Sequel.as(v, unqualified_column_for(v))
853
+ else
854
+ v
855
+ end
856
+ end
857
+ end
858
+
817
859
  # SQLite uses string literals instead of identifiers in AS clauses.
818
860
  def as_sql_append(sql, aliaz, column_aliases=nil)
819
861
  raise Error, "sqlite does not support derived column lists" if column_aliases
@@ -63,7 +63,7 @@ module Sequel
63
63
  # definitions using <tt>create_table</tt>, and +add_index+ accepts all the options
64
64
  # available for index definition.
65
65
  #
66
- # See <tt>Schema::AlterTableGenerator</tt> and the {"Migrations and Schema Modification" guide}[rdoc-ref:doc/migration.rdoc].
66
+ # See <tt>Schema::AlterTableGenerator</tt> and the {Migrations guide}[rdoc-ref:doc/migration.rdoc].
67
67
  def alter_table(name, &block)
68
68
  generator = alter_table_generator(&block)
69
69
  remove_cached_schema(name)
@@ -699,7 +699,7 @@ module Sequel
699
699
  end
700
700
 
701
701
  # Returns a copy of the dataset with a specified order. Can be safely combined with limit.
702
- # If you call limit with an offset, it will override override the offset if you've called
702
+ # If you call limit with an offset, it will override the offset if you've called
703
703
  # offset first.
704
704
  #
705
705
  # DB[:items].offset(10) # SELECT * FROM items OFFSET 10
@@ -6,6 +6,17 @@
6
6
  # the current database). The main interface is through
7
7
  # Sequel::Database#dump_schema_migration.
8
8
  #
9
+ # The schema_dumper extension is quite limited in what types of
10
+ # database objects it supports. In general, it only supports
11
+ # dumping tables, columns, primary key and foreign key constraints,
12
+ # and some indexes. It does not support most table options, CHECK
13
+ # constraints, partial indexes, database functions, triggers,
14
+ # security grants/revokes, and a wide variety of other useful
15
+ # database properties. Be aware of the limitations when using the
16
+ # schema_dumper extension. If you are dumping the schema to restore
17
+ # to the same database type, it is recommended to use your database's
18
+ # dump and restore programs instead of the schema_dumper extension.
19
+ #
9
20
  # To load the extension:
10
21
  #
11
22
  # DB.extension :schema_dumper
@@ -274,7 +274,9 @@ module Sequel
274
274
  cascade = eo[:associations]
275
275
  eager_limit = nil
276
276
 
277
- if eo[:eager_block] || eo[:loader] == false
277
+ if eo[:no_results]
278
+ no_results = true
279
+ elsif eo[:eager_block] || eo[:loader] == false
278
280
  ds = eager_loading_dataset(eo)
279
281
 
280
282
  strategy = ds.opts[:eager_limit_strategy] || strategy
@@ -313,7 +315,7 @@ module Sequel
313
315
  objects = loader.all(ids)
314
316
  end
315
317
 
316
- Sequel.synchronize_with(eo[:mutex]){objects.each(&block)}
318
+ Sequel.synchronize_with(eo[:mutex]){objects.each(&block)} unless no_results
317
319
 
318
320
  if strategy == :ruby
319
321
  apply_ruby_eager_limit_strategy(rows, eager_limit || limit_and_offset)
@@ -638,9 +640,7 @@ module Sequel
638
640
  # given the hash passed to the eager loader.
639
641
  def eager_loading_dataset(eo=OPTS)
640
642
  ds = eo[:dataset] || associated_eager_dataset
641
- if id_map = eo[:id_map]
642
- ds = ds.where(eager_loading_predicate_condition(id_map.keys))
643
- end
643
+ ds = eager_loading_set_predicate_condition(ds, eo)
644
644
  if associations = eo[:associations]
645
645
  ds = ds.eager(associations)
646
646
  end
@@ -667,6 +667,15 @@ module Sequel
667
667
  self[:model].default_eager_limit_strategy || :ruby
668
668
  end
669
669
 
670
+ # Set the predicate condition for the eager loading dataset based on the id map
671
+ # in the eager loading options.
672
+ def eager_loading_set_predicate_condition(ds, eo)
673
+ if id_map = eo[:id_map]
674
+ ds = ds.where(eager_loading_predicate_condition(id_map.keys))
675
+ end
676
+ ds
677
+ end
678
+
670
679
  # The predicate condition to use for the eager_loader.
671
680
  def eager_loading_predicate_condition(keys)
672
681
  {predicate_key=>keys}
@@ -1318,7 +1327,7 @@ module Sequel
1318
1327
 
1319
1328
  # many_to_many associations need to select a key in an associated table to eagerly load
1320
1329
  def eager_loading_use_associated_key?
1321
- true
1330
+ !separate_query_per_table?
1322
1331
  end
1323
1332
 
1324
1333
  # The source of the join table. This is the join table itself, unless it
@@ -1375,10 +1384,30 @@ module Sequel
1375
1384
  cached_fetch(:select){default_select}
1376
1385
  end
1377
1386
 
1387
+ # Whether a separate query should be used for the join table.
1388
+ def separate_query_per_table?
1389
+ self[:join_table_db]
1390
+ end
1391
+
1378
1392
  private
1379
1393
 
1394
+ # Join to the the join table, unless using a separate query per table.
1380
1395
  def _associated_dataset
1381
- super.inner_join(self[:join_table], self[:right_keys].zip(right_primary_keys), :qualify=>:deep)
1396
+ if separate_query_per_table?
1397
+ super
1398
+ else
1399
+ super.inner_join(self[:join_table], self[:right_keys].zip(right_primary_keys), :qualify=>:deep)
1400
+ end
1401
+ end
1402
+
1403
+ # Use the right_keys from the eager loading options if
1404
+ # using a separate query per table.
1405
+ def eager_loading_set_predicate_condition(ds, eo)
1406
+ if separate_query_per_table?
1407
+ ds.where(right_primary_key=>eo[:right_keys])
1408
+ else
1409
+ super
1410
+ end
1382
1411
  end
1383
1412
 
1384
1413
  # The default selection for associations that require joins. These do not use the default
@@ -1606,6 +1635,8 @@ module Sequel
1606
1635
  # after an item is set using the association setter method.
1607
1636
  # :allow_eager :: If set to false, you cannot load the association eagerly
1608
1637
  # via eager or eager_graph
1638
+ # :allow_eager_graph :: If set to false, you cannot load the association eagerly via eager_graph.
1639
+ # :allow_filtering_by :: If set to false, you cannot use the association when filtering
1609
1640
  # :before_add :: Symbol, Proc, or array of both/either specifying a callback to call
1610
1641
  # before a new item is added to the association.
1611
1642
  # :before_remove :: Symbol, Proc, or array of both/either specifying a callback to call
@@ -1773,6 +1804,9 @@ module Sequel
1773
1804
  # underscored, sorted, and joined with '_'.
1774
1805
  # :join_table_block :: proc that can be used to modify the dataset used in the add/remove/remove_all
1775
1806
  # methods. Should accept a dataset argument and return a modified dataset if present.
1807
+ # :join_table_db :: When retrieving records when using lazy loading or eager loading via +eager+, instead of
1808
+ # a join between to the join table and the associated table, use a separate query for the
1809
+ # join table using the given Database object.
1776
1810
  # :left_key :: foreign key in join table that points to current model's
1777
1811
  # primary key, as a symbol. Defaults to :"#{self.name.underscore}_id".
1778
1812
  # Can use an array of symbols for a composite key association.
@@ -2045,7 +2079,7 @@ module Sequel
2045
2079
  raise(Error, "mismatched number of right keys: #{rcks.inspect} vs #{rcpks.inspect}") unless rcks.length == rcpks.length
2046
2080
  end
2047
2081
  opts[:uses_left_composite_keys] = lcks.length > 1
2048
- opts[:uses_right_composite_keys] = rcks.length > 1
2082
+ uses_rcks = opts[:uses_right_composite_keys] = rcks.length > 1
2049
2083
  opts[:cartesian_product_number] ||= one_through_one ? 0 : 1
2050
2084
  join_table = (opts[:join_table] ||= opts.default_join_table)
2051
2085
  opts[:left_key_alias] ||= opts.default_associated_key_alias
@@ -2054,8 +2088,75 @@ module Sequel
2054
2088
  opts[:after_load] ||= []
2055
2089
  opts[:after_load].unshift(:array_uniq!)
2056
2090
  end
2057
- opts[:dataset] ||= opts.association_dataset_proc
2058
- opts[:eager_loader] ||= opts.method(:default_eager_loader)
2091
+ if join_table_db = opts[:join_table_db]
2092
+ opts[:use_placeholder_loader] = false
2093
+ opts[:allow_eager_graph] = false
2094
+ opts[:allow_filtering_by] = false
2095
+ opts[:eager_limit_strategy] = nil
2096
+ join_table_ds = join_table_db.from(join_table)
2097
+ opts[:dataset] ||= proc do |r|
2098
+ vals = join_table_ds.where(lcks.zip(lcpks.map{|k| get_column_value(k)})).select_map(right)
2099
+ ds = r.associated_dataset.where(opts.right_primary_key => vals)
2100
+ if uses_rcks
2101
+ vals.delete_if{|v| v.any?(&:nil?)}
2102
+ else
2103
+ vals.delete(nil)
2104
+ end
2105
+ ds = ds.clone(:no_results=>true) if vals.empty?
2106
+ ds
2107
+ end
2108
+ opts[:eager_loader] ||= proc do |eo|
2109
+ h = eo[:id_map]
2110
+ assign_singular = opts.assign_singular?
2111
+ rpk = opts.right_primary_key
2112
+ name = opts[:name]
2113
+
2114
+ join_map = join_table_ds.where(left=>h.keys).select_hash_groups(right, left)
2115
+
2116
+ if uses_rcks
2117
+ join_map.delete_if{|v,| v.any?(&:nil?)}
2118
+ else
2119
+ join_map.delete(nil)
2120
+ end
2121
+
2122
+ eo = Hash[eo]
2123
+
2124
+ if join_map.empty?
2125
+ eo[:no_results] = true
2126
+ else
2127
+ join_map.each_value do |vs|
2128
+ vs.replace(vs.flat_map{|v| h[v]})
2129
+ vs.uniq!
2130
+ end
2131
+
2132
+ eo[:loader] = false
2133
+ eo[:right_keys] = join_map.keys
2134
+ end
2135
+
2136
+ opts[:model].eager_load_results(opts, eo) do |assoc_record|
2137
+ rpkv = if uses_rcks
2138
+ assoc_record.values.values_at(*rpk)
2139
+ else
2140
+ assoc_record.values[rpk]
2141
+ end
2142
+
2143
+ objects = join_map[rpkv]
2144
+
2145
+ if assign_singular
2146
+ objects.each do |object|
2147
+ object.associations[name] ||= assoc_record
2148
+ end
2149
+ else
2150
+ objects.each do |object|
2151
+ object.associations[name].push(assoc_record)
2152
+ end
2153
+ end
2154
+ end
2155
+ end
2156
+ else
2157
+ opts[:dataset] ||= opts.association_dataset_proc
2158
+ opts[:eager_loader] ||= opts.method(:default_eager_loader)
2159
+ end
2059
2160
 
2060
2161
  join_type = opts[:graph_join_type]
2061
2162
  select = opts[:graph_select]
@@ -2417,7 +2518,7 @@ module Sequel
2417
2518
 
2418
2519
  # Dataset for the join table of the given many to many association reflection
2419
2520
  def _join_table_dataset(opts)
2420
- ds = model.db.from(opts.join_table_source)
2521
+ ds = (opts[:join_table_db] || model.db).from(opts.join_table_source)
2421
2522
  opts[:join_table_block] ? opts[:join_table_block].call(ds) : ds
2422
2523
  end
2423
2524
 
@@ -2438,7 +2539,12 @@ module Sequel
2438
2539
  if loader = _associated_object_loader(opts, dynamic_opts)
2439
2540
  loader.all(*opts.predicate_key_values(self))
2440
2541
  else
2441
- _associated_dataset(opts, dynamic_opts).all
2542
+ ds = _associated_dataset(opts, dynamic_opts)
2543
+ if ds.opts[:no_results]
2544
+ []
2545
+ else
2546
+ ds.all
2547
+ end
2442
2548
  end
2443
2549
  end
2444
2550
 
@@ -2899,6 +3005,8 @@ module Sequel
2899
3005
  (multiple = ((op == :IN || op == :'NOT IN') && ((is_ds = r.is_a?(Sequel::Dataset)) || (r.respond_to?(:all?) && r.all?{|x| x.is_a?(Sequel::Model)})))))
2900
3006
  l = args[0]
2901
3007
  if ar = model.association_reflections[l]
3008
+ raise Error, "filtering by associations is not allowed for #{ar.inspect}" if ar[:allow_filtering_by] == false
3009
+
2902
3010
  if multiple
2903
3011
  klass = ar.associated_class
2904
3012
  if is_ds
@@ -3394,7 +3502,7 @@ module Sequel
3394
3502
  # Allow associations that are eagerly graphed to be specified as an SQL::AliasedExpression, for
3395
3503
  # per-call determining of the alias base.
3396
3504
  def eager_graph_check_association(model, association)
3397
- if association.is_a?(SQL::AliasedExpression)
3505
+ reflection = if association.is_a?(SQL::AliasedExpression)
3398
3506
  expr = association.expression
3399
3507
  if expr.is_a?(SQL::Identifier)
3400
3508
  expr = expr.value
@@ -3403,10 +3511,17 @@ module Sequel
3403
3511
  end
3404
3512
  end
3405
3513
 
3406
- SQL::AliasedExpression.new(check_association(model, expr), association.alias || expr, association.columns)
3514
+ check_reflection = check_association(model, expr)
3515
+ SQL::AliasedExpression.new(check_reflection, association.alias || expr, association.columns)
3407
3516
  else
3408
- check_association(model, association)
3517
+ check_reflection = check_association(model, association)
3518
+ end
3519
+
3520
+ if check_reflection && check_reflection[:allow_eager_graph] == false
3521
+ raise Error, "eager_graph not allowed for #{reflection.inspect}"
3409
3522
  end
3523
+
3524
+ reflection
3410
3525
  end
3411
3526
 
3412
3527
  # The EagerGraphLoader instance used for converting eager_graph results.
@@ -123,16 +123,25 @@ module Sequel
123
123
  nil
124
124
  end
125
125
 
126
+ # Whether a separate query should be used for each join table.
127
+ def separate_query_per_table?
128
+ self[:separate_query_per_table]
129
+ end
130
+
126
131
  private
127
132
 
128
133
  def _associated_dataset
129
134
  ds = associated_class
130
- (reverse_edges + [final_reverse_edge]).each do |t|
131
- h = {:qualify=>:deep}
132
- if t[:alias] != t[:table]
133
- h[:table_alias] = t[:alias]
135
+ if separate_query_per_table?
136
+ ds = ds.dataset
137
+ else
138
+ (reverse_edges + [final_reverse_edge]).each do |t|
139
+ h = {:qualify=>:deep}
140
+ if t[:alias] != t[:table]
141
+ h[:table_alias] = t[:alias]
142
+ end
143
+ ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), h)
134
144
  end
135
- ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), h)
136
145
  end
137
146
  ds
138
147
  end
@@ -208,6 +217,7 @@ module Sequel
208
217
  # :right (last array element) :: The key joining the table to the next table. Can use an
209
218
  # array of symbols for a composite key association.
210
219
  # If a hash is provided, the following keys are respected when using eager_graph:
220
+ # :db :: The Database containing the table. This changes lookup to use a separate query for each join table.
211
221
  # :block :: A proc to use as the block argument to join.
212
222
  # :conditions :: Extra conditions to add to the JOIN ON clause. Must be a hash or array of two pairs.
213
223
  # :join_type :: The join type to use for the join, defaults to :left_outer.
@@ -233,32 +243,121 @@ module Sequel
233
243
  opts[:after_load].unshift(:array_uniq!)
234
244
  end
235
245
  opts[:cartesian_product_number] ||= one_through_many ? 0 : 2
236
- opts[:through] = opts[:through].map do |e|
246
+ separate_query_per_table = false
247
+ through = opts[:through] = opts[:through].map do |e|
237
248
  case e
238
249
  when Array
239
250
  raise(Error, "array elements of the through option/argument for many_through_many associations must have at least three elements") unless e.length == 3
240
251
  {:table=>e[0], :left=>e[1], :right=>e[2]}
241
252
  when Hash
242
253
  raise(Error, "hash elements of the through option/argument for many_through_many associations must contain :table, :left, and :right keys") unless e[:table] && e[:left] && e[:right]
254
+ separate_query_per_table = true if e[:db]
243
255
  e
244
256
  else
245
257
  raise(Error, "the through option/argument for many_through_many associations must be an enumerable of arrays or hashes")
246
258
  end
247
259
  end
260
+ opts[:separate_query_per_table] = separate_query_per_table
248
261
 
249
262
  left_key = opts[:left_key] = opts[:through].first[:left]
250
263
  opts[:left_keys] = Array(left_key)
251
- opts[:uses_left_composite_keys] = left_key.is_a?(Array)
264
+ uses_lcks = opts[:uses_left_composite_keys] = left_key.is_a?(Array)
252
265
  left_pk = (opts[:left_primary_key] ||= self.primary_key)
253
266
  raise(Error, "no primary key specified for #{inspect}") unless left_pk
254
267
  opts[:eager_loader_key] = left_pk unless opts.has_key?(:eager_loader_key)
255
268
  opts[:left_primary_keys] = Array(left_pk)
256
269
  lpkc = opts[:left_primary_key_column] ||= left_pk
257
270
  lpkcs = opts[:left_primary_key_columns] ||= Array(lpkc)
258
- opts[:dataset] ||= opts.association_dataset_proc
259
271
 
260
272
  opts[:left_key_alias] ||= opts.default_associated_key_alias
261
- opts[:eager_loader] ||= opts.method(:default_eager_loader)
273
+ if separate_query_per_table
274
+ opts[:use_placeholder_loader] = false
275
+ opts[:allow_eager_graph] = false
276
+ opts[:allow_filtering_by] = false
277
+ opts[:eager_limit_strategy] = nil
278
+
279
+ opts[:dataset] ||= proc do |r|
280
+ def_db = r.associated_class.db
281
+ vals = uses_lcks ? [lpkcs.map{|k| get_column_value(k)}] : get_column_value(left_pk)
282
+
283
+ has_results = through.each do |edge|
284
+ ds = (edge[:db] || def_db).from(edge[:table]).where(edge[:left]=>vals)
285
+ ds = ds.where(edge[:conditions]) if edge[:conditions]
286
+ right = edge[:right]
287
+ vals = ds.select_map(right)
288
+ if right.is_a?(Array)
289
+ vals.delete_if{|v| v.any?(&:nil?)}
290
+ else
291
+ vals.delete(nil)
292
+ end
293
+ break if vals.empty?
294
+ end
295
+
296
+ ds = r.associated_dataset.where(opts.right_primary_key=>vals)
297
+ ds = ds.clone(:no_results=>true) unless has_results
298
+ ds
299
+ end
300
+ opts[:eager_loader] ||= proc do |eo|
301
+ h = eo[:id_map]
302
+ assign_singular = opts.assign_singular?
303
+ uses_rcks = opts.right_primary_key.is_a?(Array)
304
+ rpk = uses_rcks ? opts.right_primary_keys : opts.right_primary_key
305
+ name = opts[:name]
306
+ def_db = opts.associated_class.db
307
+ join_map = h
308
+
309
+ run_query = through.each do |edge|
310
+ ds = (edge[:db] || def_db).from(edge[:table])
311
+ ds = ds.where(edge[:conditions]) if edge[:conditions]
312
+ left = edge[:left]
313
+ right = edge[:right]
314
+ prev_map = join_map
315
+ join_map = ds.where(left=>join_map.keys).select_hash_groups(right, left)
316
+ if right.is_a?(Array)
317
+ join_map.delete_if{|v,| v.any?(&:nil?)}
318
+ else
319
+ join_map.delete(nil)
320
+ end
321
+ break if join_map.empty?
322
+ join_map.each_value do |vs|
323
+ vs.replace(vs.flat_map{|v| prev_map[v]})
324
+ vs.uniq!
325
+ end
326
+ end
327
+
328
+ eo = Hash[eo]
329
+
330
+ if run_query
331
+ eo[:loader] = false
332
+ eo[:right_keys] = join_map.keys
333
+ else
334
+ eo[:no_results] = true
335
+ end
336
+
337
+ opts[:model].eager_load_results(opts, eo) do |assoc_record|
338
+ rpkv = if uses_rcks
339
+ assoc_record.values.values_at(*rpk)
340
+ else
341
+ assoc_record.values[rpk]
342
+ end
343
+
344
+ objects = join_map[rpkv]
345
+
346
+ if assign_singular
347
+ objects.each do |object|
348
+ object.associations[name] ||= assoc_record
349
+ end
350
+ else
351
+ objects.each do |object|
352
+ object.associations[name].push(assoc_record)
353
+ end
354
+ end
355
+ end
356
+ end
357
+ else
358
+ opts[:dataset] ||= opts.association_dataset_proc
359
+ opts[:eager_loader] ||= opts.method(:default_eager_loader)
360
+ end
262
361
 
263
362
  join_type = opts[:graph_join_type]
264
363
  select = opts[:graph_select]
@@ -169,8 +169,17 @@ module Sequel
169
169
  end
170
170
 
171
171
  case type
172
- when :insert, :insert_select, :update
172
+ when :insert, :update
173
173
  true
174
+ when :insert_select
175
+ # SQLite RETURNING support has a bug that doesn't allow for committing transactions
176
+ # when a prepared statement with RETURNING has been used on the connection:
177
+ #
178
+ # SQLite3::BusyException: cannot commit transaction - SQL statements in progress: COMMIT
179
+ #
180
+ # Disabling usage of prepared statements for insert_select on SQLite seems to be the
181
+ # simplest way to workaround the problem.
182
+ db.database_type != :sqlite
174
183
  # :nocov:
175
184
  when :delete, :refresh
176
185
  Sequel::Deprecation.deprecate("The :delete and :refresh prepared statement types", "There should be no need to check if these types are supported")
@@ -19,7 +19,7 @@ module Sequel
19
19
  #
20
20
  # # Timestamp Artist instances, forcing an overwrite of the create
21
21
  # # timestamp, and setting the update timestamp when creating
22
- # Album.plugin :timestamps, force: true, update_on_create: true
22
+ # Artist.plugin :timestamps, force: true, update_on_create: true
23
23
  module Timestamps
24
24
  # Configure the plugin by setting the available options. Note that
25
25
  # if this method is run more than once, previous settings are ignored,
@@ -214,11 +214,11 @@ module Sequel
214
214
  # reported by this plugin.
215
215
  #
216
216
  # This plugin only considers the public instance methods the
217
- # association defines to determine if it was used. If an
218
- # association is used in a way that does not call an instance
219
- # method (such as only using the association with the
220
- # dataset_associations plugin), then it would show up as unused
221
- # by this plugin.
217
+ # association defines, and direct access to the related
218
+ # association reflection via Sequel::Model.association_reflection
219
+ # to determine if the association was used. If the association
220
+ # metadata was accessed another way, it's possible this plugin
221
+ # will show the association as unused.
222
222
  #
223
223
  # As this relies on the method coverage added in Ruby 2.5, it does
224
224
  # not work on older versions of Ruby. It also does not work on
@@ -266,6 +266,18 @@ module Sequel
266
266
  # unused_associations only on the class that is loading the plugin.
267
267
  Plugins.inherited_instance_variables(self, :@unused_associations_data=>nil)
268
268
 
269
+ # Synchronize access to the used association reflections.
270
+ def used_association_reflections
271
+ Sequel.synchronize{@used_association_reflections ||= {}}
272
+ end
273
+
274
+ # Record access to association reflections to determine which associations are not used.
275
+ def association_reflection(association)
276
+ uar = used_association_reflections
277
+ Sequel.synchronize{uar[association] ||= true}
278
+ super
279
+ end
280
+
269
281
  # If modifying associations, and this association is marked as not used,
270
282
  # and the association does not include the specific :is_used option,
271
283
  # skip defining the association.
@@ -277,6 +289,12 @@ module Sequel
277
289
  super
278
290
  end
279
291
 
292
+ # Setup the used_association_reflections storage before freezing
293
+ def freeze
294
+ used_association_reflections
295
+ super
296
+ end
297
+
280
298
  # Parse the coverage result, and return the coverage data for the
281
299
  # associations for descendants of this class. If the plugin
282
300
  # uses the :coverage_file option, the existing coverage file will be loaded
@@ -298,7 +316,7 @@ module Sequel
298
316
  ([self] + descendents).each do |sc|
299
317
  next if sc.associations.empty? || !sc.name
300
318
  module_mapping[sc.send(:overridable_methods_module)] = sc
301
- coverage_data[sc.name] ||= {}
319
+ coverage_data[sc.name] ||= {''=>sc.used_association_reflections.keys.map(&:to_s).sort}
302
320
  end
303
321
 
304
322
  coverage_result.each do |file, coverage|
@@ -331,10 +349,9 @@ module Sequel
331
349
 
332
350
  ([self] + descendents).each do |sc|
333
351
  next unless cov_data = coverage_data[sc.name]
352
+ reflection_data = cov_data[''] || []
334
353
 
335
- sc.associations.each do |assoc|
336
- ref = sc.association_reflection(assoc)
337
-
354
+ sc.association_reflections.each do |assoc, ref|
338
355
  # Only report associations for the class they are defined in
339
356
  next unless ref[:model] == sc
340
357
 
@@ -343,6 +360,9 @@ module Sequel
343
360
  next if ref[:methods_module]
344
361
 
345
362
  info = {}
363
+ if reflection_data.include?(assoc.to_s)
364
+ info[:used] = [:reflection]
365
+ end
346
366
 
347
367
  _update_association_coverage_info(info, cov_data, ref.dataset_method, :dataset_method)
348
368
  _update_association_coverage_info(info, cov_data, ref.association_method, :association_method)
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 46
9
+ MINOR = 47
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.46.0
4
+ version: 5.47.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-01 00:00:00.000000000 Z
11
+ date: 2021-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -190,6 +190,7 @@ extra_rdoc_files:
190
190
  - doc/release_notes/5.44.0.txt
191
191
  - doc/release_notes/5.45.0.txt
192
192
  - doc/release_notes/5.46.0.txt
193
+ - doc/release_notes/5.47.0.txt
193
194
  - doc/release_notes/5.5.0.txt
194
195
  - doc/release_notes/5.6.0.txt
195
196
  - doc/release_notes/5.7.0.txt
@@ -264,6 +265,7 @@ files:
264
265
  - doc/release_notes/5.44.0.txt
265
266
  - doc/release_notes/5.45.0.txt
266
267
  - doc/release_notes/5.46.0.txt
268
+ - doc/release_notes/5.47.0.txt
267
269
  - doc/release_notes/5.5.0.txt
268
270
  - doc/release_notes/5.6.0.txt
269
271
  - doc/release_notes/5.7.0.txt
@@ -575,7 +577,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
575
577
  - !ruby/object:Gem::Version
576
578
  version: '0'
577
579
  requirements: []
578
- rubygems_version: 3.2.15
580
+ rubygems_version: 3.2.22
579
581
  signing_key:
580
582
  specification_version: 4
581
583
  summary: The Database Toolkit for Ruby