sequel 3.21.0 → 3.29.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 (200) hide show
  1. data/CHANGELOG +413 -3
  2. data/README.rdoc +20 -6
  3. data/Rakefile +23 -16
  4. data/bin/sequel +1 -5
  5. data/doc/association_basics.rdoc +210 -43
  6. data/doc/dataset_basics.rdoc +4 -4
  7. data/doc/mass_assignment.rdoc +54 -0
  8. data/doc/migration.rdoc +15 -538
  9. data/doc/model_hooks.rdoc +72 -27
  10. data/doc/opening_databases.rdoc +86 -50
  11. data/doc/prepared_statements.rdoc +40 -13
  12. data/doc/reflection.rdoc +8 -2
  13. data/doc/release_notes/3.22.0.txt +39 -0
  14. data/doc/release_notes/3.23.0.txt +172 -0
  15. data/doc/release_notes/3.24.0.txt +420 -0
  16. data/doc/release_notes/3.25.0.txt +88 -0
  17. data/doc/release_notes/3.26.0.txt +88 -0
  18. data/doc/release_notes/3.27.0.txt +82 -0
  19. data/doc/release_notes/3.28.0.txt +304 -0
  20. data/doc/release_notes/3.29.0.txt +459 -0
  21. data/doc/schema_modification.rdoc +547 -0
  22. data/doc/sharding.rdoc +7 -1
  23. data/doc/testing.rdoc +115 -0
  24. data/doc/transactions.rdoc +137 -0
  25. data/doc/virtual_rows.rdoc +2 -2
  26. data/lib/sequel/adapters/ado/mssql.rb +18 -6
  27. data/lib/sequel/adapters/ado.rb +40 -18
  28. data/lib/sequel/adapters/amalgalite.rb +15 -7
  29. data/lib/sequel/adapters/db2.rb +175 -86
  30. data/lib/sequel/adapters/dbi.rb +15 -15
  31. data/lib/sequel/adapters/do/mysql.rb +0 -5
  32. data/lib/sequel/adapters/do/postgres.rb +0 -5
  33. data/lib/sequel/adapters/do/sqlite.rb +0 -5
  34. data/lib/sequel/adapters/do.rb +17 -36
  35. data/lib/sequel/adapters/firebird.rb +27 -208
  36. data/lib/sequel/adapters/ibmdb.rb +448 -0
  37. data/lib/sequel/adapters/informix.rb +6 -23
  38. data/lib/sequel/adapters/jdbc/as400.rb +5 -31
  39. data/lib/sequel/adapters/jdbc/db2.rb +44 -0
  40. data/lib/sequel/adapters/jdbc/derby.rb +217 -0
  41. data/lib/sequel/adapters/jdbc/firebird.rb +29 -0
  42. data/lib/sequel/adapters/jdbc/h2.rb +26 -17
  43. data/lib/sequel/adapters/jdbc/hsqldb.rb +166 -0
  44. data/lib/sequel/adapters/jdbc/informix.rb +26 -0
  45. data/lib/sequel/adapters/jdbc/jtds.rb +29 -0
  46. data/lib/sequel/adapters/jdbc/mssql.rb +0 -32
  47. data/lib/sequel/adapters/jdbc/mysql.rb +9 -10
  48. data/lib/sequel/adapters/jdbc/oracle.rb +67 -25
  49. data/lib/sequel/adapters/jdbc/postgresql.rb +6 -11
  50. data/lib/sequel/adapters/jdbc/sqlite.rb +0 -5
  51. data/lib/sequel/adapters/jdbc/sqlserver.rb +41 -0
  52. data/lib/sequel/adapters/jdbc/transactions.rb +83 -0
  53. data/lib/sequel/adapters/jdbc.rb +107 -38
  54. data/lib/sequel/adapters/mock.rb +315 -0
  55. data/lib/sequel/adapters/mysql.rb +78 -182
  56. data/lib/sequel/adapters/mysql2.rb +24 -23
  57. data/lib/sequel/adapters/odbc/db2.rb +17 -0
  58. data/lib/sequel/adapters/odbc/mssql.rb +0 -5
  59. data/lib/sequel/adapters/odbc.rb +28 -9
  60. data/lib/sequel/adapters/openbase.rb +2 -4
  61. data/lib/sequel/adapters/oracle.rb +349 -51
  62. data/lib/sequel/adapters/postgres.rb +169 -21
  63. data/lib/sequel/adapters/shared/access.rb +21 -6
  64. data/lib/sequel/adapters/shared/db2.rb +288 -0
  65. data/lib/sequel/adapters/shared/firebird.rb +214 -0
  66. data/lib/sequel/adapters/shared/informix.rb +45 -0
  67. data/lib/sequel/adapters/shared/mssql.rb +108 -65
  68. data/lib/sequel/adapters/shared/mysql.rb +56 -13
  69. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +149 -0
  70. data/lib/sequel/adapters/shared/oracle.rb +185 -34
  71. data/lib/sequel/adapters/shared/postgres.rb +107 -58
  72. data/lib/sequel/adapters/shared/progress.rb +0 -6
  73. data/lib/sequel/adapters/shared/sqlite.rb +168 -41
  74. data/lib/sequel/adapters/sqlite.rb +27 -18
  75. data/lib/sequel/adapters/swift/mysql.rb +9 -5
  76. data/lib/sequel/adapters/swift/postgres.rb +0 -5
  77. data/lib/sequel/adapters/swift/sqlite.rb +6 -4
  78. data/lib/sequel/adapters/swift.rb +5 -5
  79. data/lib/sequel/adapters/tinytds.rb +152 -17
  80. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +63 -0
  81. data/lib/sequel/ast_transformer.rb +190 -0
  82. data/lib/sequel/connection_pool/threaded.rb +3 -2
  83. data/lib/sequel/connection_pool.rb +1 -1
  84. data/lib/sequel/core.rb +46 -5
  85. data/lib/sequel/database/connecting.rb +5 -6
  86. data/lib/sequel/database/dataset.rb +4 -4
  87. data/lib/sequel/database/dataset_defaults.rb +59 -1
  88. data/lib/sequel/database/logging.rb +1 -1
  89. data/lib/sequel/database/misc.rb +100 -19
  90. data/lib/sequel/database/query.rb +153 -58
  91. data/lib/sequel/database/schema_generator.rb +8 -5
  92. data/lib/sequel/database/schema_methods.rb +59 -29
  93. data/lib/sequel/dataset/actions.rb +194 -57
  94. data/lib/sequel/dataset/features.rb +81 -8
  95. data/lib/sequel/dataset/graph.rb +8 -7
  96. data/lib/sequel/dataset/misc.rb +50 -23
  97. data/lib/sequel/dataset/mutation.rb +5 -6
  98. data/lib/sequel/dataset/prepared_statements.rb +32 -15
  99. data/lib/sequel/dataset/query.rb +223 -42
  100. data/lib/sequel/dataset/sql.rb +58 -61
  101. data/lib/sequel/dataset.rb +8 -0
  102. data/lib/sequel/exceptions.rb +4 -0
  103. data/lib/sequel/extensions/columns_introspection.rb +61 -0
  104. data/lib/sequel/extensions/migration.rb +6 -4
  105. data/lib/sequel/extensions/named_timezones.rb +5 -0
  106. data/lib/sequel/extensions/thread_local_timezones.rb +1 -1
  107. data/lib/sequel/extensions/to_dot.rb +95 -83
  108. data/lib/sequel/model/associations.rb +997 -331
  109. data/lib/sequel/model/base.rb +386 -129
  110. data/lib/sequel/model/errors.rb +1 -1
  111. data/lib/sequel/model/exceptions.rb +5 -1
  112. data/lib/sequel/model.rb +15 -8
  113. data/lib/sequel/plugins/association_pks.rb +22 -4
  114. data/lib/sequel/plugins/class_table_inheritance.rb +4 -4
  115. data/lib/sequel/plugins/dataset_associations.rb +100 -0
  116. data/lib/sequel/plugins/defaults_setter.rb +58 -0
  117. data/lib/sequel/plugins/force_encoding.rb +6 -6
  118. data/lib/sequel/plugins/identity_map.rb +114 -7
  119. data/lib/sequel/plugins/many_through_many.rb +65 -7
  120. data/lib/sequel/plugins/prepared_statements.rb +151 -0
  121. data/lib/sequel/plugins/prepared_statements_associations.rb +84 -0
  122. data/lib/sequel/plugins/prepared_statements_safe.rb +82 -0
  123. data/lib/sequel/plugins/prepared_statements_with_pk.rb +59 -0
  124. data/lib/sequel/plugins/rcte_tree.rb +29 -15
  125. data/lib/sequel/plugins/serialization.rb +6 -1
  126. data/lib/sequel/plugins/serialization_modification_detection.rb +51 -0
  127. data/lib/sequel/plugins/sharding.rb +12 -25
  128. data/lib/sequel/plugins/single_table_inheritance.rb +3 -1
  129. data/lib/sequel/plugins/typecast_on_load.rb +9 -12
  130. data/lib/sequel/plugins/update_primary_key.rb +2 -2
  131. data/lib/sequel/plugins/xml_serializer.rb +3 -3
  132. data/lib/sequel/sql.rb +107 -51
  133. data/lib/sequel/timezones.rb +44 -35
  134. data/lib/sequel/version.rb +1 -1
  135. data/spec/adapters/db2_spec.rb +146 -0
  136. data/spec/adapters/mssql_spec.rb +65 -29
  137. data/spec/adapters/mysql_spec.rb +122 -123
  138. data/spec/adapters/oracle_spec.rb +48 -76
  139. data/spec/adapters/postgres_spec.rb +213 -61
  140. data/spec/adapters/spec_helper.rb +6 -5
  141. data/spec/adapters/sqlite_spec.rb +35 -21
  142. data/spec/core/connection_pool_spec.rb +71 -92
  143. data/spec/core/core_sql_spec.rb +20 -31
  144. data/spec/core/database_spec.rb +729 -508
  145. data/spec/core/dataset_spec.rb +980 -1044
  146. data/spec/core/expression_filters_spec.rb +159 -42
  147. data/spec/core/mock_adapter_spec.rb +378 -0
  148. data/spec/core/object_graph_spec.rb +48 -114
  149. data/spec/core/schema_generator_spec.rb +3 -3
  150. data/spec/core/schema_spec.rb +298 -38
  151. data/spec/core/spec_helper.rb +6 -48
  152. data/spec/extensions/association_pks_spec.rb +38 -0
  153. data/spec/extensions/class_table_inheritance_spec.rb +1 -1
  154. data/spec/extensions/columns_introspection_spec.rb +91 -0
  155. data/spec/extensions/dataset_associations_spec.rb +199 -0
  156. data/spec/extensions/defaults_setter_spec.rb +64 -0
  157. data/spec/extensions/identity_map_spec.rb +162 -0
  158. data/spec/extensions/instance_hooks_spec.rb +71 -0
  159. data/spec/extensions/many_through_many_spec.rb +195 -20
  160. data/spec/extensions/migration_spec.rb +17 -17
  161. data/spec/extensions/named_timezones_spec.rb +22 -2
  162. data/spec/extensions/nested_attributes_spec.rb +4 -0
  163. data/spec/extensions/prepared_statements_associations_spec.rb +126 -0
  164. data/spec/extensions/prepared_statements_safe_spec.rb +79 -0
  165. data/spec/extensions/prepared_statements_spec.rb +72 -0
  166. data/spec/extensions/prepared_statements_with_pk_spec.rb +38 -0
  167. data/spec/extensions/schema_dumper_spec.rb +2 -2
  168. data/spec/extensions/schema_spec.rb +13 -21
  169. data/spec/extensions/serialization_modification_detection_spec.rb +37 -0
  170. data/spec/extensions/serialization_spec.rb +5 -8
  171. data/spec/extensions/single_table_inheritance_spec.rb +11 -0
  172. data/spec/extensions/spec_helper.rb +7 -1
  173. data/spec/extensions/thread_local_timezones_spec.rb +22 -2
  174. data/spec/extensions/to_dot_spec.rb +3 -5
  175. data/spec/extensions/typecast_on_load_spec.rb +1 -6
  176. data/spec/extensions/xml_serializer_spec.rb +16 -4
  177. data/spec/integration/associations_test.rb +522 -21
  178. data/spec/integration/database_test.rb +4 -2
  179. data/spec/integration/dataset_test.rb +375 -62
  180. data/spec/integration/eager_loader_test.rb +19 -21
  181. data/spec/integration/model_test.rb +80 -1
  182. data/spec/integration/plugin_test.rb +304 -116
  183. data/spec/integration/prepared_statement_test.rb +200 -120
  184. data/spec/integration/schema_test.rb +161 -30
  185. data/spec/integration/spec_helper.rb +39 -30
  186. data/spec/integration/timezone_test.rb +38 -12
  187. data/spec/integration/transaction_test.rb +172 -5
  188. data/spec/integration/type_test.rb +17 -3
  189. data/spec/model/association_reflection_spec.rb +174 -7
  190. data/spec/model/associations_spec.rb +849 -661
  191. data/spec/model/base_spec.rb +255 -95
  192. data/spec/model/dataset_methods_spec.rb +7 -27
  193. data/spec/model/eager_loading_spec.rb +640 -676
  194. data/spec/model/hooks_spec.rb +309 -67
  195. data/spec/model/model_spec.rb +207 -167
  196. data/spec/model/plugins_spec.rb +24 -13
  197. data/spec/model/record_spec.rb +321 -218
  198. data/spec/model/spec_helper.rb +13 -71
  199. data/spec/model/validations_spec.rb +11 -0
  200. metadata +95 -38
