sequel 5.11.0 → 5.12.0

Sign up to get free protection for your applications and to get access to all the features.
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