sequel 3.21.0 → 3.32.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.
- data/CHANGELOG +539 -3
- data/MIT-LICENSE +1 -1
- data/README.rdoc +20 -6
- data/Rakefile +37 -19
- data/bin/sequel +1 -5
- data/doc/advanced_associations.rdoc +17 -0
- data/doc/association_basics.rdoc +293 -43
- data/doc/dataset_basics.rdoc +4 -4
- data/doc/dataset_filtering.rdoc +8 -0
- data/doc/mass_assignment.rdoc +54 -0
- data/doc/migration.rdoc +15 -538
- data/doc/model_hooks.rdoc +72 -27
- data/doc/opening_databases.rdoc +89 -50
- data/doc/prepared_statements.rdoc +40 -13
- data/doc/reflection.rdoc +8 -2
- data/doc/release_notes/3.22.0.txt +39 -0
- data/doc/release_notes/3.23.0.txt +172 -0
- data/doc/release_notes/3.24.0.txt +420 -0
- data/doc/release_notes/3.25.0.txt +88 -0
- data/doc/release_notes/3.26.0.txt +88 -0
- data/doc/release_notes/3.27.0.txt +82 -0
- data/doc/release_notes/3.28.0.txt +304 -0
- data/doc/release_notes/3.29.0.txt +459 -0
- data/doc/release_notes/3.30.0.txt +135 -0
- data/doc/release_notes/3.31.0.txt +146 -0
- data/doc/release_notes/3.32.0.txt +202 -0
- data/doc/schema_modification.rdoc +547 -0
- data/doc/sharding.rdoc +7 -1
- data/doc/testing.rdoc +129 -0
- data/doc/transactions.rdoc +137 -0
- data/doc/virtual_rows.rdoc +2 -2
- data/lib/sequel/adapters/ado/mssql.rb +18 -6
- data/lib/sequel/adapters/ado.rb +40 -18
- data/lib/sequel/adapters/amalgalite.rb +17 -9
- data/lib/sequel/adapters/db2.rb +175 -86
- data/lib/sequel/adapters/dbi.rb +15 -15
- data/lib/sequel/adapters/do/mysql.rb +5 -7
- data/lib/sequel/adapters/do/postgres.rb +0 -5
- data/lib/sequel/adapters/do/sqlite.rb +0 -5
- data/lib/sequel/adapters/do.rb +17 -36
- data/lib/sequel/adapters/firebird.rb +27 -208
- data/lib/sequel/adapters/ibmdb.rb +448 -0
- data/lib/sequel/adapters/informix.rb +6 -23
- data/lib/sequel/adapters/jdbc/as400.rb +16 -34
- data/lib/sequel/adapters/jdbc/db2.rb +52 -0
- data/lib/sequel/adapters/jdbc/derby.rb +260 -0
- data/lib/sequel/adapters/jdbc/firebird.rb +29 -0
- data/lib/sequel/adapters/jdbc/h2.rb +66 -30
- data/lib/sequel/adapters/jdbc/hsqldb.rb +177 -0
- data/lib/sequel/adapters/jdbc/informix.rb +26 -0
- data/lib/sequel/adapters/jdbc/jtds.rb +34 -0
- data/lib/sequel/adapters/jdbc/mssql.rb +3 -33
- data/lib/sequel/adapters/jdbc/mysql.rb +11 -11
- data/lib/sequel/adapters/jdbc/oracle.rb +88 -25
- data/lib/sequel/adapters/jdbc/postgresql.rb +9 -13
- data/lib/sequel/adapters/jdbc/sqlite.rb +2 -6
- data/lib/sequel/adapters/jdbc/sqlserver.rb +71 -0
- data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
- data/lib/sequel/adapters/jdbc.rb +231 -78
- data/lib/sequel/adapters/mock.rb +339 -0
- data/lib/sequel/adapters/mysql.rb +85 -184
- data/lib/sequel/adapters/mysql2.rb +26 -25
- data/lib/sequel/adapters/odbc/db2.rb +17 -0
- data/lib/sequel/adapters/odbc/mssql.rb +1 -6
- data/lib/sequel/adapters/odbc.rb +28 -9
- data/lib/sequel/adapters/openbase.rb +3 -5
- data/lib/sequel/adapters/oracle.rb +355 -53
- data/lib/sequel/adapters/postgres.rb +194 -33
- data/lib/sequel/adapters/shared/access.rb +32 -9
- data/lib/sequel/adapters/shared/db2.rb +328 -0
- data/lib/sequel/adapters/shared/firebird.rb +221 -0
- data/lib/sequel/adapters/shared/informix.rb +53 -0
- data/lib/sequel/adapters/shared/mssql.rb +278 -107
- data/lib/sequel/adapters/shared/mysql.rb +203 -65
- data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +149 -0
- data/lib/sequel/adapters/shared/oracle.rb +237 -46
- data/lib/sequel/adapters/shared/postgres.rb +214 -82
- data/lib/sequel/adapters/shared/progress.rb +5 -8
- data/lib/sequel/adapters/shared/sqlite.rb +200 -50
- data/lib/sequel/adapters/sqlite.rb +29 -21
- data/lib/sequel/adapters/swift/mysql.rb +12 -7
- data/lib/sequel/adapters/swift/postgres.rb +0 -5
- data/lib/sequel/adapters/swift/sqlite.rb +8 -6
- data/lib/sequel/adapters/swift.rb +5 -5
- data/lib/sequel/adapters/tinytds.rb +160 -19
- data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +66 -0
- data/lib/sequel/adapters/utils/stored_procedures.rb +1 -11
- data/lib/sequel/ast_transformer.rb +190 -0
- data/lib/sequel/connection_pool/threaded.rb +3 -2
- data/lib/sequel/connection_pool.rb +1 -1
- data/lib/sequel/core.rb +68 -5
- data/lib/sequel/database/connecting.rb +5 -6
- data/lib/sequel/database/dataset.rb +4 -4
- data/lib/sequel/database/dataset_defaults.rb +59 -1
- data/lib/sequel/database/logging.rb +1 -1
- data/lib/sequel/database/misc.rb +104 -19
- data/lib/sequel/database/query.rb +163 -59
- data/lib/sequel/database/schema_generator.rb +17 -7
- data/lib/sequel/database/schema_methods.rb +59 -29
- data/lib/sequel/dataset/actions.rb +240 -76
- data/lib/sequel/dataset/features.rb +91 -8
- data/lib/sequel/dataset/graph.rb +38 -11
- data/lib/sequel/dataset/misc.rb +50 -23
- data/lib/sequel/dataset/mutation.rb +12 -7
- data/lib/sequel/dataset/prepared_statements.rb +59 -27
- data/lib/sequel/dataset/query.rb +236 -43
- data/lib/sequel/dataset/sql.rb +676 -267
- data/lib/sequel/dataset.rb +8 -0
- data/lib/sequel/exceptions.rb +4 -0
- data/lib/sequel/extensions/columns_introspection.rb +61 -0
- data/lib/sequel/extensions/migration.rb +6 -4
- data/lib/sequel/extensions/named_timezones.rb +5 -0
- data/lib/sequel/extensions/query.rb +1 -1
- data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
- data/lib/sequel/extensions/to_dot.rb +95 -83
- data/lib/sequel/model/associations.rb +1081 -344
- data/lib/sequel/model/base.rb +420 -133
- data/lib/sequel/model/errors.rb +1 -1
- data/lib/sequel/model/exceptions.rb +5 -1
- data/lib/sequel/model.rb +20 -10
- data/lib/sequel/plugins/active_model.rb +13 -1
- data/lib/sequel/plugins/association_pks.rb +22 -4
- data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
- data/lib/sequel/plugins/composition.rb +43 -10
- data/lib/sequel/plugins/dataset_associations.rb +100 -0
- data/lib/sequel/plugins/defaults_setter.rb +58 -0
- data/lib/sequel/plugins/force_encoding.rb +6 -6
- data/lib/sequel/plugins/identity_map.rb +114 -7
- data/lib/sequel/plugins/many_through_many.rb +68 -7
- data/lib/sequel/plugins/nested_attributes.rb +65 -10
- data/lib/sequel/plugins/prepared_statements.rb +151 -0
- data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
- data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
- data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
- data/lib/sequel/plugins/rcte_tree.rb +29 -15
- data/lib/sequel/plugins/serialization.rb +98 -49
- data/lib/sequel/plugins/serialization_modification_detection.rb +63 -0
- data/lib/sequel/plugins/sharding.rb +21 -54
- data/lib/sequel/plugins/single_table_inheritance.rb +3 -1
- data/lib/sequel/plugins/typecast_on_load.rb +9 -12
- data/lib/sequel/plugins/update_primary_key.rb +2 -2
- data/lib/sequel/plugins/xml_serializer.rb +3 -3
- data/lib/sequel/sql.rb +109 -52
- data/lib/sequel/timezones.rb +57 -38
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/db2_spec.rb +146 -0
- data/spec/adapters/mssql_spec.rb +135 -30
- data/spec/adapters/mysql_spec.rb +244 -212
- data/spec/adapters/oracle_spec.rb +67 -77
- data/spec/adapters/postgres_spec.rb +267 -78
- data/spec/adapters/spec_helper.rb +6 -5
- data/spec/adapters/sqlite_spec.rb +35 -21
- data/spec/core/connection_pool_spec.rb +71 -92
- data/spec/core/core_sql_spec.rb +21 -30
- data/spec/core/database_spec.rb +789 -512
- data/spec/core/dataset_spec.rb +1176 -1061
- data/spec/core/expression_filters_spec.rb +159 -42
- data/spec/core/mock_adapter_spec.rb +419 -0
- data/spec/core/object_graph_spec.rb +86 -114
- data/spec/core/schema_generator_spec.rb +3 -3
- data/spec/core/schema_spec.rb +298 -38
- data/spec/core/spec_helper.rb +6 -48
- data/spec/extensions/active_model_spec.rb +13 -0
- data/spec/extensions/association_autoreloading_spec.rb +1 -10
- data/spec/extensions/association_dependencies_spec.rb +2 -12
- data/spec/extensions/association_pks_spec.rb +66 -32
- data/spec/extensions/caching_spec.rb +23 -50
- data/spec/extensions/class_table_inheritance_spec.rb +31 -83
- data/spec/extensions/columns_introspection_spec.rb +91 -0
- data/spec/extensions/composition_spec.rb +18 -13
- data/spec/extensions/dataset_associations_spec.rb +199 -0
- data/spec/extensions/defaults_setter_spec.rb +64 -0
- data/spec/extensions/hook_class_methods_spec.rb +65 -91
- data/spec/extensions/identity_map_spec.rb +114 -22
- data/spec/extensions/instance_filters_spec.rb +11 -21
- data/spec/extensions/instance_hooks_spec.rb +53 -0
- data/spec/extensions/json_serializer_spec.rb +4 -5
- data/spec/extensions/lazy_attributes_spec.rb +16 -20
- data/spec/extensions/list_spec.rb +17 -39
- data/spec/extensions/many_through_many_spec.rb +283 -250
- data/spec/extensions/migration_spec.rb +35 -32
- data/spec/extensions/named_timezones_spec.rb +23 -3
- data/spec/extensions/nested_attributes_spec.rb +97 -88
- data/spec/extensions/optimistic_locking_spec.rb +9 -20
- data/spec/extensions/prepared_statements_associations_spec.rb +111 -0
- data/spec/extensions/prepared_statements_safe_spec.rb +61 -0
- data/spec/extensions/prepared_statements_spec.rb +53 -0
- data/spec/extensions/prepared_statements_with_pk_spec.rb +31 -0
- data/spec/extensions/pretty_table_spec.rb +1 -6
- data/spec/extensions/query_spec.rb +6 -0
- data/spec/extensions/rcte_tree_spec.rb +41 -43
- data/spec/extensions/schema_dumper_spec.rb +5 -8
- data/spec/extensions/schema_spec.rb +13 -21
- data/spec/extensions/serialization_modification_detection_spec.rb +72 -0
- data/spec/extensions/serialization_spec.rb +81 -43
- data/spec/extensions/sharding_spec.rb +66 -140
- data/spec/extensions/single_table_inheritance_spec.rb +18 -29
- data/spec/extensions/spec_helper.rb +13 -61
- data/spec/extensions/sql_expr_spec.rb +20 -60
- data/spec/extensions/tactical_eager_loading_spec.rb +9 -19
- data/spec/extensions/thread_local_timezones_spec.rb +22 -2
- data/spec/extensions/timestamps_spec.rb +6 -6
- data/spec/extensions/to_dot_spec.rb +4 -7
- data/spec/extensions/touch_spec.rb +13 -14
- data/spec/extensions/tree_spec.rb +11 -26
- data/spec/extensions/typecast_on_load_spec.rb +1 -6
- data/spec/extensions/update_primary_key_spec.rb +30 -24
- data/spec/extensions/validation_class_methods_spec.rb +30 -51
- data/spec/extensions/validation_helpers_spec.rb +16 -35
- data/spec/extensions/xml_serializer_spec.rb +16 -4
- data/spec/integration/associations_test.rb +661 -21
- data/spec/integration/database_test.rb +4 -2
- data/spec/integration/dataset_test.rb +500 -67
- data/spec/integration/eager_loader_test.rb +19 -21
- data/spec/integration/model_test.rb +80 -1
- data/spec/integration/plugin_test.rb +367 -119
- data/spec/integration/prepared_statement_test.rb +222 -125
- data/spec/integration/schema_test.rb +217 -39
- data/spec/integration/spec_helper.rb +39 -30
- data/spec/integration/timezone_test.rb +38 -12
- data/spec/integration/transaction_test.rb +182 -5
- data/spec/integration/type_test.rb +17 -3
- data/spec/model/association_reflection_spec.rb +201 -7
- data/spec/model/associations_spec.rb +955 -662
- data/spec/model/base_spec.rb +275 -95
- data/spec/model/dataset_methods_spec.rb +7 -27
- data/spec/model/eager_loading_spec.rb +677 -676
- data/spec/model/hooks_spec.rb +349 -67
- data/spec/model/model_spec.rb +222 -168
- data/spec/model/plugins_spec.rb +24 -13
- data/spec/model/record_spec.rb +381 -218
- data/spec/model/spec_helper.rb +13 -71
- data/spec/model/validations_spec.rb +11 -0
- metadata +117 -73
|
@@ -58,6 +58,16 @@ module Sequel
|
|
|
58
58
|
self[:uses_left_composite_keys] ? (0...self[:through].first[:left].length).map{|i| :"x_foreign_key_#{i}_x"} : :x_foreign_key_x
|
|
59
59
|
end
|
|
60
60
|
|
|
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
|
+
|
|
61
71
|
# The list of joins to use when eager graphing
|
|
62
72
|
def edges
|
|
63
73
|
self[:edges] || calculate_edges || self[:edges]
|
|
@@ -116,6 +126,7 @@ module Sequel
|
|
|
116
126
|
nil
|
|
117
127
|
end
|
|
118
128
|
end
|
|
129
|
+
|
|
119
130
|
module ClassMethods
|
|
120
131
|
# Create a many_through_many association. Arguments:
|
|
121
132
|
# * name - Same as associate, the name of the association.
|
|
@@ -181,13 +192,27 @@ module Sequel
|
|
|
181
192
|
left_key_alias = opts[:left_key_alias] ||= opts.default_associated_key_alias
|
|
182
193
|
opts[:eager_loader] ||= lambda do |eo|
|
|
183
194
|
h = eo[:key_hash][left_pk]
|
|
184
|
-
|
|
195
|
+
rows = eo[:rows]
|
|
196
|
+
rows.each{|object| object.associations[name] = []}
|
|
185
197
|
ds = opts.associated_class
|
|
186
198
|
opts.reverse_edges.each{|t| ds = ds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
|
187
199
|
ft = opts[:final_reverse_edge]
|
|
188
|
-
|
|
189
|
-
ds =
|
|
190
|
-
|
|
200
|
+
ds = ds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + [[opts.eager_loading_predicate_key, h.keys]], :table_alias=>ft[:alias])
|
|
201
|
+
ds = model.eager_loading_dataset(opts, ds, nil, eo[:associations], eo)
|
|
202
|
+
case opts.eager_limit_strategy
|
|
203
|
+
when :window_function
|
|
204
|
+
delete_rn = true
|
|
205
|
+
rn = ds.row_number_column
|
|
206
|
+
ds = apply_window_function_eager_limit_strategy(ds, opts)
|
|
207
|
+
when :correlated_subquery
|
|
208
|
+
ds = apply_correlated_subquery_eager_limit_strategy(ds, opts) do |xds|
|
|
209
|
+
dsa = ds.send(:dataset_alias, 2)
|
|
210
|
+
opts.reverse_edges.each{|t| xds = xds.join(t[:table], Array(t[:left]).zip(Array(t[:right])), :table_alias=>t[:alias])}
|
|
211
|
+
xds.join(ft[:table], Array(ft[:left]).zip(Array(ft[:right])) + left_keys.map{|k| [k, SQL::QualifiedIdentifier.new(ft[:table], k)]}, :table_alias=>dsa)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
ds.all do |assoc_record|
|
|
215
|
+
assoc_record.values.delete(rn) if delete_rn
|
|
191
216
|
hash_key = if uses_lcks
|
|
192
217
|
left_key_alias.map{|k| assoc_record.values.delete(k)}
|
|
193
218
|
else
|
|
@@ -196,6 +221,10 @@ module Sequel
|
|
|
196
221
|
next unless objects = h[hash_key]
|
|
197
222
|
objects.each{|object| object.associations[name].push(assoc_record)}
|
|
198
223
|
end
|
|
224
|
+
if opts.eager_limit_strategy == :ruby
|
|
225
|
+
limit, offset = opts.limit_and_offset
|
|
226
|
+
rows.each{|o| o.associations[name] = o.associations[name].slice(offset||0, limit) || []}
|
|
227
|
+
end
|
|
199
228
|
end
|
|
200
229
|
|
|
201
230
|
join_type = opts[:graph_join_type]
|
|
@@ -204,19 +233,51 @@ module Sequel
|
|
|
204
233
|
only_conditions = opts[:graph_only_conditions]
|
|
205
234
|
use_only_conditions = opts.include?(:graph_only_conditions)
|
|
206
235
|
conditions = opts[:graph_conditions]
|
|
207
|
-
opts[:eager_grapher] ||= proc do |
|
|
208
|
-
|
|
236
|
+
opts[:eager_grapher] ||= proc do |eo|
|
|
237
|
+
ds = eo[:self]
|
|
238
|
+
iq = eo[:implicit_qualifier]
|
|
209
239
|
opts.edges.each do |t|
|
|
210
240
|
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])
|
|
211
241
|
iq = nil
|
|
212
242
|
end
|
|
213
243
|
fe = opts[:final_edge]
|
|
214
|
-
ds.graph(opts.associated_class, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>
|
|
244
|
+
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)
|
|
215
245
|
end
|
|
216
246
|
|
|
217
247
|
def_association_dataset_methods(opts)
|
|
218
248
|
end
|
|
219
249
|
end
|
|
250
|
+
|
|
251
|
+
module DatasetMethods
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
# Use a subquery to filter rows to those related to the given associated object
|
|
255
|
+
def many_through_many_association_filter_expression(op, ref, obj)
|
|
256
|
+
lpks = ref[:left_primary_keys]
|
|
257
|
+
lpks = lpks.first if lpks.length == 1
|
|
258
|
+
lpks = ref.qualify(model.table_name, lpks)
|
|
259
|
+
edges = ref.edges
|
|
260
|
+
first, rest = edges.first, edges[1..-1]
|
|
261
|
+
last = edges.last
|
|
262
|
+
ds = model.db[first[:table]].select(*Array(ref.qualify(first[:table], first[:right])))
|
|
263
|
+
rest.each{|e| ds = ds.join(e[:table], e.fetch(:only_conditions, (Array(e[:right]).zip(Array(e[:left])) + e[:conditions])), :table_alias=>ds.unused_table_alias(e[:table]), &e[:block])}
|
|
264
|
+
last_alias = if rest.empty?
|
|
265
|
+
first[:table]
|
|
266
|
+
else
|
|
267
|
+
last_join = ds.opts[:join].last
|
|
268
|
+
last_join.table_alias || last_join.table
|
|
269
|
+
end
|
|
270
|
+
meths = ref.right_primary_keys
|
|
271
|
+
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)
|
|
273
|
+
if exp == SQL::Constants::FALSE
|
|
274
|
+
association_filter_handle_inversion(op, exp, Array(lpks))
|
|
275
|
+
else
|
|
276
|
+
ds = ds.where(exp).exclude(SQL::BooleanExpression.from_value_pairs(ds.opts[:select].zip([]), :OR))
|
|
277
|
+
association_filter_handle_inversion(op, SQL::BooleanExpression.from_value_pairs(lpks=>ds), Array(lpks))
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
220
281
|
end
|
|
221
282
|
end
|
|
222
283
|
end
|
|
@@ -1,20 +1,75 @@
|
|
|
1
1
|
module Sequel
|
|
2
2
|
module Plugins
|
|
3
|
-
# The nested_attributes plugin allows you to update
|
|
4
|
-
# objects directly
|
|
5
|
-
# Nested
|
|
6
|
-
#
|
|
7
|
-
# Nested attributes are created using the nested_attributes method:
|
|
3
|
+
# The nested_attributes plugin allows you to create, update, and delete
|
|
4
|
+
# associated objects directly by calling a method on the current object.
|
|
5
|
+
# Nested attributes are defined using the nested_attributes class method:
|
|
8
6
|
#
|
|
9
7
|
# Artist.one_to_many :albums
|
|
10
8
|
# Artist.plugin :nested_attributes
|
|
11
9
|
# Artist.nested_attributes :albums
|
|
12
|
-
# a = Artist.new(:name=>'YJM',
|
|
13
|
-
# :albums_attributes=>[{:name=>'RF'}, {:name=>'MO'}])
|
|
14
|
-
# # No database activity yet
|
|
15
10
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
11
|
+
# The nested_attributes call defines a single method, <tt><i>association</i>_attributes=</tt>,
|
|
12
|
+
# (e.g. <tt>albums_attributes=</tt>). So if you have an Artist instance:
|
|
13
|
+
#
|
|
14
|
+
# a = Artist.new(:name=>'YJM')
|
|
15
|
+
#
|
|
16
|
+
# You can create new album instances related to this artist:
|
|
17
|
+
#
|
|
18
|
+
# a.albums_attributes = [{:name=>'RF'}, {:name=>'MO'}]
|
|
19
|
+
#
|
|
20
|
+
# Note that this doesn't send any queries to the database yet. That doesn't happen till
|
|
21
|
+
# you save the object:
|
|
22
|
+
#
|
|
23
|
+
# a.save
|
|
24
|
+
#
|
|
25
|
+
# That will save the artist first, and then save both albums. If either the artist
|
|
26
|
+
# is invalid or one of the albums is invalid, none of the objects will be saved to the
|
|
27
|
+
# database, and all related validation errors will be available in the artist's validation
|
|
28
|
+
# errors.
|
|
29
|
+
#
|
|
30
|
+
# In addition to creating new associated objects, you can also update existing associated
|
|
31
|
+
# objects. You just need to make sure that the primary key field is filled in for the
|
|
32
|
+
# associated object:
|
|
33
|
+
#
|
|
34
|
+
# a.update(:albums_attributes => [{:id=>1, :name=>'T'}])
|
|
35
|
+
#
|
|
36
|
+
# Since the primary key field is filled in, the plugin will update the album with id 1 instead
|
|
37
|
+
# of creating a new album.
|
|
38
|
+
#
|
|
39
|
+
# If you would like to delete the associated object instead of updating it, you add a _delete
|
|
40
|
+
# entry to the hash:
|
|
41
|
+
#
|
|
42
|
+
# a.update(:albums_attributes => [{:id=>1, :_delete=>true}])
|
|
43
|
+
#
|
|
44
|
+
# This will delete the related associated object from the database. If you want to leave the
|
|
45
|
+
# associated object in the database, but just remove it from the association, add a _remove
|
|
46
|
+
# entry in the hash:
|
|
47
|
+
#
|
|
48
|
+
# a.update(:albums_attributes => [{:id=>1, :_remove=>true}])
|
|
49
|
+
#
|
|
50
|
+
# The above example was for a one_to_many association, but the plugin also works similarly
|
|
51
|
+
# for other association types. For one_to_one and many_to_one associations, you need to
|
|
52
|
+
# pass a single hash instead of an array of hashes.
|
|
53
|
+
#
|
|
54
|
+
# This plugin is mainly designed to make it easy to use on html forms, where a single form
|
|
55
|
+
# submission can contained nested attributes (and even nested attributes of those attributes).
|
|
56
|
+
# You just need to name your form inputs correctly:
|
|
57
|
+
#
|
|
58
|
+
# artist[name]
|
|
59
|
+
# artist[albums_attributes][0][:name]
|
|
60
|
+
# artist[albums_attributes][1][:id]
|
|
61
|
+
# artist[albums_attributes][1][:name]
|
|
62
|
+
#
|
|
63
|
+
# Your web stack will probably parse that into a nested hash similar to:
|
|
64
|
+
#
|
|
65
|
+
# {:artist=>{:name=>?, :albums_attributes=>{0=>{:name=>?}, 1=>{:id=>?, :name=>?}}}}
|
|
66
|
+
#
|
|
67
|
+
# Then you can do:
|
|
68
|
+
#
|
|
69
|
+
# artist.update(params[:artist])
|
|
70
|
+
#
|
|
71
|
+
# To save changes to the artist, create the first album and associate it to the artist,
|
|
72
|
+
# and update the other existing associated album.
|
|
18
73
|
module NestedAttributes
|
|
19
74
|
# Depend on the instance_hooks plugin.
|
|
20
75
|
def self.apply(model)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements plugin modifies the model to use prepared statements for
|
|
4
|
+
# instance level deletes and saves, as well as class level lookups by
|
|
5
|
+
# primary key.
|
|
6
|
+
#
|
|
7
|
+
# Note that this plugin is unsafe in some circumstances, as it can allow up to
|
|
8
|
+
# 2^N prepared statements to be created for each type of insert and update query, where
|
|
9
|
+
# N is the number of colums in the table. It is recommended that you use the
|
|
10
|
+
# +prepared_statements_safe+ plugin in addition to this plugin to reduce the number
|
|
11
|
+
# of prepared statements that can be created, unless you tightly control how your
|
|
12
|
+
# model instances are saved.
|
|
13
|
+
#
|
|
14
|
+
# This plugin probably does not work correctly with the instance filters plugin.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
#
|
|
18
|
+
# # Make all model subclasses use prepared statements (called before loading subclasses)
|
|
19
|
+
# Sequel::Model.plugin :prepared_statements
|
|
20
|
+
#
|
|
21
|
+
# # Make the Album class use prepared statements
|
|
22
|
+
# Album.plugin :prepared_statements
|
|
23
|
+
module PreparedStatements
|
|
24
|
+
# Synchronize access to the integer sequence so that no two calls get the same integer.
|
|
25
|
+
MUTEX = Mutex.new
|
|
26
|
+
|
|
27
|
+
i = 0
|
|
28
|
+
# This plugin names prepared statements uniquely using an integer sequence, this
|
|
29
|
+
# lambda returns the next integer to use.
|
|
30
|
+
NEXT = lambda{MUTEX.synchronize{i += 1}}
|
|
31
|
+
|
|
32
|
+
# Setup the datastructure used to hold the prepared statements in the model.
|
|
33
|
+
def self.apply(model)
|
|
34
|
+
model.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module ClassMethods
|
|
38
|
+
# Setup the datastructure used to hold the prepared statements in the subclass.
|
|
39
|
+
def inherited(subclass)
|
|
40
|
+
super
|
|
41
|
+
subclass.instance_variable_set(:@prepared_statements, :insert=>{}, :insert_select=>{}, :update=>{}, :lookup_sql=>{})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Create a prepared statement based on the given dataset with a unique name for the given
|
|
47
|
+
# type of query and values.
|
|
48
|
+
def prepare_statement(ds, type, vals={})
|
|
49
|
+
ds.prepare(type, :"smpsp_#{NEXT.call}", vals)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return a sorted array of columns for use as a hash key.
|
|
53
|
+
def prepared_columns(cols)
|
|
54
|
+
RUBY_VERSION >= '1.9' ? cols.sort : cols.sort_by{|c| c.to_s}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return a prepared statement that can be used to delete a row from this model's dataset.
|
|
58
|
+
def prepared_delete
|
|
59
|
+
@prepared_statements[:delete] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :delete)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Return a prepared statement that can be used to insert a row using the given columns.
|
|
63
|
+
def prepared_insert(cols)
|
|
64
|
+
@prepared_statements[:insert][prepared_columns(cols)] ||= prepare_statement(dataset, :insert, prepared_statement_key_hash(cols))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Return a prepared statement that can be used to insert a row using the given columns
|
|
68
|
+
# and return that column values for the row created.
|
|
69
|
+
def prepared_insert_select(cols)
|
|
70
|
+
if dataset.supports_insert_select?
|
|
71
|
+
@prepared_statements[:insert_select][prepared_columns(cols)] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)), :insert_select, prepared_statement_key_hash(cols))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Return a prepared statement that can be used to lookup a row solely based on the primary key.
|
|
76
|
+
def prepared_lookup
|
|
77
|
+
@prepared_statements[:lookup] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :first)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Return a prepared statement that can be used to refresh a row to get new column values after insertion.
|
|
81
|
+
def prepared_refresh
|
|
82
|
+
@prepared_statements[:refresh] ||= prepare_statement(naked.clone(:server=>dataset.opts.fetch(:server, :default)).filter(prepared_statement_key_array(primary_key)), :first)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Return an array of two element arrays with the column symbol as the first entry and the
|
|
86
|
+
# placeholder symbol as the second entry.
|
|
87
|
+
def prepared_statement_key_array(keys)
|
|
88
|
+
if dataset.requires_placeholder_type_specifiers?
|
|
89
|
+
sch = db_schema
|
|
90
|
+
Array(keys).map do |k|
|
|
91
|
+
if (s = sch[k]) && (t = s[:type])
|
|
92
|
+
[k, :"$#{k}__#{t}"]
|
|
93
|
+
else
|
|
94
|
+
[k, :"$#{k}"]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
Array(keys).map{|k| [k, :"$#{k}"]}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Return a hash mapping column symbols to placeholder symbols.
|
|
103
|
+
def prepared_statement_key_hash(keys)
|
|
104
|
+
Hash[*(prepared_statement_key_array(keys).flatten)]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Return a prepared statement that can be used to update row using the given columns.
|
|
108
|
+
def prepared_update(cols)
|
|
109
|
+
@prepared_statements[:update][prepared_columns(cols)] ||= prepare_statement(filter(prepared_statement_key_array(primary_key)), :update, prepared_statement_key_hash(cols))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Use a prepared statement to query the database for the row matching the given primary key.
|
|
113
|
+
def primary_key_lookup(pk)
|
|
114
|
+
prepared_lookup.call(primary_key_hash(pk))
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module InstanceMethods
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# Use a prepared statement to delete the row.
|
|
122
|
+
def _delete_without_checking
|
|
123
|
+
model.send(:prepared_delete).call(pk_hash)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Use a prepared statement to insert the values into the model's dataset.
|
|
127
|
+
def _insert_raw(ds)
|
|
128
|
+
model.send(:prepared_insert, @values.keys).call(@values)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Use a prepared statement to insert the values into the model's dataset
|
|
132
|
+
# and return the new column values.
|
|
133
|
+
def _insert_select_raw(ds)
|
|
134
|
+
if ps = model.send(:prepared_insert_select, @values.keys)
|
|
135
|
+
ps.call(@values)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Use a prepared statement to refresh this model's column values.
|
|
140
|
+
def _refresh_get(ds)
|
|
141
|
+
model.send(:prepared_refresh).call(pk_hash)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Use a prepared statement to update this model's columns in the database.
|
|
145
|
+
def _update_without_checking(columns)
|
|
146
|
+
model.send(:prepared_update, columns.keys).call(columns.merge(pk_hash))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements_associations plugin modifies the regular association
|
|
4
|
+
# load method to use a cached prepared statement to load the associations.
|
|
5
|
+
# It will not work on all associations, but it should skip the use of prepared
|
|
6
|
+
# statements for associations where it will not work, assuming you load the
|
|
7
|
+
# plugin before defining the associations.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
#
|
|
11
|
+
# # Make all model subclasses more safe when using prepared statements (called before loading subclasses)
|
|
12
|
+
# Sequel::Model.plugin :prepared_statements_associations
|
|
13
|
+
#
|
|
14
|
+
# # Make the Album class more safe when using prepared statements
|
|
15
|
+
# Album.plugin :prepared_statements_associations
|
|
16
|
+
module PreparedStatementsAssociations
|
|
17
|
+
# Synchronize access to the integer sequence so that no two calls get the same integer.
|
|
18
|
+
MUTEX = Mutex.new
|
|
19
|
+
|
|
20
|
+
i = 0
|
|
21
|
+
# This plugin names prepared statements uniquely using an integer sequence, this
|
|
22
|
+
# lambda returns the next integer to use.
|
|
23
|
+
NEXT = lambda{MUTEX.synchronize{i += 1}}
|
|
24
|
+
|
|
25
|
+
module ClassMethods
|
|
26
|
+
# Disable prepared statement use if a block is given, or the :dataset or :conditions
|
|
27
|
+
# options are used, or you are cloning an association.
|
|
28
|
+
def associate(type, name, opts = {}, &block)
|
|
29
|
+
if block || opts[:dataset] || opts[:conditions] || (opts[:clone] && association_reflection(opts[:clone])[:prepared_statement] == false)
|
|
30
|
+
opts = opts.merge(:prepared_statement=>false)
|
|
31
|
+
end
|
|
32
|
+
super(type, name, opts, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module InstanceMethods
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Return a bound variable hash that maps the keys in +ks+ (qualified by the +table+)
|
|
40
|
+
# to the values of the results of sending the methods in +vs+.
|
|
41
|
+
def association_bound_variable_hash(table, ks, vs)
|
|
42
|
+
Hash[*ks.zip(vs).map{|k, v| [:"#{table}.#{k}", send(v)]}.flatten]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Given an association reflection, return a bound variable hash for the given
|
|
46
|
+
# association for this instance's values.
|
|
47
|
+
def association_bound_variables(opts)
|
|
48
|
+
case opts[:type]
|
|
49
|
+
when :many_to_one
|
|
50
|
+
association_bound_variable_hash(opts.associated_class.table_name, opts.primary_keys, opts[:keys])
|
|
51
|
+
when :one_to_many
|
|
52
|
+
association_bound_variable_hash(opts.associated_class.table_name, opts[:keys], opts[:primary_keys])
|
|
53
|
+
when :many_to_many
|
|
54
|
+
association_bound_variable_hash(opts.join_table_alias, opts[:left_keys], opts[:left_primary_keys])
|
|
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])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Given an association reflection, return and cache a prepared statement for this association such
|
|
62
|
+
# that, given appropriate bound variables, the prepared statement will work correctly for any
|
|
63
|
+
# instance.
|
|
64
|
+
def association_prepared_statement(opts)
|
|
65
|
+
opts[:prepared_statement] ||= _associated_dataset(opts, {}).unbind.first.prepare(opts.returns_array? ? :select : :first, :"smpsap_#{NEXT.call}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# If a prepared statement can be used to load the associated objects, execute it to retrieve them. Otherwise,
|
|
69
|
+
# fall back to the default implementation.
|
|
70
|
+
def _load_associated_objects(opts, dynamic_opts={})
|
|
71
|
+
if !opts.can_have_associated_objects?(self) || dynamic_opts[:callback] || (ps = opts[:prepared_statement]) == false
|
|
72
|
+
super
|
|
73
|
+
else
|
|
74
|
+
if bv = association_bound_variables(opts)
|
|
75
|
+
(ps || association_prepared_statement(opts)).call(bv)
|
|
76
|
+
else
|
|
77
|
+
super
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements_safe plugin modifies the model to reduce the number of
|
|
4
|
+
# prepared statements that can be created, by setting as many columns as possible
|
|
5
|
+
# before creating, and by changing +save_changes+ to save all columns instead of
|
|
6
|
+
# just the changed ones.
|
|
7
|
+
#
|
|
8
|
+
# This plugin depends on the +prepared_statements+ plugin.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
#
|
|
12
|
+
# # Make all model subclasses more safe when using prepared statements (called before loading subclasses)
|
|
13
|
+
# Sequel::Model.plugin :prepared_statements_safe
|
|
14
|
+
#
|
|
15
|
+
# # Make the Album class more safe when using prepared statements
|
|
16
|
+
# Album.plugin :prepared_statements_safe
|
|
17
|
+
module PreparedStatementsSafe
|
|
18
|
+
# Depend on the prepared_statements plugin
|
|
19
|
+
def self.apply(model)
|
|
20
|
+
model.plugin(:prepared_statements)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Set the column defaults to use when creating on the model.
|
|
24
|
+
def self.configure(model)
|
|
25
|
+
model.send(:set_prepared_statements_column_defaults)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
# A hash with column symbol keys and default values. Instance's
|
|
30
|
+
# values are merged into this hash before creating to reduce the
|
|
31
|
+
# number of free columns (columns that may or may not be present
|
|
32
|
+
# in the INSERT statement), as the number of prepared statements
|
|
33
|
+
# that can be created is 2^N (where N is the number of free columns).
|
|
34
|
+
attr_reader :prepared_statements_column_defaults
|
|
35
|
+
|
|
36
|
+
def inherited(subclass)
|
|
37
|
+
super
|
|
38
|
+
subclass.instance_variable_set(:@prepared_statements_column_defaults, @prepared_statements_column_defaults) if @prepared_statements_column_defaults && !subclass.prepared_statements_column_defaults
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Set the column defaults to use when creating on the subclass.
|
|
42
|
+
def set_dataset(*)
|
|
43
|
+
x = super
|
|
44
|
+
set_prepared_statements_column_defaults
|
|
45
|
+
x
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Set the column defaults based on the database schema. All columns
|
|
51
|
+
# are set to a default value unless they are a primary key column or
|
|
52
|
+
# they don't have a parseable default.
|
|
53
|
+
def set_prepared_statements_column_defaults
|
|
54
|
+
if db_schema
|
|
55
|
+
h = {}
|
|
56
|
+
db_schema.each do |k, v|
|
|
57
|
+
h[k] = v[:ruby_default] if (v[:ruby_default] || !v[:default]) && !v[:primary_key]
|
|
58
|
+
end
|
|
59
|
+
@prepared_statements_column_defaults = h
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module InstanceMethods
|
|
65
|
+
# Merge the current values into the default values to reduce the number
|
|
66
|
+
# of free columns.
|
|
67
|
+
def before_create
|
|
68
|
+
if v = model.prepared_statements_column_defaults
|
|
69
|
+
set_values(v.merge(values))
|
|
70
|
+
end
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Always do a full save of all columns to reduce the number of prepared
|
|
75
|
+
# statements that can be used.
|
|
76
|
+
def save_changes(opts={})
|
|
77
|
+
save(opts) || false if modified?
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module Sequel
|
|
2
|
+
module Plugins
|
|
3
|
+
# The prepared_statements_with_pk plugin allows Dataset#with_pk for model datasets
|
|
4
|
+
# to use prepared statements by extract the values of previously bound variables
|
|
5
|
+
# using <tt>Dataset#unbind</tt>, and attempting to use a prepared statement if the
|
|
6
|
+
# variables can be unbound correctly. See +Unbinder+ for details about what types of
|
|
7
|
+
# dataset filters can be unbound correctly.
|
|
8
|
+
#
|
|
9
|
+
# This plugin depends on the +prepared_statements+ plugin and should be considered unsafe.
|
|
10
|
+
# Unbinding dataset values cannot be done correctly in all cases, and use of this plugin
|
|
11
|
+
# in cases where not there are variables that are not unbound can lead to an denial of
|
|
12
|
+
# service attack by allocating an arbitrary number of prepared statements. You have been
|
|
13
|
+
# warned.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
#
|
|
17
|
+
# # Make all model subclasses use prepared statements for Dataset#with_pk (called before loading subclasses)
|
|
18
|
+
# Sequel::Model.plugin :prepared_statements_with_pk
|
|
19
|
+
#
|
|
20
|
+
# # Make the Album class use prepared statements for Dataset#with_pk
|
|
21
|
+
# Album.plugin :prepared_statements_with_pk
|
|
22
|
+
module PreparedStatementsWithPk
|
|
23
|
+
# Depend on the prepared_statements plugin
|
|
24
|
+
def self.apply(model)
|
|
25
|
+
model.plugin(:prepared_statements)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module ClassMethods
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Return a prepared statement that can be used to lookup a row given a dataset for the row matching
|
|
32
|
+
# the primary key.
|
|
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)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
module DatasetMethods
|
|
39
|
+
# Use a prepared statement to find a row with the matching primary key
|
|
40
|
+
# inside this dataset.
|
|
41
|
+
def with_pk(pk)
|
|
42
|
+
begin
|
|
43
|
+
ds, bv = unbind
|
|
44
|
+
rescue UnbindDuplicate
|
|
45
|
+
super
|
|
46
|
+
else
|
|
47
|
+
begin
|
|
48
|
+
bv = bv.merge!(model.primary_key_hash(pk)){|k, v1, v2| ((v1 == v2) ? v1 : raise(UnbindDuplicate))}
|
|
49
|
+
rescue UnbindDuplicate
|
|
50
|
+
super
|
|
51
|
+
else
|
|
52
|
+
model.send(:prepared_lookup_dataset, ds).call(bv)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|