sequel 3.34.1 → 3.35.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 (101) hide show
  1. data/CHANGELOG +52 -0
  2. data/README.rdoc +3 -1
  3. data/Rakefile +2 -10
  4. data/doc/active_record.rdoc +1 -0
  5. data/doc/migration.rdoc +18 -7
  6. data/doc/model_hooks.rdoc +6 -0
  7. data/doc/opening_databases.rdoc +3 -0
  8. data/doc/prepared_statements.rdoc +0 -1
  9. data/doc/release_notes/3.35.0.txt +144 -0
  10. data/doc/schema_modification.rdoc +16 -1
  11. data/doc/thread_safety.rdoc +17 -0
  12. data/lib/sequel/adapters/do.rb +2 -2
  13. data/lib/sequel/adapters/do/postgres.rb +1 -52
  14. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  15. data/lib/sequel/adapters/firebird.rb +1 -1
  16. data/lib/sequel/adapters/ibmdb.rb +2 -2
  17. data/lib/sequel/adapters/jdbc.rb +23 -19
  18. data/lib/sequel/adapters/jdbc/db2.rb +0 -5
  19. data/lib/sequel/adapters/jdbc/derby.rb +29 -2
  20. data/lib/sequel/adapters/jdbc/firebird.rb +0 -5
  21. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  22. data/lib/sequel/adapters/jdbc/hsqldb.rb +7 -0
  23. data/lib/sequel/adapters/jdbc/informix.rb +0 -5
  24. data/lib/sequel/adapters/jdbc/jtds.rb +0 -5
  25. data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
  26. data/lib/sequel/adapters/jdbc/postgresql.rb +4 -35
  27. data/lib/sequel/adapters/jdbc/sqlite.rb +0 -5
  28. data/lib/sequel/adapters/jdbc/sqlserver.rb +0 -5
  29. data/lib/sequel/adapters/jdbc/transactions.rb +4 -4
  30. data/lib/sequel/adapters/mysql2.rb +1 -1
  31. data/lib/sequel/adapters/odbc.rb +3 -3
  32. data/lib/sequel/adapters/odbc/mssql.rb +14 -1
  33. data/lib/sequel/adapters/oracle.rb +6 -18
  34. data/lib/sequel/adapters/postgres.rb +36 -53
  35. data/lib/sequel/adapters/shared/db2.rb +16 -2
  36. data/lib/sequel/adapters/shared/mssql.rb +40 -9
  37. data/lib/sequel/adapters/shared/mysql.rb +16 -4
  38. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +2 -2
  39. data/lib/sequel/adapters/shared/oracle.rb +2 -0
  40. data/lib/sequel/adapters/shared/postgres.rb +135 -211
  41. data/lib/sequel/adapters/sqlite.rb +2 -2
  42. data/lib/sequel/adapters/swift.rb +1 -1
  43. data/lib/sequel/adapters/swift/postgres.rb +1 -71
  44. data/lib/sequel/adapters/tinytds.rb +3 -3
  45. data/lib/sequel/core.rb +27 -4
  46. data/lib/sequel/database/connecting.rb +7 -8
  47. data/lib/sequel/database/logging.rb +6 -1
  48. data/lib/sequel/database/misc.rb +20 -4
  49. data/lib/sequel/database/query.rb +38 -18
  50. data/lib/sequel/database/schema_generator.rb +5 -2
  51. data/lib/sequel/database/schema_methods.rb +34 -8
  52. data/lib/sequel/dataset/prepared_statements.rb +1 -1
  53. data/lib/sequel/dataset/sql.rb +18 -24
  54. data/lib/sequel/extensions/core_extensions.rb +0 -23
  55. data/lib/sequel/extensions/migration.rb +22 -8
  56. data/lib/sequel/extensions/pg_auto_parameterize.rb +4 -0
  57. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  58. data/lib/sequel/model.rb +2 -2
  59. data/lib/sequel/model/associations.rb +95 -70
  60. data/lib/sequel/model/base.rb +16 -18
  61. data/lib/sequel/plugins/dirty.rb +214 -0
  62. data/lib/sequel/plugins/identity_map.rb +1 -1
  63. data/lib/sequel/plugins/json_serializer.rb +16 -1
  64. data/lib/sequel/plugins/many_through_many.rb +22 -32
  65. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +2 -2
  66. data/lib/sequel/plugins/prepared_statements.rb +22 -8
  67. data/lib/sequel/plugins/prepared_statements_associations.rb +2 -3
  68. data/lib/sequel/plugins/prepared_statements_with_pk.rb +1 -1
  69. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  70. data/lib/sequel/plugins/subclasses.rb +10 -2
  71. data/lib/sequel/plugins/timestamps.rb +1 -1
  72. data/lib/sequel/plugins/xml_serializer.rb +12 -1
  73. data/lib/sequel/sql.rb +1 -1
  74. data/lib/sequel/version.rb +2 -2
  75. data/spec/adapters/postgres_spec.rb +30 -79
  76. data/spec/core/database_spec.rb +46 -2
  77. data/spec/core/dataset_spec.rb +28 -22
  78. data/spec/core/schema_generator_spec.rb +1 -1
  79. data/spec/core/schema_spec.rb +51 -0
  80. data/spec/extensions/arbitrary_servers_spec.rb +0 -4
  81. data/spec/extensions/association_autoreloading_spec.rb +17 -0
  82. data/spec/extensions/association_proxies_spec.rb +4 -4
  83. data/spec/extensions/core_extensions_spec.rb +1 -24
  84. data/spec/extensions/dirty_spec.rb +155 -0
  85. data/spec/extensions/json_serializer_spec.rb +13 -0
  86. data/spec/extensions/migration_spec.rb +28 -15
  87. data/spec/extensions/named_timezones_spec.rb +6 -8
  88. data/spec/extensions/pg_auto_parameterize_spec.rb +6 -5
  89. data/spec/extensions/schema_dumper_spec.rb +3 -1
  90. data/spec/extensions/xml_serializer_spec.rb +13 -0
  91. data/spec/files/{transactionless_migrations → transaction_specified_migrations}/001_create_alt_basic.rb +1 -1
  92. data/spec/files/{transactionless_migrations → transaction_specified_migrations}/002_create_basic.rb +0 -0
  93. data/spec/files/{transaction_migrations → transaction_unspecified_migrations}/001_create_alt_basic.rb +0 -0
  94. data/spec/files/{transaction_migrations → transaction_unspecified_migrations}/002_create_basic.rb +0 -0
  95. data/spec/integration/associations_test.rb +5 -7
  96. data/spec/integration/dataset_test.rb +25 -7
  97. data/spec/integration/plugin_test.rb +1 -1
  98. data/spec/integration/schema_test.rb +16 -1
  99. data/spec/model/associations_spec.rb +2 -2
  100. metadata +14 -9
  101. data/lib/sequel/adapters/odbc/db2.rb +0 -17
