sequel 5.11.0 → 5.12.0

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/advanced_associations.rdoc +132 -14
  4. data/doc/postgresql.rdoc +14 -0
  5. data/doc/release_notes/5.12.0.txt +141 -0
  6. data/lib/sequel/adapters/ado/mssql.rb +1 -1
  7. data/lib/sequel/adapters/oracle.rb +5 -6
  8. data/lib/sequel/adapters/postgres.rb +18 -5
  9. data/lib/sequel/adapters/shared/mysql.rb +5 -5
  10. data/lib/sequel/adapters/sqlite.rb +0 -5
  11. data/lib/sequel/adapters/tinytds.rb +0 -5
  12. data/lib/sequel/adapters/utils/emulate_offset_with_reverse_and_count.rb +2 -5
  13. data/lib/sequel/core.rb +6 -1
  14. data/lib/sequel/dataset/graph.rb +25 -9
  15. data/lib/sequel/dataset/placeholder_literalizer.rb +47 -17
  16. data/lib/sequel/dataset/prepared_statements.rb +86 -18
  17. data/lib/sequel/dataset/sql.rb +5 -1
  18. data/lib/sequel/extensions/caller_logging.rb +79 -0
  19. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  20. data/lib/sequel/extensions/pg_static_cache_updater.rb +2 -2
  21. data/lib/sequel/model/associations.rb +56 -23
  22. data/lib/sequel/model/base.rb +3 -3
  23. data/lib/sequel/plugins/eager_graph_eager.rb +139 -0
  24. data/lib/sequel/plugins/static_cache.rb +9 -8
  25. data/lib/sequel/plugins/tactical_eager_loading.rb +63 -1
  26. data/lib/sequel/version.rb +1 -1
  27. data/spec/adapters/oracle_spec.rb +44 -0
  28. data/spec/adapters/postgres_spec.rb +39 -0
  29. data/spec/core/dataset_spec.rb +23 -9
  30. data/spec/core/object_graph_spec.rb +314 -284
  31. data/spec/extensions/caller_logging_spec.rb +52 -0
  32. data/spec/extensions/eager_graph_eager_spec.rb +100 -0
  33. data/spec/extensions/finder_spec.rb +1 -1
  34. data/spec/extensions/prepared_statements_spec.rb +7 -12
  35. data/spec/extensions/static_cache_spec.rb +14 -0
  36. data/spec/extensions/tactical_eager_loading_spec.rb +262 -1
  37. data/spec/integration/associations_test.rb +72 -0
  38. data/spec/integration/dataset_test.rb +3 -3
  39. data/spec/model/eager_loading_spec.rb +90 -0
  40. metadata +8 -2
@@ -431,7 +431,7 @@ module Sequel
431
431
  end
432
432
  end
433
433
 
434
- ds = from(:sequel_constraint_validations)
434
+ ds = from(constraint_validations_table)
435
435
  unless drop_rows.empty?
436
436
  ds.where([:table, :constraint_name]=>drop_rows).delete
437
437
  end
@@ -121,7 +121,7 @@ SQL
121
121
 
122
122
  oid_map = {}
123
123
  models.each do |model|
124
- raise Error, "#{model.inspect} does not use the static_cache plugin" unless model.respond_to?(:load_cache, true)
124
+ raise Error, "#{model.inspect} does not use the static_cache plugin" unless model.respond_to?(:load_cache)
125
125
  oid_map[get(regclass_oid(model.dataset.first_source_table))] = model
126
126
  end
127
127
 
@@ -129,7 +129,7 @@ SQL
129
129
  begin
130
130
  listen(opts[:channel_name]||default_static_cache_update_name, {:loop=>true}.merge!(opts)) do |_, _, oid|
131
131
  if model = oid_map[oid.to_i]
132
- model.send(:load_cache)
132
+ model.load_cache
133
133
  end
134
134
  end
135
135
  ensure