@@ -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
@@ -109,7 +109,15 @@ module Sequel
109
109
  ka = opts[:key_alias] ||= :x_root_x
110
110
  t = opts[:cte_name] ||= :t
111
111
  opts[:reciprocal] = nil
112
- c_all = SQL::ColumnAll.new(model.table_name)
112
+ c_all = if model.dataset.recursive_cte_requires_column_aliases?
113
+ # Work around Oracle/ruby-oci8 bug that returns integers as BigDecimals in recursive queries.
114
+ conv_bd = model.db.database_type == :oracle
115
+ col_aliases = model.dataset.columns
116
+ model_table = model.table_name
117
+ col_aliases.map{|c| SQL::QualifiedIdentifier.new(model_table, c)}
118
+ else
119
+ [SQL::ColumnAll.new(model.table_name)]
120
+ end
113
121
 
114
122
  a = opts.merge(opts.fetch(:ancestors, {}))
115
123
  ancestors = a.fetch(:name, :ancestors)
@@ -125,9 +133,9 @@ module Sequel
125
133
  end
126
134
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
127
135
  model.from(t => table_alias).
128
- with_recursive(t, base_ds.select_all,
129
- recursive_ds.
130
- select(c_all))
136
+ with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
137
+ recursive_ds.select(*c_all),
138
+ :args=>col_aliases)
131
139
  end