@@ -220,7 +220,7 @@ module Sequel
220
220
  # end # COMMIT
221
221
  def db
222
222
  return @db if @db
223
- @db = self == Model ? DATABASES.first : superclass.db
223
+ @db = self == Model ? Sequel.synchronize{DATABASES.first} : superclass.db
224
224
  raise(Error, "No database associated with #{self}: have you called Sequel.connect or #{self}.db= ?") unless @db
225
225
  @db
226
226
  end
@@ -295,21 +295,6 @@ module Sequel
295
295
  end
296
296
  end
297
297
 
298
- module_eval(if RUBY_VERSION < '1.8.7'
299
- <<-END
300
- def def_model_dataset_method_block(arg)
301
- meta_def(arg){|*args| dataset.send(arg, *args)}
302
- end
303
- END
304
- else
305
- <<-END
306
- def def_model_dataset_method_block(arg)
307
- meta_def(arg){|*args, &block| dataset.send(arg, *args, &block)}
308
- end
309
- END
310
- end, __FILE__, __LINE__ - 4)
311
- private :def_model_dataset_method_block
312
-
313
298
  # Finds a single record according to the supplied filter.
314
299
  # You are encouraged to use Model.[] or Model.first instead of this method.
315
300
  #
@@ -685,6 +670,13 @@ module Sequel
685
670
  end