@@ -633,10 +633,15 @@ module Sequel
633
633
  ds = ds.eager(associations)
634
634
  end
635
635
  if block = eo[:eager_block]
636
+ orig_ds = ds
636
637
  ds = block.call(ds)
637
638
  end
638
639
  if eager_loading_use_associated_key?
639
- ds = ds.select_append(*associated_key_array)
640
+ ds = if ds.opts[:eager_graph] && !orig_ds.opts[:eager_graph]
641
+ block.call(orig_ds.select_append(*associated_key_array))
642
+ else
643
+ ds.select_append(*associated_key_array)
644
+ end
640
645
  end
641
646
  if self[:eager_graph]
642
647
  raise(Error, "cannot eagerly load a #{self[:type]} association that uses :eager_graph") if eager_loading_use_associated_key?
@@ -2949,7 +2954,7 @@ module Sequel
2949
2954
  # Replace the array of plain hashes with an array of model objects will all eager_graphed
2950
2955
  # associations set in the associations cache for each object.
2951
2956
  def eager_graph_build_associations(hashes)
2952
- hashes.replace(EagerGraphLoader.new(self).load(hashes))
2957
+ hashes.replace(_eager_graph_build_associations(hashes, eager_graph_loader))
2953
2958
  end
2954
2959
 
2955
2960
  private
@@ -2960,6 +2965,12 @@ module Sequel
2960
2965
  clone(:join=>clone(:graph_from_self=>false).eager_graph_with_options(associations, :join_type=>type, :join_only=>true).opts[:join])
2961
2966
  end
2962
2967
 
2968
+ # Process the array of hashes using the eager graph loader to return an array
2969
+ # of model objects with the associations set.
2970
+ def _eager_graph_build_associations(hashes, egl)
2971
+ egl.load(hashes)
2972
+ end
2973
+
2963
2974
  # If the association has conditions itself, then it requires additional filters be
2964
2975
  # added to the current dataset to ensure that the passed in object would also be
2965
2976
  # included by the association's conditions.
@@ -3051,6 +3062,14 @@ module Sequel
3051
3062
  end
3052
3063
  end
3053
3064
 
3065
+ # The EagerGraphLoader instance used for converting eager_graph results.
3066
+ def eager_graph_loader
3067
+ unless egl = cache_get(:_model_eager_graph_loader)
3068
+ egl = cache_set(:_model_eager_graph_loader, EagerGraphLoader.new(self))
3069
+ end
3070
+ egl.dup
3071
+ end
3072
+
3054
3073
  # Eagerly load all specified associations
3055
3074
  def eager_load(a, eager_assoc=@opts[:eager])
3056
3075
  return if a.empty?
@@ -3238,12 +3257,15 @@ module Sequel
3238
3257
  :offset
3239
3258
  end
3240
3259
  end
3260
+ after_load_map.freeze
3261
+ alias_map.freeze
3262
+ type_map.freeze
3241
3263
 
3242
3264
  # Make dependency map hash out of requirements array for each association.
3243
3265
  # This builds a tree of dependencies that will be used for recursion
3244
3266
  # to ensure that all parts of the object graph are loaded into the
3245
3267
  # appropriate subordinate association.
3246
- @dependency_map = {}
3268
+ dependency_map = @dependency_map = {}
3247
3269
  # Sort the associations by requirements length, so that
3248
3270
  # requirements are added to the dependency hash before their
3249
3271
  # dependencies.
@@ -3259,18 +3281,12 @@ module Sequel
3259
3281
  hash[ta] = {}
3260
3282
  end
3261
3283
  end
3284
+ freezer = lambda do |h|
3285
+ h.freeze
3286
+ h.each_value(&freezer)
3287
+ end
3288
+ freezer.call(dependency_map)
3262
3289
 