132
140
  aal = Array(a[:after_load])
133
141
  aal << proc do |m, ancs|
@@ -163,9 +171,9 @@ module Sequel
163
171
  end
164
172
  r = model.association_reflection(ancestors)
165
173
  base_case = model.filter(prkey=>id_map.keys).
166
- select(SQL::AliasedExpression.new(prkey, ka), c_all)
174
+ select(SQL::AliasedExpression.new(prkey, ka), *c_all)
167
175
  recursive_case = model.join(t, key=>prkey).
168
- select(SQL::QualifiedIdentifier.new(t, ka), c_all)
176
+ select(SQL::QualifiedIdentifier.new(t, ka), *c_all)
169
177
  if c = r[:conditions]
170
178
  (base_case, recursive_case) = [base_case, recursive_case].collect do |ds|
171
179
  (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
@@ -175,7 +183,8 @@ module Sequel
175
183
  elds = model.eager_loading_dataset(r,
176
184
  model.from(t => table_alias).
177
185
  with_recursive(t, base_case,
178
- recursive_case),
186
+ recursive_case,
187
+ :args=>(([ka] + col_aliases) if col_aliases)),
179
188
  r.select,
180
189
  eo[:associations], eo)
181
190
  elds = elds.select_append(ka) unless elds.opts[:select] == nil
