sequel 3.34.0 → 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 (102) hide show
  1. data/CHANGELOG +56 -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/postgres.rb +1 -52
  13. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  14. data/lib/sequel/adapters/do.rb +2 -2
  15. data/lib/sequel/adapters/firebird.rb +1 -1
  16. data/lib/sequel/adapters/ibmdb.rb +2 -2
  17. data/lib/sequel/adapters/jdbc/db2.rb +0 -5
  18. data/lib/sequel/adapters/jdbc/derby.rb +29 -2
  19. data/lib/sequel/adapters/jdbc/firebird.rb +0 -5
  20. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  21. data/lib/sequel/adapters/jdbc/hsqldb.rb +7 -0
  22. data/lib/sequel/adapters/jdbc/informix.rb +0 -5
  23. data/lib/sequel/adapters/jdbc/jtds.rb +0 -5
  24. data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
  25. data/lib/sequel/adapters/jdbc/postgresql.rb +4 -35
  26. data/lib/sequel/adapters/jdbc/sqlite.rb +0 -5
  27. data/lib/sequel/adapters/jdbc/sqlserver.rb +0 -5
  28. data/lib/sequel/adapters/jdbc/transactions.rb +4 -4
  29. data/lib/sequel/adapters/jdbc.rb +23 -19
  30. data/lib/sequel/adapters/mysql2.rb +1 -1
  31. data/lib/sequel/adapters/odbc/mssql.rb +14 -1
  32. data/lib/sequel/adapters/odbc.rb +3 -3
  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/postgres.rb +1 -71
  43. data/lib/sequel/adapters/swift.rb +1 -1
  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/associations.rb +95 -70
  59. data/lib/sequel/model/base.rb +19 -20
  60. data/lib/sequel/model.rb +2 -2
  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 +1 -1
  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/single_table_inheritance_spec.rb +5 -0
  91. data/spec/extensions/xml_serializer_spec.rb +13 -0
  92. data/spec/files/{transactionless_migrations → transaction_specified_migrations}/001_create_alt_basic.rb +1 -1
  93. data/spec/integration/associations_test.rb +5 -7
  94. data/spec/integration/dataset_test.rb +25 -7
  95. data/spec/integration/plugin_test.rb +1 -1
  96. data/spec/integration/schema_test.rb +16 -1
  97. data/spec/model/associations_spec.rb +2 -2
  98. metadata +14 -9
  99. data/lib/sequel/adapters/odbc/db2.rb +0 -17
  100. /data/spec/files/{transactionless_migrations → transaction_specified_migrations}/002_create_basic.rb +0 -0
  101. /data/spec/files/{transaction_migrations → transaction_unspecified_migrations}/001_create_alt_basic.rb +0 -0
  102. /data/spec/files/{transaction_migrations → transaction_unspecified_migrations}/002_create_basic.rb +0 -0
@@ -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
@@ -53,8 +53,7 @@ module Sequel
53
53
  when :many_to_many
54
54
  association_bound_variable_hash(opts.join_table_alias, opts[:left_keys], opts[:left_primary_keys])
55
55
  when :many_through_many
56
- opts.reverse_edges
57
- association_bound_variable_hash(opts[:final_reverse_edge][:alias], Array(opts[:left_key]), opts[:left_primary_keys])
56
+ association_bound_variable_hash(opts.final_reverse_edge[:alias], Array(opts[:left_key]), opts[:left_primary_keys])
58
57
  end
59
58
  end
60
59
 
@@ -62,7 +61,7 @@ module Sequel
62
61
  # that, given appropriate bound variables, the prepared statement will work correctly for any
63
62
  # instance.
64
63
  def association_prepared_statement(opts)
65
- opts[:prepared_statement] ||= begin
64
+ opts.send(:cached_fetch, :prepared_statement) do
66
65
  ps = _associated_dataset(opts, {}).unbind.first.prepare(opts.returns_array? ? :select : :first, :"smpsap_#{NEXT.call}")
67
66
  ps.log_sql = true
68
67
  ps
@@ -31,7 +31,7 @@ module Sequel
31
31
  # Return a prepared statement that can be used to lookup a row given a dataset for the row matching
32
32
  # the primary key.
33
33
  def prepared_lookup_dataset(ds)