3263
- # This mapping is used to make sure that duplicate entries in the
3264
- # result set are mapped to a single record. For example, using a
3265
- # single one_to_many association with 10 associated records,
3266
- # the main object column values appear in the object graph 10 times.
3267
- # We map by primary key, if available, or by the object's entire values,
3268
- # if not. The mapping must be per table, so create sub maps for each table
3269
- # alias.
3270
- records_map = {@master=>{}}
3271
- alias_map.keys.each{|ta| records_map[ta] = {}}
3272
- @records_map = records_map
3273
-
3274
3290
  datasets = opts[:graph][:table_aliases].to_a.reject{|ta,ds| ds.nil?}
3275
3291
  column_aliases = opts[:graph][:column_aliases]
3276
3292
  primary_keys = {}
@@ -3296,9 +3312,9 @@ module Sequel
3296
3312
  h.select{|ca, c| primary_keys[ta] = ca if pk == c}
3297
3313
  end
3298
3314
  end
3299
- @column_maps = column_maps
3300
- @primary_keys = primary_keys
3301
- @row_procs = row_procs
3315
+ @column_maps = column_maps.freeze
3316
+ @primary_keys = primary_keys.freeze
3317
+ @row_procs = row_procs.freeze
3302
3318
 
3303
3319
  # For performance, create two special maps for the master table,
3304
3320
  # so you can skip a hash lookup.
@@ -3310,22 +3326,35 @@ module Sequel
3310
3326
  # used for performance, to get all values in one hash lookup instead of
3311
3327
  # separate hash lookups for each data structure.
3312
3328
  ta_map = {}
3313
- alias_map.keys.each do |ta|
3314
- ta_map[ta] = [records_map[ta], row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]]
3329
+ alias_map.each_key do |ta|
3330
+ ta_map[ta] = [row_procs[ta], alias_map[ta], type_map[ta], reciprocal_map[ta]].freeze
3315
3331
  end
3316
- @ta_map = ta_map
3332
+ @ta_map = ta_map.freeze
3333
+ freeze
3317
3334
  end
3318
3335
 
3319
3336
  # Return an array of primary model instances with the associations cache prepopulated
3320
3337
  # for all model objects (both primary and associated).
3321
3338
  def load(hashes)
3339
+ # This mapping is used to make sure that duplicate entries in the
3340
+ # result set are mapped to a single record. For example, using a
3341
+ # single one_to_many association with 10 associated records,
3342
+ # the main object column values appear in the object graph 10 times.
3343
+ # We map by primary key, if available, or by the object's entire values,
3344
+ # if not. The mapping must be per table, so create sub maps for each table
3345
+ # alias.
3346
+ @records_map = records_map = {}
3347
+ alias_map.keys.each{|ta| records_map[ta] = {}}
3348
+
3322
3349
  master = master()
3323
3350
 
3324
3351
  # Assign to local variables for speed increase
3325
3352
  rp = row_procs[master]
3326
- rm = records_map[master]
3353
+ rm = records_map[master] = {}
3327
3354
  dm = dependency_map
3328
3355
 
3356
+ records_map.freeze
3357
+
3329
3358
  # This will hold the final record set that we will be replacing the object graph with.
3330
3359
  records = []
3331
3360
 
@@ -3346,6 +3375,9 @@ module Sequel
3346
3375
  # Run after_load procs if there are any
3347
3376
  post_process(records, dm) if @unique || !after_load_map.empty? || !limit_map.empty?
3348
3377
 
3378
+ records_map.each_value(&:freeze)
3379
+ freeze
3380
+
3349
3381
  records
3350
3382
  end
3351
3383
 
@@ -3365,13 +3397,14 @@ module Sequel
3365
3397
  end
3366
3398
  key = hkey(ta_h)
3367
3399
  end
3368
- rm, rp, assoc_name, tm, rcm = @ta_map[ta]
3400
+ rp, assoc_name, tm, rcm = @ta_map[ta]
3401
+ rm = records_map[ta]
3369
3402
 