@@ -192,7 +201,9 @@ module Sequel
192
201
  (children_map[obj[key]] ||= []) << obj
193
202
  end
194
203
 
195
- if roots = id_map[obj.values.delete(ka)]
204
+ kv = obj.values.delete(ka)
205
+ kv = kv.to_i if conv_bd && kv.is_a?(BigDecimal)
206
+ if roots = id_map[kv]
196
207
  roots.each do |root|
197
208
  root.associations[ancestors] << obj
198
209
  end
@@ -222,9 +233,9 @@ module Sequel
222
233
  end
223
234
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
224
235
  model.from(t => table_alias).
225
- with_recursive(t, base_ds.select_all,
226
- recursive_ds.
227
- select(SQL::ColumnAll.new(model.table_name)))
236
+ with_recursive(t, col_aliases ? base_ds.select(*col_aliases) : base_ds.select_all,
237
+ recursive_ds.select(*c_all),
238
+ :args=>col_aliases)
228
239
  end
229
240
  dal = Array(d[:after_load])
230
241
  dal << proc do |m, descs|
@@ -259,9 +270,9 @@ module Sequel
259
270
  end
260
271
  r = model.association_reflection(descendants)
261
272
  base_case = model.filter(key=>id_map.keys).
262
- select(SQL::AliasedExpression.new(key, ka), c_all)
273
+ select(SQL::AliasedExpression.new(key, ka), *c_all)
263
274
  recursive_case = model.join(t, prkey=>key).