34
- @prepared_statements[:lookup_sql][ds.sql] ||= prepare_statement(ds.filter(prepared_statement_key_array(primary_key).map{|k, v| [SQL::QualifiedIdentifier.new(ds.model.table_name, k), v]}), :first)
34
+ cached_prepared_statement(:lookup_sql, ds.sql){prepare_statement(ds.filter(prepared_statement_key_array(primary_key).map{|k, v| [SQL::QualifiedIdentifier.new(ds.model.table_name, k), v]}), :first)}
35
35
  end
36
36
  end
37
37
 
@@ -131,7 +131,7 @@ module Sequel
131
131
  # keys for all of their descendant classes.
132
132
  def sti_subclass_added(key)
133
133
  if sti_key_array
134
- sti_key_array << key
134
+ Sequel.synchronize{sti_key_array << key}
135
135
  superclass.sti_subclass_added(key)
136
136
  end
137
137
  end
@@ -28,7 +28,7 @@ module Sequel
28
28
 
29
29
  # All descendent classes of this model.
30
30
  def descendents
31
- subclasses.map{|x| [x] + x.descendents}.flatten
31
+ Sequel.synchronize{_descendents}
32
32
  end
33
33
 
34
34
  # Add the subclass to this model's current subclasses,
@@ -36,9 +36,17 @@ module Sequel
36
36
  # in the subclass.
37
37
  def inherited(subclass)
38
38
  super
39
- subclasses << subclass
39
+ Sequel.synchronize{subclasses << subclass}
40
40
  subclass.instance_variable_set(:@subclasses, [])
41
41
  end
42
+
43
+ private
44
+
45
+ # Recursive, non-thread safe version of descendents, since
46
+ # the mutex Sequel uses isn't reentrant.
47
+ def _descendents
48
+ subclasses.map{|x| [x] + x.send(:_descendents)}.flatten
49
+ end
42
50
  end
43
51
  end
44
52
  end
@@ -19,7 +19,7 @@ module Sequel
19
19
  # # timestamp, and setting the update timestamp when creating
20
20
  # Album.plugin :timestamps, :force=>true, :update_on_create=>true
21
21
  module Timestamps
22
- # Configure the plugin by setting the avialable options. Note that
22
+ # Configure the plugin by setting the available options. Note that
23
23
  # if this method is run more than once, previous settings are ignored,
24
24
  # and it will just use the settings given or the default settings. Options:
25
25
  # * :create - The field to hold the create timestamp (default: :created_at)
@@ -79,6 +79,11 @@ module Sequel
79
79
  #
80
80
  # Album.array_from_xml(Album.to_xml) # same as Album.all
81
81
  #
82
+ # If you have an existing array of model instances you want to convert to
83
+ # XML, you can call the class to_xml method with the :array option:
84
+ #
85
+ # Album.to_xml(:array=>[Album[1], Album[2]])
86
+ #
82
87
  # Usage:
83
88
  #
84
89
  # # Add XML output capability to all model subclass instances (called before loading subclasses)
@@ -316,8 +321,14 @@ module Sequel
316
321
  raise(Sequel::Error, "Dataset#to_xml") unless row_proc
317
322
  x = model.xml_builder(opts)
318
323
  name_proc = model.xml_serialize_name_proc(opts)
324
+ array = if opts[:array]
325
+ opts = opts.dup
326
+ opts.delete(:array)
327
+ else
328
+ all
329
+ end
319
330
  x.send(name_proc[opts.fetch(:array_root_name, model.send(:pluralize, model.send(:underscore, model.name))).to_s]) do |x1|
320
- all.each do |obj|
331
+ array.each do |obj|
321
332
  obj.to_xml(opts.merge(:builder=>x1))
322
333
  end
323
334
  end
data/lib/sequel/sql.rb CHANGED
@@ -138,7 +138,7 @@ module Sequel
138
138
  MATHEMATICAL_OPERATORS = [:+, :-, :/, :*]
139
139
 
140
140
  # Bitwise mathematical operators used in +NumericMethods+
141
- BITWISE_OPERATORS = [:&, :|, :^, :<<, :>>]
141
+ BITWISE_OPERATORS = [:&, :|, :^, :<<, :>>, :%]
142
142
 
143
143
  # Inequality operators used in +InequalityMethods+
144
144
  INEQUALITY_OPERATORS = [:<, :>, :<=, :>=]
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 3
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 34
6
+ MINOR = 35
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0