686
671
  end
687
672
 
673
+ # Define a model method that calls the dataset method with the same name,
674
+ # only used for methods with names that can't be presented directly in
675
+ # ruby code.
676
+ def def_model_dataset_method_block(arg)
677
+ meta_def(arg){|*args, &block| dataset.send(arg, *args, &block)}
678
+ end
679
+
688
680
  # Get the schema from the database, fall back on checking the columns
689
681
  # via the database if that will return inaccurate results or if
690
682
  # it raises an error.
@@ -935,8 +927,7 @@ module Sequel
935
927
  v = typecast_value(column, value)
936
928
  vals = @values
937
929
  if new? || !vals.include?(column) || v != (c = vals[column]) || v.class != c.class
938
- changed_columns << column unless changed_columns.include?(column)
939
- vals[column] = v
930
+ change_column_value(column, v)
940
931
  end
941
932
  end
942
933
 
@@ -1710,6 +1701,13 @@ module Sequel
1710
1701
  use_transaction?(opts) ? db.transaction({:server=>this_server}.merge(opts)){yield} : yield
1711
1702
  end
1712
1703
 
1704
+ # Change the value of the column to given value, recording the change.
1705
+ def change_column_value(column, value)
1706
+ cc = changed_columns
1707
+ cc << column unless cc.include?(column)
1708
+ @values[column] = value
1709
+ end
1710
+
1713
1711
  # Set the columns with the given hash. By default, the same as +set+, but
1714
1712
  # exists so it can be overridden. This is called only for new records, before
1715
1713
  # changed_columns is cleared.