264
- select(SQL::QualifiedIdentifier.new(t, ka), c_all)
275
+ select(SQL::QualifiedIdentifier.new(t, ka), *c_all)
265
276
  if c = r[:conditions]
266
277
  (base_case, recursive_case) = [base_case, recursive_case].collect do |ds|
267
278
  (c.is_a?(Array) && !Sequel.condition_specifier?(c)) ? ds.filter(*c) : ds.filter(c)
@@ -276,7 +287,8 @@ module Sequel
276
287
  end
277
288
  table_alias = model.dataset.schema_and_table(model.table_name)[1].to_sym
278
289
  elds = model.eager_loading_dataset(r,
279
- model.from(t => table_alias).with_recursive(t, base_case, recursive_case),
290
+ model.from(t => table_alias).with_recursive(t, base_case, recursive_case,
291
+ :args=>(([ka] + col_aliases + (level ? [la] : [])) if col_aliases)),
280
292
  r.select,
281
293
  associations, eo)
282
294
  elds = elds.select_append(ka) unless elds.opts[:select] == nil
@@ -296,7 +308,9 @@ module Sequel
296
308
  parent_map[opk] = obj
297
309
  end
298
310
 
299
- if root = id_map[obj.values.delete(ka)].first
311
+ kv = obj.values.delete(ka)
312
+ kv = kv.to_i if conv_bd && kv.is_a?(BigDecimal)
313
+ if root = id_map[kv].first
300
314
  root.associations[descendants] << obj
301
315
  end
302
316
 
@@ -110,7 +110,7 @@ module Sequel
110
110
  attr_reader :deserialized_values
111
111
 
112
112
  # Set @deserialized_values to the empty hash
113
- def initialize(*args, &block)
113
+ def initialize_set(values)
114
114
  @deserialized_values = {}
115
115
  super
116
116
  end
@@ -127,6 +127,11 @@ module Sequel
127
127
  super
128
128
  end
129
129
 
130
+ def set_values(*)
131
+ @deserialized_values ||= {}
132
+ super
133
+ end
134
+
130
135
  private
131
136
 
132
137
  # Deserialize the column from either marshal or yaml format
@@ -0,0 +1,51 @@
1
+ module Sequel
2
+ module Plugins
3
+ # Sequel's built in Serialization plugin doesn't check for modification
4
+ # of the serialized objects, because it requires an extra deserialization of a potentially
5
+ # very large object. This plugin can detect changes in serialized values by
6
+ # checking whether the current deserialized value is the same as the original
7
+ # deserialized value. This does require deserializing the value twice, but the
8
+ # original deserialized value is cached.
9
+ #
10
+ # == Example
11
+ #
12
+ # require 'sequel'
13
+ # require 'json'
14
+ # class User < Sequel::Model
15
+ # plugin :serialization, :json, :permissions
16
+ # plugin :serialization_modification_detection
17
+ # end
18
+ # user = User.create(:permissions => {})
19
+ # user.permissions[:global] = 'read-only'
20
+ # user.save_changes
21
+ module SerializationModificationDetection
22
+ # Load the serialization plugin automatically.
23
+ def self.apply(model)
24
+ model.plugin :serialization
25
+ end
26
+
27
+ module InstanceMethods
28
+ # Detect which serialized columns have changed.
29
+ def changed_columns
30
+ cc = super
31
+ deserialized_values.each{|c, v| cc << c if !cc.include?(c) && original_deserialized_value(c) != v}
32
+ cc
33
+ end
34
+
35
+ private
36
+
37
+ # Clear the cache of original deserialized values after saving so that it doesn't
38
+ # show the column is modified after saving.
39
+ def after_save
40
+ super
41
+ @original_deserialized_values.clear if @original_deserialized_values
42
+ end
43
+
44
+ # Return the original deserialized value of the column, caching it to improve performance.
45
+ def original_deserialized_value(column)
46
+ (@original_deserialized_values ||= {})[column] ||= deserialize_value(column, self[column])
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -40,6 +40,18 @@ module Sequel
40
40
  def new_using_server(s, values={}, &block)
41
41
  new(values, &block).set_server(s)
42
42
  end