3370
3403
  # Check type map for all dependencies, and use a unique
3371
3404
  # object if any are dependencies for multiple objects,
3372
3405
  # to prevent duplicate objects from showing up in the case
3373
3406
  # the normal duplicate removal code is not being used.
3374
- if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][3]}
3407
+ if !@unique && !deps.empty? && deps.any?{|dep_key,_| @ta_map[dep_key][2]}
3375
3408
  key = [current.object_id, key]
3376
3409
  end
3377
3410
 
@@ -1415,9 +1415,9 @@ module Sequel
1415
1415
  # is valid and before hooks execute successfully. Fails if:
1416
1416
  #
1417
1417
  # * the record is not valid, or
1418
- # * before_save returns false, or
1419
- # * the record is new and before_create returns false, or
1420
- # * the record is not new and before_update returns false.
1418
+ # * before_save calls cancel_action, or
1419
+ # * the record is new and before_create calls cancel_action, or
1420
+ # * the record is not new and before_update calls cancel_action.
1421
1421
  #
1422
1422
  # If +save+ fails and either raise_on_save_failure or the
1423
1423
  # :raise_on_failure option is true, it raises ValidationFailed
@@ -0,0 +1,139 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The eager_graph_eager plugin allows for chaining eager loads after eager_graph
6
+ # loads. Given the following model associations:
7
+ #
8
+ # Band.one_to_many :albums
9
+ # Album.one_to_many :tracks
10
+ #
11
+ # Let's say you wanted to return bands ordered by album name, and eagerly load
12
+ # those albums, you can do that using:
13
+ #
14
+ # Band.eager_graph(:albums).order{albums[:name]}
15
+ #
16
+ # Let's say you also wanted to eagerly load the tracks for each album. You could
17
+ # just add them to the eager_graph call:
18
+ #
19
+ # Band.eager_graph(albums: :tracks).order{albums[:name]}
20
+ #
21
+ # However, the bloats the result set, and you aren't ordering by the track
22
+ # information, so a join is not required. The eager_graph_eager plugin allows
23
+ # you to specify that the tracks be eagerly loaded in a separate query after
24
+ # the eager_graph load of albums:
25
+ #
26
+ # Band.eager_graph(:albums).eager_graph_eager([:albums], :tracks).order{albums[:name]}
27
+ #
28
+ # <tt>Dataset#eager_graph_eager</tt>'s first argument is a dependency chain, specified
29
+ # as an array of symbols. This specifies the point at which to perform the eager load.
30
+ # The remaining arguments are arguments that could be passed to Dataset#eager to specify
31
+ # what dependent associations should be loaded at that point.
32
+ #
33
+ # If you also have the following model association:
34
+ #
35
+ # Track.one_to_many :lyrics
36
+ #
37
+ # Here's some different ways of performing eager loading:
38
+ #
39
+ # # 4 Queries: bands, albums, tracks, lyrics
40
+ # Band.eager(albums: {tracks: :lyrics})
41
+ #
42
+ # # 1 Query: bands+albums+tracks+lyrics
43
+ # Band.eager_graph(albums: {tracks: :lyrics})
44
+ #
45
+ # # 3 Queries: bands+albums, tracks, lyrics
46
+ # Band.eager_graph(:albums).eager_graph_eager([:albums], tracks: :lyrics)
47
+ #
48
+ # # 2 Queries: bands+albums+tracks, lyrics
49
+ # Band.eager_graph(albums: :tracks).eager_graph_eager([:albums, :tracks], :lyrics)
50
+ #
51
+ # # 2 Queries: bands+albums, tracks+lyrics
52
+ # Band.eager_graph(:albums).eager_graph_eager([:albums], tracks: proc{|ds| ds.eager_graph(:lyrics)})
53
+ #
54
+ # Usage:
55
+ #
56
+ # # Support eager_graph_eager in all subclass datasets (called before loading subclasses)
57
+ # Sequel::Model.plugin :eager_graph_eager
58
+ #
59
+ # # Support eager_graph_eager in Album class datasets
60
+ # Album.plugin :eager_graph_eager
61
+ module EagerGraphEager
62
+ module DatasetMethods
63
+ # Specify for the given dependency chain, after loading objects for the
64
+ # current dataset via eager_graph, eagerly load the given associations at that point in the
65
+ # dependency chain.
66
+ #
67
+ # dependency_chain :: Array of association symbols, with the first association symbol
68
+ # specifying an association in the dataset's model, the next
69
+ # association specifying an association in the previous association's
70
+ # associated model, and so on.
71
+ # assocs :: Symbols or hashes specifying associations to eagerly load at the point
72
+ # specified by the dependency chain.
73
+ def eager_graph_eager(dependency_chain, *assocs)
74
+ unless dependency_chain.is_a?(Array) && dependency_chain.all?{|s| s.is_a?(Symbol)} && !dependency_chain.empty?
75
+ raise Error, "eager_graph_eager first argument must be array of symbols"
76
+ end
77
+
78
+ current = model
79
+ deps = dependency_chain.map do |dep|
80
+ unless ref = current.association_reflection(dep)
81
+ raise Error, "invalid association #{dep.inspect} for #{current.inspect}"
82
+ end
83
+ current = ref.associated_class
84
+ [dep, ref.returns_array?]
85
+ end
86
+ assocs = current.dataset.send(:eager_options_for_associations, assocs)
87
+
88
+ deps.each(&:freeze)
89
+ deps.unshift(current)
90
+ deps.freeze
91
+
92
+ assocs.freeze
93
+
94
+ if h = @opts[:eager_graph_eager]
95
+ h = Hash[h]
96
+ h[deps] = assocs
97
+ else
98
+ h = {deps => assocs}
99
+ end
100
+
101
+ clone(:eager_graph_eager=>h.freeze)
102
+ end
103
+
104
+ protected
105
+
106
+ # After building objects from the rows, if eager_graph_eager has been
107
+ # called on the datasets, for each dependency chain specified, eagerly
108
+ # load the appropriate associations.
109
+ def eager_graph_build_associations(rows)
110
+ objects = super
111
+
112
+ if eager_data = @opts[:eager_graph_eager]
113
+ eager_data.each do |deps, assocs|
114
+ current = objects
115
+
116
+ last_class, *deps = deps
117
+ deps.each do |dep, is_multiple|
118
+ current_assocs = current.map(&:associations)
119
+
120
+ if is_multiple
121
+ current = current_assocs.flat_map{|a| a[dep]}
122
+ else
123
+ current = current_assocs.map{|a| a[dep]}
124
+ current.compact!
125
+ end
126
+
127
+ current.uniq!(&:object_id)
128
+ end
129
+
130
+ last_class.dataset.send(:eager_load, current, assocs)
131
+ end
132
+ end
133
+
134
+ objects
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -55,6 +55,7 @@ module Sequel
55
55
  # Now if you +#dup+ a Model object (the resulting object is not frozen), you