@@ -0,0 +1,214 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The dirty plugin makes Sequel save the initial value of
4
+ # a column when setting a new value for the column. This
5
+ # makes it easier to see what changes were made to the object:
6
+ #
7
+ # artist.name # => 'Foo'
8
+ # artist.name = 'Bar'
9
+ # artist.initial_value(:name) # 'Foo'
10
+ # artist.column_change(:name) # ['Foo', 'Bar']
11
+ # artist.column_changes # {:name => ['Foo', 'Bar']}
12
+ # artist.column_changed?(:name) # true
13
+ # artist.reset_column(:name)
14
+ # artist.name # => 'Foo'
15
+ # artist.column_changed?(:name) # false
16
+ #
17
+ # It allows makes changed_columns more accurate in that it
18
+ # can detect when a the column value is changed and then
19
+ # changed back:
20
+ #
21
+ # artist.name # => 'Foo'
22
+ # artist.name = 'Bar'
23
+ # artist.changed_columns # => [:name]
24
+ # artist.name = 'Foo'
25
+ # artist.changed_columns # => []
26
+ #
27
+ # It can handle situations where a column value is
28
+ # modified in place:
29
+ #
30
+ # artist.will_change_column(:name)
31
+ # artist.name.gsub!(/o/, 'u')
32
+ # artist.changed_columns # => [:name]
33
+ # artist.initial_value(:name) # => 'Foo'
34
+ # artist.column_change(:name) # => ['Foo', 'Fuu']
35
+ #
36
+ # It also saves the previously changed values after an update:
37
+ #
38
+ # artist.update(:name=>'Bar')
39
+ # artist.column_changes # => {}
40
+ # artist.previous_changes # => {:name=>['Foo', 'Bar']}
41
+ #
42
+ # Usage:
43
+ #
44
+ # # Make all model subclass instances record previous values (called before loading subclasses)
45
+ # Sequel::Model.plugin :dirty
46
+ #
47
+ # # Make the Album class record previous values
48
+ # Album.plugin :dirty
49
+ module Dirty
50
+ module InstanceMethods
51
+ # A hash of previous changes before the object was
52
+ # saved, in the same format as #column_changes.
53
+ # Note that this is not necessarily the same as the columns
54
+ # that were used in the update statement.
55
+ attr_reader :previous_changes
56
+
57
+ # An array with the initial value and the current value
58
+ # of the column, if the column has been changed. If the
59
+ # column has not been changed, returns nil.
60
+ #
61
+ # column_change(:name) # => ['Initial', 'Current']
62
+ def column_change(column)
63
+ [initial_value(column), send(column)] if column_changed?(column)
64
+ end
65
+
66
+ # A hash with column symbol keys and pairs of initial and
67
+ # current values for all changed columns.
68
+ #
69
+ # column_changes # => {:name => ['Initial', 'Current']}
70
+ def column_changes
71
+ h = {}
72
+ initial_values.each do |column, value|
73
+ h[column] = [value, send(column)]
74
+ end
75
+ h
76
+ end
77
+
78
+ # Either true or false depending on whether the column has
79
+ # changed. Note that this is not exactly the same as checking if
80
+ # the column is in changed_columns, if the column was not set
81
+ # initially.
82
+ #
83
+ # column_changed?(:name) # => true
84
+ def column_changed?(column)
85
+ initial_values.has_key?(column)
86
+ end
87
+
88
+ # The initial value of the given column. If the column value has
89
+ # not changed, this will be the same as the current value of the
90
+ # column.
91
+ #
92
+ # initial_value(:name) # => 'Initial'
93
+ def initial_value(column)
94
+ initial_values.fetch(column){send(column)}
95
+ end
96
+
97
+ # A hash with column symbol keys and initial values.
98
+ #
99
+ # initial_values # {:name => 'Initial'}
100
+ def initial_values
101
+ @initial_values ||= {}
102
+ end
103
+
104
+ # Reset the column to its initial value. If the column was not set
105
+ # initial, removes it from the values.
106
+ #
107
+ # reset_column(:name)
108
+ # name # => 'Initial'
109
+ def reset_column(column)
110
+ if initial_values.has_key?(column)
111
+ send(:"#{column}=", initial_values[column])
112
+ end
113
+ if missing_initial_values.include?(column)
114
+ values.delete(column)
115
+ end
116
+ end
117
+
118
+ # Manually specify that a column will change. This should only be used
119
+ # if you plan to modify a column value in place, which is not recommended.
120
+ #
121
+ # will_change_column(:name)
122
+ # name.gsub(/i/i, 'o')
123
+ # column_change(:name) # => ['Initial', 'onotoal']
124
+ def will_change_column(column)
125
+ changed_columns << column unless changed_columns.include?(column)
126
+ check_missing_initial_value(column)
127
+
128
+ value = if initial_values.has_key?(column)
129
+ initial_values[column]
130
+ else
131
+ send(column)
132
+ end
133
+
134
+ initial_values[column] = if value && value != true && value.respond_to?(:clone)
135
+ begin
136
+ value.clone
137
+ rescue TypeError
138
+ value
139
+ end
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ private
146
+
147
+ # Reset the initial values after saving.
148
+ def after_save
149
+ super
150
+ reset_initial_values
151
+ end
152
+
153
+ # Save the current changes so they are available after updating. This happens
154
+ # before after_save resets them.
155
+ def after_update
156
+ super
157
+ @previous_changes = column_changes
158
+ end
159
+
160
+ # Reset the initial values when refreshing.
161
+ def _refresh(dataset)
162
+ super
163
+ reset_initial_values
164
+ end
165
+
166
+ # When changing the column value, save the initial column value. If the column
167
+ # value is changed back to the initial value, update changed columns to remove
168
+ # the column.
169
+ def change_column_value(column, value)
170
+ if (iv = initial_values).has_key?(column)
171
+ initial = iv[column]
172
+ super
173
+ if value == initial
174
+ changed_columns.delete(column) unless missing_initial_values.include?(column)
175
+ iv.delete(column)
176
+ end
177
+ else
178
+ check_missing_initial_value(column)
179
+ iv[column] = send(column)
180
+ super
181
+ end
182
+ end
183
+
184
+ # If the values hash does not contain the column, make sure missing_initial_values
185
+ # does so that it doesn't get deleted from changed_columns if changed back,
186
+ # and so that reseting the column value can be handled correctly.
187
+ def check_missing_initial_value(column)
188
+ unless values.has_key?(column) || (miv = missing_initial_values).include?(column)
189
+ miv << column
190
+ end
191
+ end
192
+
193
+ # Reset the initial values when initializing.
194
+ def initialize_set(h)
195
+ super
196
+ reset_initial_values
197
+ end
198
+
199
+ # Array holding column symbols that were not present initially. This is necessary
200
+ # to differentiate between values that were not present and values that were
201
+ # present but equal to nil.
202
+ def missing_initial_values
203
+ @missing_initial_values ||= []
204
+ end
205
+
206
+ # Clear the data structures that store the initial values.
207
+ def reset_initial_values
208
+ @initial_values.clear if @initial_values
209
+ @missing_initial_values.clear if @missing_initial_values
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -111,7 +111,7 @@ module Sequel
111
111
  eo[:rows].each{|object| object.associations[name] = []}