43
+
44
+ private
45
+
46
+ # Set the server for each graphed dataset to the current server
47
+ # unless the graphed dataset already has a server set.
48
+ def eager_graph_dataset(opts, eager_options)
49
+ ds = super
50
+ if s = eager_options[:self].opts[:server]
51
+ ds = ds.server(s) unless ds.opts[:server]
52
+ end
53
+ ds
54
+ end
43
55
  end
44
56
 
45
57
  module InstanceMethods
@@ -78,11 +90,6 @@ module Sequel
78
90
  use_server(super)
79
91
  end
80
92
 
81
- # Make sure to use the correct shard when using a transaction
82
- def checked_transaction(opts={}, &block)
83
- super(@server ? {:server=>@server}.merge(opts) : opts, &block)
84
- end
85
-
86
93
  # If creating the object by doing <tt>add_association</tt> for a
87
94
  # +many_to_many+ association, make sure the associated object is created on the
88
95
  # current object's shard, unless the passed object already has an assigned shard.
@@ -109,26 +116,6 @@ module Sequel
109
116
  end
110
117
  ds
111
118
  end
112
-
113
- private
114
-
115
- # Set the shard of all retrieved objects to the shard of
116
- # the table's dataset, or the current shard if the table's
117
- # dataset does not have a shard.
118
- def graph_each
119
- ta = @opts[:graph][:table_aliases]
120
- s = @opts[:server]
121
- super do |r|
122
- r.each do |k, v|
123
- if ds = ta[k]
124
- dss = ds.opts[:server]
125
- end
126
- vs = dss || s
127
- v.set_server(vs) if vs && v.respond_to?(:set_server)
128
- end
129
- yield r
130
- end
131
- end
132
119
  end
133
120
  end
134
121
  end
@@ -108,8 +108,10 @@ module Sequel
108
108
  key = skm[subclass]
109
109
  sti_subclass_added(key)
110
110
  ska = [key]
111
+ rp = dataset.row_proc
111
112
  subclass.set_dataset(sd.filter(SQL::QualifiedIdentifier.new(table_name, sk)=>ska), :inherited=>true)
112
113
  subclass.instance_eval do
114
+ dataset.row_proc = rp
113
115
  @sti_key = sk
114
116
  @sti_key_array = ska
115
117
  @sti_dataset = sd
@@ -122,7 +124,7 @@ module Sequel
122
124
  # Return an instance of the class specified by sti_key,
123
125
  # used by the row_proc.
124
126
  def sti_load(r)
125
- sti_class(sti_model_map[r[sti_key]]).load(r)
127
+ sti_class(sti_model_map[r[sti_key]]).call(r)
126
128
  end
127
129
 
128
130
  # Make sure that all subclasses of the parent class correctly include
@@ -5,7 +5,7 @@ module Sequel
5
5
  # typecast correctly (with correct being defined as how the model object
6
6
  # would typecast the same column values).
7
7
  #
8
- # This plugin modifies Model.load to call the setter methods (which typecast
8
+ # This plugin modifies Model#set_values to call the setter methods (which typecast
9
9
  # by default) for all columns given. You can either specify the columns to
10
10
  # typecast on load in the plugin call itself, or afterwards using
11
11
  # add_typecast_on_load_columns:
@@ -41,16 +41,11 @@ module Sequel
41
41
  super
42
42
  subclass.instance_variable_set(:@typecast_on_load_columns, typecast_on_load_columns.dup)
43
43
  end
44
-
45
- # Call the setter method for each of the typecast on load columns,
46
- # ensuring the model object will have the correct typecasting even
47
- # if the database doesn't typecast the columns correctly.
48
- def load(values)
49
- super.load_typecast
50
- end
51
44
  end
52
45
 
53
46
  module InstanceMethods
47
+ # Call the setter method for each of the model's typecast_on_load_columns
48
+ # with the current value, so it can be typecasted correctly.
54
49
  def load_typecast
55
50
  model.typecast_on_load_columns.each do |c|
56
51
  if v = values[c]
@@ -61,10 +56,12 @@ module Sequel
61
56
  self
62
57
  end
63
58
 
64
- private
65
-
66
- def _refresh(dataset)
67
- super.load_typecast
59
+ # Typecast values using #load_typecast when the values are retrieved from
60
+ # the database.
61
+ def set_values(values)
62
+ ret = super
63
+ load_typecast
64
+ ret
68
65
  end
69
66
  end
70
67
  end