56
56
  # will be able to update and save the duplicate.
57
57
  # Note the caveats around your responsibility to update the cache still applies.
58
+ # You can update the cache via `.load_cache` method.
58
59
  module StaticCache
59
60
  # Populate the static caches when loading the plugin. Options:
60
61
  # :frozen :: Whether retrieved model objects are frozen. The default is true,
@@ -209,14 +210,6 @@ module Sequel
209
210
  !@static_cache_frozen
210
211
  end
211
212
 
212
- private
213
-
214
- # Return the frozen object with the given pk, or nil if no such object exists
215
- # in the cache, without issuing a database query.
216
- def primary_key_lookup(pk)
217
- static_cache_object(cache[pk])
218
- end
219
-
220
213
  # Reload the cache for this model by retrieving all of the instances in the dataset
221
214
  # freezing them, and populating the cached array and hash.
222
215
  def load_cache
@@ -230,6 +223,14 @@ module Sequel
230
223
  @cache = h.freeze
231
224
  end
232
225
 
226
+ private
227
+
228
+ # Return the frozen object with the given pk, or nil if no such object exists
229
+ # in the cache, without issuing a database query.
230
+ def primary_key_lookup(pk)
231
+ static_cache_object(cache[pk])
232
+ end
233
+
233
234
  # If frozen: false is not used, just return the argument. Otherwise,