112
112
  ds = opts.associated_class
113
113
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
114
- ft = opts[:final_reverse_edge]
114
+ ft = opts.final_reverse_edge
115
115
  conds = uses_lcks ? [[left_keys.map{|k| SQL::QualifiedIdentifier.new(ft[:table], k)}, h.keys]] : [[left_key, h.keys]]
116
116
 
117
117
  # See above comment in many_to_many eager_loader
@@ -56,6 +56,11 @@ module Sequel
56
56
  # Album.to_json
57
57
  # Album.filter(:artist_id=>1).to_json(:include=>:tags)
58
58
  #
59
+ # If you have an existing array of model instances you want to convert to
60
+ # JSON, you can call the class to_json method with the :array option:
61
+ #
62
+ # Album.to_json(:array=>[Album[1], Album[2]])
63
+ #
59
64
  # Usage:
60
65
  #
61
66
  # # Add JSON output capability to all model subclass instances (called before loading subclasses)
@@ -203,7 +208,17 @@ module Sequel
203
208
  else
204
209
  opts = model.json_serializer_opts
205
210
  end
206
- res = row_proc ? all.map{|obj| Literal.new(obj.to_json(opts))} : all
211
+ res = if row_proc
212
+ array = if opts[:array]
213
+ opts = opts.dup
214
+ opts.delete(:array)
215
+ else
216
+ all
217
+ end
218
+ array.map{|obj| Literal.new(obj.to_json(opts))}
219
+ else
220
+ all
221
+ end
207
222
  opts[:root] ? {model.send(:pluralize, model.send(:underscore, model.to_s)) => res}.to_json(*a) : res.to_json(*a)
208
223
  end
209
224
  end
@@ -47,30 +47,18 @@ module Sequel
47
47
  class ManyThroughManyAssociationReflection < Sequel::Model::Associations::ManyToManyAssociationReflection
48
48
  Sequel::Model::Associations::ASSOCIATION_TYPES[:many_through_many] = self
49
49
 
50
- # The table containing the column to use for the associated key when eagerly loading
51
- def associated_key_table
52
- self[:associated_key_table] = self[:final_reverse_edge][:alias]
53
- end
54
-
55
50
  # The default associated key alias(es) to use when eager loading
56
51
  # associations via eager.
57
52
  def default_associated_key_alias
58
53
  self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
59
54
  end
60
55
 