234
235
  # create a new instance with the arguments values if the argument is
235
236
  # not nil.
@@ -60,6 +60,46 @@ module Sequel
60
60
  # # SELECT * FROM artists WHERE name > 'N' AND id IN (...)
61
61
  # albums.first.artists(eager: lambda{|ds| ds.where(Sequel[:name] > 'N')})
62
62
  #
63
+ # The tactical_eager_loading plugin also allows transparent eager
64
+ # loading when calling association methods on associated objects
65
+ # eagerly loaded via Dataset#eager_graph. This can reduce N queries
66
+ # to a single query when iterating over all associated objects.
67
+ # Consider the following code:
68
+ #
69
+ # artists = Artist.eager_graph(:albums).all
70
+ # artists.each do |artist|
71
+ # artist.albums.each do |album|
72
+ # album.tracks
73
+ # end
74
+ # end
75
+ #
76
+ # By default this will issue a single query to load the artists and
77
+ # albums, and then one query for each album to load the tracks for
78
+ # the album:
79
+ #
80
+ # # SELECT artists.id, ...
81
+ # albums.id, ...
82
+ # # FROM artists
83
+ # # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
84
+ # # SELECT * FROM tracks WHERE album_id = 1;
85
+ # # SELECT * FROM tracks WHERE album_id = 2;
86
+ # # SELECT * FROM tracks WHERE album_id = 10;
87
+ # # ...
88
+ #
89
+ # With the tactical_eager_loading plugin, this uses the same
90
+ # query to load the artists and albums, but then issues a single query
91
+ # to load the tracks for all albums.
92
+ #
93
+ # # SELECT artists.id, ...
94
+ # albums.id, ...
95
+ # # FROM artists
96
+ # # LEFT OUTER JOIN albums ON (albums.artist_id = artists.id);
97
+ # # SELECT * FROM tracks WHERE (tracks.album_id IN (1, 2, 10, ...));
98
+ #
99
+ # Note that transparent eager loading for associated objects
100
+ # loaded by eager_graph will only take place if the associated classes
101
+ # also use the tactical_eager_loading plugin.
102
+ #
63
103
  # Usage:
64
104
  #
65
105
  # # Make all model subclass instances use tactical eager loading (called before loading subclasses)
@@ -112,7 +152,29 @@ module Sequel
112
152
  module DatasetMethods
113
153
  private
114
154
 
115
- # Set the retrieved_with and retrieved_by attributes for the object
155
+ # Set the retrieved_with and retrieved_by attributes for each of the associated objects
156
+ # created by the eager graph loader with the appropriate class dataset and array of objects.
157
+ def _eager_graph_build_associations(_, egl)
158
+ objects = super
159
+
160
+ master = egl.master
161
+ egl.records_map.each do |k, v|
162
+ next if k == master || v.empty?
163
+
164
+ by = opts[:graph][:table_aliases][k]
165
+ values = v.values
166
+
167
+ values.each do |o|
168
+ next unless o.is_a?(TacticalEagerLoading::InstanceMethods) && !o.retrieved_by
169
+ o.retrieved_by = by
170
+ o.retrieved_with = values
171
+ end
172
+ end
173
+
174
+ objects
175
+ end
176
+
177
+ # Set the retrieved_with and retrieved_by attributes for each object
116
178
  # with the current dataset and array of all objects.
117
179
  def post_load(objects)
118
180
  super