61
- # The hash key to use for the eager loading predicate (left side of IN (1, 2, 3))
62
- def eager_loading_predicate_key
63
- self[:eager_loading_predicate_key] ||= begin
64
- calculate_edges
65
- e = self[:edges].first
66
- f = self[:final_reverse_edge]
67
- qualify(f[:alias], e[:right])
68
- end
69
- end
70
-
71
- # The list of joins to use when eager graphing
72
- def edges
73
- self[:edges] || calculate_edges || self[:edges]
56
+ %w'associated_key_table eager_loading_predicate_key edges final_edge final_reverse_edge reverse_edges'.each do |meth|
57
+ class_eval(<<-END, __FILE__, __LINE__+1)
58
+ def #{meth}
59
+ cached_fetch(:#{meth}){calculate_edges[:#{meth}]}
60
+ end
61
+ END
74
62
  end
75
63
 
76
64
  # Many through many associations don't have a reciprocal
@@ -78,11 +66,6 @@ module Sequel
78
66
  nil
79
67
  end
80
68
 
81
- # The list of joins to use when lazy loading or eager loading
82
- def reverse_edges
83
- self[:reverse_edges] || calculate_edges || self[:reverse_edges]
84
- end
85
-
86
69
  private
87
70
 
88
71
  # Make sure to use unique table aliases when lazy loading or eager loading
@@ -119,11 +102,18 @@ module Sequel
119
102
  reverse_edges = es.reverse.map{|e| {:table=>e[:left_table], :left=>e[:left_key], :right=>e[:right_key]}}
120
103
  reverse_edges.pop
121
104
  calculate_reverse_edge_aliases(reverse_edges)
122
- self[:final_edge] = edges.pop
123
- self[:final_reverse_edge] = reverse_edges.pop
124
- self[:edges] = edges
125
- self[:reverse_edges] = reverse_edges
126
- nil
105
+ final_reverse_edge = reverse_edges.pop
106
+ final_reverse_alias = final_reverse_edge[:alias]
107
+
108
+ h = {:final_edge=>edges.pop,
109
+ :final_reverse_edge=>final_reverse_edge,
110
+ :edges=>edges,
111
+ :reverse_edges=>reverse_edges,
112
+ :eager_loading_predicate_key=>qualify(final_reverse_alias, edges.first[:right]),
113
+ :associated_key_table=>final_reverse_edge[:alias],
114
+ }
115
+ h.each{|k, v| cached_set(k, v)}
116
+ h
127
117
  end
128
118
  end
129
119
 
@@ -185,7 +175,7 @@ module Sequel
185
175
  opts[:dataset] ||= lambda do
186
176
  ds = opts.associated_class
187
177
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
188
- ft = opts[:final_reverse_edge]
178
+ ft = opts.final_reverse_edge
189
179
  ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.zip(left_pks.map{|k| send(k)}), :table_alias=>ft[:alias])
190
180
  end
191
181
 
@@ -196,7 +186,7 @@ module Sequel
196
186
  rows.each{|object| object.associations[name] = []}
197
187
  ds = opts.associated_class
198
188
  opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
199
- ft = opts[:final_reverse_edge]
189
+ ft = opts.final_reverse_edge
200
190
  ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + [[opts.eager_loading_predicate_key, h.keys]], :table_alias=>ft[:alias])
201
191
  ds = model.eager_loading_dataset(opts, ds, nil, eo[:associations], eo)
202
192
  case opts.eager_limit_strategy
@@ -240,7 +230,7 @@ module Sequel
240
230
  ds = ds.graph(t[:table], t.fetch(:only_conditions, (Array(t[:right]).zip(Array(t[:left])) + t[:conditions])), :select=>false, :table_alias=>ds.unused_table_alias(t[:table]), :join_type=>t[:join_type], :implicit_qualifier=>iq, &t[:block])
241
231
  iq = nil
242
232
  end
243
- fe = opts[:final_edge]
233
+ fe = opts.final_edge
244
234
  ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :join_type=>join_type, &graph_block)
245
235
  end
246
236
 
@@ -269,7 +259,7 @@ module Sequel
269
259
  end
270
260
  meths = ref.right_primary_keys
271
261
  meths = ref.qualify(obj.model.table_name, meths) if obj.is_a?(Sequel::Dataset)
272
- exp = association_filter_key_expression(ref.qualify(last_alias, Array(ref[:final_edge][:left])), meths, obj)
262
+ exp = association_filter_key_expression(ref.qualify(last_alias, Array(ref.final_edge[:left])), meths, obj)
273
263
  if exp == SQL::Constants::FALSE
274
264
  association_filter_handle_inversion(op, exp, Array(lpks))
275
265
  else
@@ -38,8 +38,8 @@ module Sequel
38
38
  # caching if the associated model is using caching.
39
39
  def _load_associated_object(opts, dynamic_opts)
40
40
  klass = opts.associated_class
41
- cache_lookup = opts.fetch(:many_to_one_pk_lookup) do
42
- opts[:many_to_one_pk_lookup] = opts[:type] == :many_to_one &&
41
+ cache_lookup = opts.send(:cached_fetch, :many_to_one_pk_lookup) do
42
+ opts[:type] == :many_to_one &&
43
43
  opts[:key] &&
44
44
  opts.primary_key == klass.primary_key
45
45
  end
@@ -31,14 +31,14 @@ module Sequel
31
31
 
32
32
  # Setup the datastructure used to hold the prepared statements in the model.
33
33
  def self.apply(model)
34
- model.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
34
+ model.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{}, :fixed=>{})
35
35
  end
36
36
 
37
37
  module ClassMethods
38
38
  # Setup the datastructure used to hold the prepared statements in the subclass.
39
39
  def inherited(subclass)
40
40
  super
41
- subclass.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
41
+ subclass.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{}, :fixed=>{})
42
42
  end
43
43
 
44
44
  private
@@ -58,30 +58,30 @@ module Sequel
58
58
 
59
59
  # Return a prepared statement that can be used to delete a row from this model's dataset.
60
60
  def prepared_delete
61
- @prepared_statements[:delete] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :delete)
61
+ cached_prepared_statement(:fixed, :delete){prepare_statement(filter(prepared_statement_key_array(primary_key)), :delete)}
62
62
  end
63
63
 
64
64
  # Return a prepared statement that can be used to insert a row using the given columns.
65
65
  def prepared_insert(cols)
66
- @prepared_statements[:insert][prepared_columns(cols)] ||= prepare_statement(dataset, :insert, prepared_statement_key_hash(cols))
66
+ cached_prepared_statement(:insert, prepared_columns(cols)){prepare_statement(dataset, :insert, prepared_statement_key_hash(cols))}
67
67
  end
68
68
 
69
69
  # Return a prepared statement that can be used to insert a row using the given columns
70
70
  # and return that column values for the row created.
71
71
  def prepared_insert_select(cols)
72
72
  if dataset.supports_insert_select?
73
- @prepared_statements[:insert_select][prepared_columns(cols)] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)), :insert_select, prepared_statement_key_hash(cols))
73
+ cached_prepared_statement(:insert_select, prepared_columns(cols)){prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)), :insert_select, prepared_statement_key_hash(cols))}
74
74
  end
75
75
  end
76
76
 
77
77
  # Return a prepared statement that can be used to lookup a row solely based on the primary key.
78
78
  def prepared_lookup
79
- @prepared_statements[:lookup] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :first)
79
+ cached_prepared_statement(:fixed, :lookup){prepare_statement(filter(prepared_statement_key_array(primary_key)), :first)}
80
80
  end
81
81
 
82
82
  # Return a prepared statement that can be used to refresh a row to get new column values after insertion.
83
83
  def prepared_refresh
84
- @prepared_statements[:refresh] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)).filter(prepared_statement_key_array(primary_key)), :first)
84
+ cached_prepared_statement(:fixed, :refresh){prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)).filter(prepared_statement_key_array(primary_key)), :first)}
85
85
  end
86
86
 
87
87
  # Return an array of two element arrays with the column symbol as the first entry and the
@@ -108,13 +108,27 @@ module Sequel
108
108
 
109
109
  # Return a prepared statement that can be used to update row using the given columns.
110
110
  def prepared_update(cols)
111
- @prepared_statements[:update][prepared_columns(cols)] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :update, prepared_statement_key_hash(cols))
111
+ cached_prepared_statement(:update, prepared_columns(cols)){prepare_statement(filter(prepared_statement_key_array(primary_key)), :update, prepared_statement_key_hash(cols))}
112
112
  end
113
113
 
114
114
  # Use a prepared statement to query the database for the row matching the given primary key.
115
115
  def primary_key_lookup(pk)
116
116
  prepared_lookup.call(primary_key_hash(pk))
117
117
  end
118
+
119
+ private
120
+
121
+ # If a prepared statement has already been cached for the given type and subtype,
122
+ # return it. Otherwise, yield to the block to get the prepared statement, and cache it.
123
+ def cached_prepared_statement(type, subtype)
124
+ h = @prepared_statements[type]
125
+ Sequel.synchronize do
126
+ if v = h[subtype]
127
+ return v end
128
+ end
129
+ ps = yield
130
+ Sequel.synchronize{h[subtype] = ps}
131
+ end
118
132
  end
119
133
 
120
134
  module InstanceMethods