sequel 5.45.0 → 5.77.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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +434 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +59 -27
  5. data/bin/sequel +11 -3
  6. data/doc/advanced_associations.rdoc +16 -14
  7. data/doc/association_basics.rdoc +119 -24
  8. data/doc/cheat_sheet.rdoc +11 -3
  9. data/doc/mass_assignment.rdoc +1 -1
  10. data/doc/migration.rdoc +27 -6
  11. data/doc/model_hooks.rdoc +1 -1
  12. data/doc/object_model.rdoc +8 -8
  13. data/doc/opening_databases.rdoc +28 -12
  14. data/doc/postgresql.rdoc +16 -8
  15. data/doc/querying.rdoc +5 -3
  16. data/doc/release_notes/5.46.0.txt +87 -0
  17. data/doc/release_notes/5.47.0.txt +59 -0
  18. data/doc/release_notes/5.48.0.txt +14 -0
  19. data/doc/release_notes/5.49.0.txt +59 -0
  20. data/doc/release_notes/5.50.0.txt +78 -0
  21. data/doc/release_notes/5.51.0.txt +47 -0
  22. data/doc/release_notes/5.52.0.txt +87 -0
  23. data/doc/release_notes/5.53.0.txt +23 -0
  24. data/doc/release_notes/5.54.0.txt +27 -0
  25. data/doc/release_notes/5.55.0.txt +21 -0
  26. data/doc/release_notes/5.56.0.txt +51 -0
  27. data/doc/release_notes/5.57.0.txt +23 -0
  28. data/doc/release_notes/5.58.0.txt +31 -0
  29. data/doc/release_notes/5.59.0.txt +73 -0
  30. data/doc/release_notes/5.60.0.txt +22 -0
  31. data/doc/release_notes/5.61.0.txt +43 -0
  32. data/doc/release_notes/5.62.0.txt +132 -0
  33. data/doc/release_notes/5.63.0.txt +33 -0
  34. data/doc/release_notes/5.64.0.txt +50 -0
  35. data/doc/release_notes/5.65.0.txt +21 -0
  36. data/doc/release_notes/5.66.0.txt +24 -0
  37. data/doc/release_notes/5.67.0.txt +32 -0
  38. data/doc/release_notes/5.68.0.txt +61 -0
  39. data/doc/release_notes/5.69.0.txt +26 -0
  40. data/doc/release_notes/5.70.0.txt +35 -0
  41. data/doc/release_notes/5.71.0.txt +21 -0
  42. data/doc/release_notes/5.72.0.txt +33 -0
  43. data/doc/release_notes/5.73.0.txt +66 -0
  44. data/doc/release_notes/5.74.0.txt +45 -0
  45. data/doc/release_notes/5.75.0.txt +35 -0
  46. data/doc/release_notes/5.76.0.txt +86 -0
  47. data/doc/release_notes/5.77.0.txt +63 -0
  48. data/doc/schema_modification.rdoc +1 -1
  49. data/doc/security.rdoc +9 -9
  50. data/doc/sharding.rdoc +3 -1
  51. data/doc/sql.rdoc +27 -15
  52. data/doc/testing.rdoc +23 -13
  53. data/doc/transactions.rdoc +6 -6
  54. data/doc/virtual_rows.rdoc +1 -1
  55. data/lib/sequel/adapters/ado/access.rb +1 -1
  56. data/lib/sequel/adapters/ado.rb +1 -1
  57. data/lib/sequel/adapters/amalgalite.rb +3 -5
  58. data/lib/sequel/adapters/ibmdb.rb +3 -3
  59. data/lib/sequel/adapters/jdbc/derby.rb +8 -0
  60. data/lib/sequel/adapters/jdbc/h2.rb +63 -10
  61. data/lib/sequel/adapters/jdbc/hsqldb.rb +8 -0
  62. data/lib/sequel/adapters/jdbc/postgresql.rb +7 -4
  63. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  64. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  65. data/lib/sequel/adapters/jdbc.rb +24 -22
  66. data/lib/sequel/adapters/mysql.rb +92 -67
  67. data/lib/sequel/adapters/mysql2.rb +56 -51
  68. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  69. data/lib/sequel/adapters/odbc.rb +1 -1
  70. data/lib/sequel/adapters/oracle.rb +4 -3
  71. data/lib/sequel/adapters/postgres.rb +89 -45
  72. data/lib/sequel/adapters/shared/access.rb +11 -1
  73. data/lib/sequel/adapters/shared/db2.rb +42 -0
  74. data/lib/sequel/adapters/shared/mssql.rb +91 -10
  75. data/lib/sequel/adapters/shared/mysql.rb +78 -3
  76. data/lib/sequel/adapters/shared/oracle.rb +86 -7
  77. data/lib/sequel/adapters/shared/postgres.rb +576 -171
  78. data/lib/sequel/adapters/shared/sqlanywhere.rb +21 -5
  79. data/lib/sequel/adapters/shared/sqlite.rb +92 -8
  80. data/lib/sequel/adapters/sqlanywhere.rb +1 -1
  81. data/lib/sequel/adapters/sqlite.rb +99 -18
  82. data/lib/sequel/adapters/tinytds.rb +1 -1
  83. data/lib/sequel/adapters/trilogy.rb +117 -0
  84. data/lib/sequel/adapters/utils/columns_limit_1.rb +22 -0
  85. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
  86. data/lib/sequel/ast_transformer.rb +6 -0
  87. data/lib/sequel/connection_pool/sharded_single.rb +5 -7
  88. data/lib/sequel/connection_pool/sharded_threaded.rb +16 -11
  89. data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
  90. data/lib/sequel/connection_pool/single.rb +6 -8
  91. data/lib/sequel/connection_pool/threaded.rb +14 -8
  92. data/lib/sequel/connection_pool/timed_queue.rb +270 -0
  93. data/lib/sequel/connection_pool.rb +57 -31
  94. data/lib/sequel/core.rb +17 -18
  95. data/lib/sequel/database/connecting.rb +27 -3
  96. data/lib/sequel/database/dataset.rb +16 -6
  97. data/lib/sequel/database/misc.rb +70 -14
  98. data/lib/sequel/database/query.rb +73 -2
  99. data/lib/sequel/database/schema_generator.rb +11 -6
  100. data/lib/sequel/database/schema_methods.rb +23 -4
  101. data/lib/sequel/database/transactions.rb +6 -0
  102. data/lib/sequel/dataset/actions.rb +111 -15
  103. data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +42 -0
  104. data/lib/sequel/dataset/features.rb +20 -1
  105. data/lib/sequel/dataset/misc.rb +12 -2
  106. data/lib/sequel/dataset/placeholder_literalizer.rb +20 -9
  107. data/lib/sequel/dataset/query.rb +170 -41
  108. data/lib/sequel/dataset/sql.rb +190 -71
  109. data/lib/sequel/dataset.rb +4 -0
  110. data/lib/sequel/extensions/_model_pg_row.rb +0 -12
  111. data/lib/sequel/extensions/_pretty_table.rb +1 -1
  112. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  113. data/lib/sequel/extensions/async_thread_pool.rb +14 -13
  114. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  115. data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
  116. data/lib/sequel/extensions/connection_expiration.rb +15 -9
  117. data/lib/sequel/extensions/connection_validator.rb +16 -11
  118. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  119. data/lib/sequel/extensions/core_refinements.rb +36 -11
  120. data/lib/sequel/extensions/date_arithmetic.rb +36 -8
  121. data/lib/sequel/extensions/date_parse_input_handler.rb +67 -0
  122. data/lib/sequel/extensions/datetime_parse_to_time.rb +5 -1
  123. data/lib/sequel/extensions/duplicate_columns_handler.rb +11 -10
  124. data/lib/sequel/extensions/index_caching.rb +5 -1
  125. data/lib/sequel/extensions/inflector.rb +1 -1
  126. data/lib/sequel/extensions/is_distinct_from.rb +141 -0
  127. data/lib/sequel/extensions/looser_typecasting.rb +3 -0
  128. data/lib/sequel/extensions/migration.rb +57 -15
  129. data/lib/sequel/extensions/named_timezones.rb +22 -6
  130. data/lib/sequel/extensions/pagination.rb +1 -1
  131. data/lib/sequel/extensions/pg_array.rb +33 -4
  132. data/lib/sequel/extensions/pg_array_ops.rb +2 -2
  133. data/lib/sequel/extensions/pg_auto_parameterize.rb +509 -0
  134. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  135. data/lib/sequel/extensions/pg_enum.rb +1 -2
  136. data/lib/sequel/extensions/pg_extended_date_support.rb +39 -28
  137. data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
  138. data/lib/sequel/extensions/pg_hstore.rb +6 -1
  139. data/lib/sequel/extensions/pg_hstore_ops.rb +53 -3
  140. data/lib/sequel/extensions/pg_inet.rb +10 -11
  141. data/lib/sequel/extensions/pg_inet_ops.rb +1 -1
  142. data/lib/sequel/extensions/pg_interval.rb +11 -11
  143. data/lib/sequel/extensions/pg_json.rb +13 -15
  144. data/lib/sequel/extensions/pg_json_ops.rb +125 -2
  145. data/lib/sequel/extensions/pg_multirange.rb +367 -0
  146. data/lib/sequel/extensions/pg_range.rb +13 -26
  147. data/lib/sequel/extensions/pg_range_ops.rb +37 -9
  148. data/lib/sequel/extensions/pg_row.rb +20 -19
  149. data/lib/sequel/extensions/pg_row_ops.rb +1 -1
  150. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  151. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  152. data/lib/sequel/extensions/s.rb +2 -1
  153. data/lib/sequel/extensions/schema_caching.rb +1 -1
  154. data/lib/sequel/extensions/schema_dumper.rb +45 -11
  155. data/lib/sequel/extensions/server_block.rb +10 -13
  156. data/lib/sequel/extensions/set_literalizer.rb +58 -0
  157. data/lib/sequel/extensions/sql_comments.rb +110 -3
  158. data/lib/sequel/extensions/sql_log_normalizer.rb +108 -0
  159. data/lib/sequel/extensions/sqlite_json_ops.rb +255 -0
  160. data/lib/sequel/extensions/string_agg.rb +1 -1
  161. data/lib/sequel/extensions/string_date_time.rb +19 -23
  162. data/lib/sequel/extensions/symbol_aref.rb +2 -0
  163. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  164. data/lib/sequel/model/associations.rb +286 -92
  165. data/lib/sequel/model/base.rb +53 -33
  166. data/lib/sequel/model/dataset_module.rb +3 -0
  167. data/lib/sequel/model/errors.rb +10 -1
  168. data/lib/sequel/model/exceptions.rb +15 -3
  169. data/lib/sequel/model/inflections.rb +1 -1
  170. data/lib/sequel/plugins/auto_restrict_eager_graph.rb +62 -0
  171. data/lib/sequel/plugins/auto_validations.rb +74 -16
  172. data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
  173. data/lib/sequel/plugins/column_encryption.rb +29 -8
  174. data/lib/sequel/plugins/composition.rb +3 -2
  175. data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
  176. data/lib/sequel/plugins/constraint_validations.rb +8 -5
  177. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  178. data/lib/sequel/plugins/dirty.rb +1 -1
  179. data/lib/sequel/plugins/enum.rb +124 -0
  180. data/lib/sequel/plugins/finder.rb +4 -2
  181. data/lib/sequel/plugins/insert_conflict.rb +4 -0
  182. data/lib/sequel/plugins/instance_specific_default.rb +1 -1
  183. data/lib/sequel/plugins/json_serializer.rb +2 -2
  184. data/lib/sequel/plugins/lazy_attributes.rb +3 -0
  185. data/lib/sequel/plugins/list.rb +8 -3
  186. data/lib/sequel/plugins/many_through_many.rb +109 -10
  187. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  188. data/lib/sequel/plugins/nested_attributes.rb +4 -4
  189. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  190. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  191. data/lib/sequel/plugins/paged_operations.rb +181 -0
  192. data/lib/sequel/plugins/pg_array_associations.rb +46 -34
  193. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +9 -3
  194. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  195. data/lib/sequel/plugins/prepared_statements.rb +12 -2
  196. data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
  197. data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
  198. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  199. data/lib/sequel/plugins/require_valid_schema.rb +67 -0
  200. data/lib/sequel/plugins/serialization.rb +1 -0
  201. data/lib/sequel/plugins/serialization_modification_detection.rb +1 -0
  202. data/lib/sequel/plugins/single_table_inheritance.rb +8 -0
  203. data/lib/sequel/plugins/sql_comments.rb +189 -0
  204. data/lib/sequel/plugins/static_cache.rb +39 -1
  205. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  206. data/lib/sequel/plugins/subclasses.rb +28 -11
  207. data/lib/sequel/plugins/tactical_eager_loading.rb +23 -10
  208. data/lib/sequel/plugins/timestamps.rb +1 -1
  209. data/lib/sequel/plugins/unused_associations.rb +521 -0
  210. data/lib/sequel/plugins/update_or_create.rb +1 -1
  211. data/lib/sequel/plugins/validate_associated.rb +22 -12
  212. data/lib/sequel/plugins/validation_helpers.rb +41 -11
  213. data/lib/sequel/plugins/validation_helpers_generic_type_messages.rb +73 -0
  214. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  215. data/lib/sequel/sql.rb +1 -1
  216. data/lib/sequel/timezones.rb +12 -14
  217. data/lib/sequel/version.rb +1 -1
  218. metadata +109 -19
@@ -68,6 +68,10 @@ module Sequel
68
68
  private
69
69
 
70
70
  if RUBY_VERSION >= '2.6'
71
+ # Whether Time.at with :nsec and :in is broken. True on JRuby < 9.3.9.0.
72
+ BROKEN_TIME_AT_WITH_NSEC = defined?(JRUBY_VERSION) && (JRUBY_VERSION < '9.3' || (JRUBY_VERSION < '9.4' && JRUBY_VERSION.split('.')[2].to_i < 9))
73
+ private_constant :BROKEN_TIME_AT_WITH_NSEC
74
+
71
75
  # Convert the given input Time (which must be in UTC) to the given input timezone,
72
76
  # which should be a TZInfo::Timezone instance.
73
77
  def convert_input_time_other(v, input_timezone)
@@ -76,33 +80,45 @@ module Sequel
76
80
  raise unless disamb = tzinfo_disambiguator_for(v)
77
81
  period = input_timezone.period_for_local(v, &disamb)
78
82
  offset = period.utc_total_offset
79
- Time.at(v.to_i - offset, :in => input_timezone)
83
+ # :nocov:
84
+ if BROKEN_TIME_AT_WITH_NSEC
85
+ Time.at(v.to_i - offset, :in => input_timezone) + v.nsec/1000000000.0
86
+ # :nocov:
87
+ else
88
+ Time.at(v.to_i - offset, v.nsec, :nsec, :in => input_timezone)
89
+ end
80
90
  end
81
91
 
82
92
  # Convert the given input Time to the given output timezone,
83
93
  # which should be a TZInfo::Timezone instance.
84
94
  def convert_output_time_other(v, output_timezone)
85
- Time.at(v.to_i, :in => output_timezone)
95
+ # :nocov:
96
+ if BROKEN_TIME_AT_WITH_NSEC
97
+ Time.at(v.to_i, :in => output_timezone) + v.nsec/1000000000.0
98
+ # :nocov:
99
+ else
100
+ Time.at(v.to_i, v.nsec, :nsec, :in => output_timezone)
101
+ end
86
102
  end
87
103
  # :nodoc:
88
104
  # :nocov:
89
105
  else
90
106
  def convert_input_time_other(v, input_timezone)
91
107
  local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
92
- Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
108
+ Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0
93
109
  end
94
110
 
95
111
  if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2'
96
112
  def convert_output_time_other(v, output_timezone)
97
113
  v = output_timezone.utc_to_local(v.getutc)
98
114
  local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
99
- Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + local_offset
115
+ Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 + local_offset
100
116
  end
101
117
  else
102
118
  def convert_output_time_other(v, output_timezone)
103
119
  v = output_timezone.utc_to_local(v.getutc)
104
120
  local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
105
- Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
121
+ Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0
106
122
  end
107
123
  end
108
124
  # :nodoc:
@@ -120,7 +136,7 @@ module Sequel
120
136
  v = output_timezone.utc_to_local(v.new_offset(0))
121
137
 
122
138
  # Force DateTime output instead of TZInfo::DateTimeWithOffset
123
- DateTime.jd(v.jd, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
139
+ DateTime.civil(v.year, v.month, v.day, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start)
124
140
  end
125
141
  # :nodoc:
126
142
  # :nocov:
@@ -54,7 +54,7 @@ module Sequel
54
54
  # an enumerator if no block is given.
55
55
  def each_page(page_size)
56
56
  raise(Error, "You cannot paginate a dataset that already has a limit") if @opts[:limit]
57
- return to_enum(:each_page, page_size) unless block_given?
57
+ return to_enum(:each_page, page_size) unless defined?(yield)
58
58
  record_count = count
59
59
  total_pages = (record_count / page_size.to_f).ceil
60
60
  (1..total_pages).each{|page_no| yield paginate(page_no, page_size, record_count)}
@@ -228,16 +228,37 @@ module Sequel
228
228
  when Array
229
229
  "{#{a.map{|i| bound_variable_array(i)}.join(',')}}"
230
230
  when Sequel::SQL::Blob
231
- "\"#{literal(a)[BLOB_RANGE].gsub("''", "'").gsub(/("|\\)/, '\\\\\1')}\""
231
+ bound_variable_array_string(literal(a)[BLOB_RANGE].gsub("''", "'"))
232
232
  when Sequel::LiteralString
233
233
  a
234
234
  when String
235
- "\"#{a.gsub(/("|\\)/, '\\\\\1')}\""
235
+ bound_variable_array_string(a)
236
+ when Float
237
+ if a.infinite?
238
+ a > 0 ? '"Infinity"' : '"-Infinity"'
239
+ elsif a.nan?
240
+ '"NaN"'
241
+ else
242
+ literal(a)
243
+ end
244
+ when Time, Date
245
+ @default_dataset.literal_date_or_time(a)
236
246
  else
237
- literal(a)
247
+ if (s = bound_variable_arg(a, nil)).is_a?(String)
248
+ bound_variable_array_string(s)
249
+ else
250
+ literal(a)
251
+ end
238
252
  end
239
253
  end
240
254
 
255
+ # Escape strings used as array members in bound variables. Most complex
256
+ # will create a regular string with bound_variable_arg, and then use this
257
+ # escaping to format it as an array member.
258
+ def bound_variable_array_string(s)
259
+ "\"#{s.gsub(/("|\\)/, '\\\\\1')}\""
260
+ end
261
+
241
262
  # Look into both the current database's array schema types and the global
242
263
  # array schema types to get the type symbol for the given database type
243
264
  # string.
@@ -246,7 +267,7 @@ module Sequel
246
267
  end
247
268
 
248
269
  # Make the column type detection handle registered array types.
249
- def schema_column_type(db_type)
270
+ def schema_array_type(db_type)
250
271
  if (db_type =~ /\A([^(]+)(?:\([^(]+\))?\[\]\z/io) && (type = pg_array_schema_type($1))
251
272
  type
252
273
  else
@@ -457,6 +478,14 @@ module Sequel
457
478
  end
458
479
  end
459
480
 
481
+ # Allow automatic parameterization of the receiver if all elements can be
482
+ # can be automatically parameterized.
483
+ def sequel_auto_param_type(ds)
484
+ if array_type && all?{|x| nil == x || ds.send(:auto_param_type, x)}
485
+ "::#{array_type}[]"
486
+ end
487
+ end
488
+
460
489
  private
461
490
 
462
491
  # Recursive method that handles multi-dimensional
@@ -108,7 +108,7 @@ module Sequel
108
108
 
109
109
  # Call the ANY function:
110
110
  #
111
- # array_op.all # ANY(array)
111
+ # array_op.any # ANY(array)
112
112
  #
113
113
  # Usually used like:
114
114
  #
@@ -329,7 +329,7 @@ end
329
329
  if defined?(Sequel::CoreRefinements)
330
330
  module Sequel::CoreRefinements
331
331
  refine Symbol do
332
- include Sequel::Postgres::ArrayOpMethods
332
+ send INCLUDE_METH, Sequel::Postgres::ArrayOpMethods
333
333
  end
334
334
  end
335
335
  end
@@ -0,0 +1,509 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # This extension changes Sequel's postgres adapter to automatically
4
+ # parameterize queries by default. Sequel's default behavior has always
5
+ # been to literalize all arguments unless specifically using
6
+ # parameters (via :$arg placeholders and the Dataset#prepare/call methods).
7
+ # This extension makes Sequel use string, numeric, blob, date, and
8
+ # time types as parameters. Example:
9
+ #
10
+ # # Default
11
+ # DB[:test].where(:a=>1)
12
+ # # SQL: SELECT * FROM test WHERE a = 1
13
+ #
14
+ # DB.extension :pg_auto_parameterize
15
+ # DB[:test].where(:a=>1)
16
+ # # SQL: SELECT * FROM test WHERE a = $1 (args: [1])
17
+ #
18
+ # Other pg_* extensions that ship with Sequel and add support for
19
+ # PostgreSQL-specific types support automatically parameterizing those
20
+ # types when used with this extension.
21
+ #
22
+ # This extension is not generally faster than the default behavior.
23
+ # In some cases it is faster, such as when using large strings.
24
+ # However, the use of parameters avoids potential security issues,
25
+ # in case Sequel does not correctly literalize one of the arguments
26
+ # that this extension would automatically parameterize.
27
+ #
28
+ # There are some known issues with automatic parameterization:
29
+ #
30
+ # 1. In order to avoid most type errors, the extension attempts to guess
31
+ # the appropriate type and automatically casts most placeholders,
32
+ # except plain Ruby strings (which PostgreSQL treats as an unknown
33
+ # type).
34
+ #
35
+ # Unfortunately, if the type guess is incorrect, or a plain Ruby
36
+ # string is used and PostgreSQL cannot determine the data type for it,
37
+ # the query may result in a DatabaseError. To fix both issues, you can
38
+ # explicitly cast values using <tt>Sequel.cast(value, type)</tt>, and
39
+ # Sequel will cast to that type.
40
+ #
41
+ # 2. PostgreSQL supports a maximum of 65535 parameters per query.
42
+ # Attempts to use a query with more than this number of parameters
43
+ # will result in a Sequel::DatabaseError being raised. Sequel tries
44
+ # to mitigate this issue by turning <tt>column IN (int, ...)</tt>
45
+ # queries into <tt>column = ANY(CAST($ AS int8[]))</tt> using an
46
+ # array parameter, to reduce the number of parameters. It also limits
47
+ # inserting multiple rows at once to a maximum of 40 rows per query by
48
+ # default. While these mitigations handle the most common cases
49
+ # where a large number of parameters would be used, there are other
50
+ # cases.
51
+ #
52
+ # 3. Automatic parameterization will consider the same objects as
53
+ # equivalent when building SQL. However, for performance, it does
54
+ # not perform equality checks. So code such as:
55
+ #
56
+ # DB[:t].select{foo('a').as(:f)}.group{foo('a')}
57
+ # # SELECT foo('a') AS "f" FROM "t" GROUP BY foo('a')
58
+ #
59
+ # Will get auto paramterized as:
60
+ #
61
+ # # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($2)
62
+ #
63
+ # Which will result in a DatabaseError, since that is not valid SQL.
64
+ #
65
+ # If you use the same expression, it will use the same parameter:
66
+ #
67
+ # foo = Sequel.function(:foo, 'a')
68
+ # DB[:t].select(foo.as(:f)).group(foo)
69
+ # # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($1)
70
+ #
71
+ # Note that Dataset#select_group and similar methods that take arguments
72
+ # used in multiple places in the SQL will generally handle this
73
+ # automatically, since they will use the same objects:
74
+ #
75
+ # DB[:t].select_group{foo('a').as(:f)}
76
+ # # SELECT foo($1) AS "f" FROM "t" GROUP BY foo($1)
77
+ #
78
+ # You can work around any issues that come up by disabling automatic
79
+ # parameterization by calling the +no_auto_parameterize+ method on the
80
+ # dataset (which returns a clone of the dataset). You can avoid
81
+ # parameterization for specific values in the query by wrapping them
82
+ # with +Sequel.skip_pg_auto_param+.
83
+ #
84
+ # It is likely there are corner cases not mentioned above
85
+ # when using this extension. Users are encouraged to provide feedback
86
+ # when using this extension if they come across such corner cases.
87
+ #
88
+ # This extension is only compatible when using the pg driver, not
89
+ # when using the sequel-postgres-pr, jeremyevans-postgres-pr, or
90
+ # postgres-pr drivers, as those do not support bound variables.
91
+ #
92
+ # Related module: Sequel::Postgres::AutoParameterize
93
+
94
+ module Sequel
95
+ module Postgres
96
+ # Enable automatically parameterizing queries.
97
+ module AutoParameterize
98
+ # SQL query string that also holds an array of parameters
99
+ class QueryString < ::String
100
+ # The array of parameters used by this query.
101
+ attr_reader :args
102
+
103
+ # Add a new parameter to this query, which adds
104
+ # the parameter to the array of parameters, and an
105
+ # SQL placeholder to the query itself.
106
+ def add_arg(s)
107
+ unless defined?(@args)
108
+ @args = []
109
+ @arg_map = {}
110
+ @arg_map.compare_by_identity
111
+ end
112
+
113
+ unless pos = @arg_map[s]
114
+ @args << s
115
+ pos = @arg_map[s] = @args.length.to_s
116
+ end
117
+ self << '$' << pos
118
+ end
119
+
120
+ # Return a new QueryString with the given string appended
121
+ # to the receiver, and the same arguments.
122
+ def +(other)
123
+ v = self.class.new(super)
124
+ v.instance_variable_set(:@args, @args) if @args
125
+ v
126
+ end
127
+
128
+ # Whether this query string currently supports
129
+ # automatic parameterization. Automatic parameterization
130
+ # is disabled at certain points during query building where
131
+ # PostgreSQL does not support it.
132
+ def auto_param?
133
+ !@skip_auto_param
134
+ end
135
+
136
+ # Skip automatic parameterization inside the passed block.
137
+ # This is used during query generation to disable
138
+ # automatic parameterization for clauses not supporting it.
139
+ def skip_auto_param
140
+ skip_auto_param = @skip_auto_param
141
+ begin
142
+ @skip_auto_param = true
143
+ yield
144
+ ensure
145
+ @skip_auto_param = skip_auto_param
146
+ end
147
+ end
148
+
149
+ # Freeze the stored arguments when freezing the query string.
150
+ def freeze
151
+ if @args
152
+ @args.freeze
153
+ @arg_map.freeze
154
+ end
155
+ super
156
+ end
157
+
158
+ # Show args when the query string is inspected
159
+ def inspect
160
+ @args ? "#{self}; #{@args.inspect}".inspect : super
161
+ end
162
+
163
+ def initialize_copy(other)
164
+ super
165
+ if args = other.instance_variable_get(:@args)
166
+ @args = args.dup
167
+ @arg_map = other.instance_variable_get(:@arg_map).dup
168
+ end
169
+ end
170
+ end
171
+
172
+ # Wrapper class that skips auto parameterization for the wrapped object.
173
+ class SkipAutoParam < SQL::Wrapper
174
+ def to_s_append(ds, sql)
175
+ if sql.is_a?(QueryString)
176
+ sql.skip_auto_param{super}
177
+ else
178
+ super
179
+ end
180
+ end
181
+ end
182
+
183
+ # PlacholderLiteralizer subclass with support for stored auto parameters.
184
+ class PlaceholderLiteralizer < ::Sequel::Dataset::PlaceholderLiteralizer
185
+ def initialize(dataset, fragments, final_sql, arity)
186
+ s = dataset.sql.dup
187
+ s.clear
188
+ @sql_origin = s.freeze
189
+ super
190
+ end
191
+
192
+ private
193
+
194
+ def sql_origin
195
+ @sql_origin.dup
196
+ end
197
+ end
198
+
199
+ module DatabaseMethods
200
+ def self.extended(db)
201
+ unless (db.adapter_scheme == :postgres && USES_PG) || (db.adapter_scheme == :mock && db.database_type == :postgres)
202
+ raise Error, "pg_auto_parameterize is only supported when using the postgres adapter with the pg driver"
203
+ end
204
+ db.extend_datasets(DatasetMethods)
205
+ end
206
+
207
+ # If the sql string has an embedded parameter array,
208
+ # extract the parameter values from that.
209
+ def execute(sql, opts={})
210
+ if sql.is_a?(QueryString) && (args = sql.args)
211
+ opts = opts.merge(:arguments=>args)
212
+ end
213
+ super
214
+ end
215
+
216
+ private
217
+
218
+ # Disable auto_parameterization during COPY TABLE.
219
+ def copy_table_sql(table, opts=OPTS)
220
+ table = _no_auto_parameterize(table)
221
+ super
222
+ end
223
+
224
+ # Disable auto_parameterization during CREATE TABLE AS.
225
+ def create_table_as(name, sql, options)
226
+ sql = _no_auto_parameterize(sql)
227
+ super
228
+ end
229
+
230
+ # Disable auto_parameterization during CREATE VIEW.
231
+ def create_view_sql(name, source, options)
232
+ source = _no_auto_parameterize(source)
233
+ super
234
+ end
235
+
236
+ # Disable automatic parameterization for the given table if supported.
237
+ def _no_auto_parameterize(table)
238
+ if table.is_a?(DatasetMethods)
239
+ table.no_auto_parameterize
240
+ else
241
+ table
242
+ end
243
+ end
244
+ end
245
+
246
+ module DatasetMethods
247
+ # Return a clone of the dataset that will not do
248
+ # automatic parameterization.
249
+ def no_auto_parameterize
250
+ cached_dataset(:_no_auto_parameterize_ds) do
251
+ @opts[:no_auto_parameterize] ? self : clone(:no_auto_parameterize=>true)
252
+ end
253
+ end
254
+
255
+ # Do not add implicit typecasts for directly typecasted values,
256
+ # since the user is presumably doing so to set the type, not convert
257
+ # from the implicitly typecasted type.
258
+ def cast_sql_append(sql, expr, type)
259
+ if auto_param?(sql) && auto_param_type(expr)
260
+ sql << 'CAST('
261
+ sql.add_arg(expr)
262
+ sql << ' AS ' << db.cast_type_literal(type).to_s << ')'
263
+ else
264
+ super
265
+ end
266
+ end
267
+
268
+ # Transform column IN (int, ...) expressions into column = ANY($)
269
+ # and column NOT IN (int, ...) expressions into column != ALL($)
270
+ # using an integer array bound variable for the ANY/ALL argument.
271
+ # This is the same optimization PostgreSQL performs internally,
272
+ # but this reduces the number of bound variables.
273
+ def complex_expression_sql_append(sql, op, args)
274
+ case op
275
+ when :IN, :"NOT IN"
276
+ l, r = args
277
+ if auto_param?(sql) && !l.is_a?(Array) && _integer_array?(r) && r.size > 1
278
+ if op == :IN
279
+ op = :"="
280
+ func = :ANY
281
+ else
282
+ op = :!=
283
+ func = :ALL
284
+ end
285
+ args = [l, Sequel.function(func, Sequel.cast(_integer_array_auto_param(r), 'int8[]'))]
286
+ end
287
+ end
288
+
289
+ super
290
+ end
291
+
292
+ # Parameterize insertion of multiple values
293
+ def multi_insert_sql(columns, values)
294
+ if @opts[:no_auto_parameterize]
295
+ super
296
+ else
297
+ [clone(:multi_insert_values=>values.map{|r| Array(r)}).insert_sql(columns, LiteralString.new('VALUES '))]
298
+ end
299
+ end
300
+
301
+ # For strings, numeric arguments, and date/time arguments, add
302
+ # them as parameters to the query instead of literalizing them
303
+ # into the SQL.
304
+ def literal_append(sql, v)
305
+ if auto_param?(sql) && (type = auto_param_type(v))
306
+ sql.add_arg(v) << type
307
+ else
308
+ super
309
+ end
310
+ end
311
+
312
+ # The class to use for placeholder literalizers.
313
+ def placeholder_literalizer_class
314
+ if @opts[:no_auto_parameterize]
315
+ super
316
+ else
317
+ PlaceholderLiteralizer
318
+ end
319
+ end
320
+
321
+ # Disable automatic parameterization when using a cursor.
322
+ def use_cursor(*)
323
+ super.no_auto_parameterize
324
+ end
325
+
326
+ # Store receiving dataset and args when with_sql is used with a method name symbol, so sql
327
+ # can be parameterized correctly if used as a subselect.
328
+ def with_sql(*a)
329
+ ds = super
330
+ if Symbol === a[0]
331
+ ds = ds.clone(:with_sql_dataset=>self, :with_sql_args=>a.freeze)
332
+ end
333
+ ds
334
+ end
335
+
336
+ protected
337
+
338
+ # Disable automatic parameterization for prepared statements,
339
+ # since they will use manual parameterization.
340
+ def to_prepared_statement(*a)
341
+ @opts[:no_auto_parameterize] ? super : no_auto_parameterize.to_prepared_statement(*a)
342
+ end
343
+
344
+ private
345
+
346
+ # If auto parameterization is supported for the value, return a string
347
+ # for the implicit typecast to use. Return false/nil if the value should not be
348
+ # automatically parameterized.
349
+ def auto_param_type(v)
350
+ case v
351
+ when String
352
+ case v
353
+ when LiteralString
354
+ false
355
+ when Sequel::SQL::Blob
356
+ "::bytea"
357
+ else
358
+ ""
359
+ end
360
+ when Integer
361
+ ((v > 2147483647 || v < -2147483648) ? "::int8" : "::int4")
362
+ when Float
363
+ # PostgreSQL treats literal floats as numeric, not double precision
364
+ # But older versions of PostgreSQL don't handle Infinity/NaN in numeric
365
+ v.finite? ? "::numeric" : "::double precision"
366
+ when BigDecimal
367
+ "::numeric"
368
+ when Sequel::SQLTime
369
+ "::time"
370
+ when Time
371
+ "::#{@db.cast_type_literal(Time)}"
372
+ when DateTime
373
+ "::#{@db.cast_type_literal(DateTime)}"
374
+ when Date
375
+ "::date"
376
+ else
377
+ v.respond_to?(:sequel_auto_param_type) ? v.sequel_auto_param_type(self) : auto_param_type_fallback(v)
378
+ end
379
+ end
380
+
381
+ # Allow other extensions to support auto parameterization in ways that do not
382
+ # require adding the sequel_auto_param_type method.
383
+ def auto_param_type_fallback(v)
384
+ super if defined?(super)
385
+ end
386
+
387
+ # Whether the given query string currently supports automatic parameterization.
388
+ def auto_param?(sql)
389
+ sql.is_a?(QueryString) && sql.auto_param?
390
+ end
391
+
392
+ # Default the import slice to 40, since PostgreSQL supports a maximum of 1600
393
+ # columns per table, and it supports a maximum of 65k parameters. Technically,
394
+ # there can be more than one parameter per column, so this doesn't prevent going
395
+ # over the limit, though it does make it less likely.
396
+ def default_import_slice
397
+ 40
398
+ end
399
+
400
+ # Handle parameterization of multi_insert_sql
401
+ def _insert_values_sql(sql, values)
402
+ super
403
+
404
+ if values = @opts[:multi_insert_values]
405
+ expression_list_append(sql, values.map{|r| Array(r)})
406
+ end
407
+ end
408
+
409
+ # Whether the given argument is an array of integers or NULL values, recursively.
410
+ def _integer_array?(v)
411
+ Array === v && v.all?{|x| nil == x || Integer === x}
412
+ end
413
+
414
+ # Create the bound variable string that will be used for the IN (int, ...) to = ANY($)
415
+ # optimization for integer arrays.
416
+ def _integer_array_auto_param(v)
417
+ buf = String.new
418
+ buf << '{'
419
+ comma = false
420
+ v.each do |x|
421
+ if comma
422
+ buf << ","
423
+ else
424
+ comma = true
425
+ end
426
+
427
+ buf << (x ? x.to_s : 'NULL')
428
+ end
429
+ buf << '}'
430
+ end
431
+
432
+ # Skip auto parameterization in LIMIT and OFFSET clauses
433
+ def select_limit_sql(sql)
434
+ if auto_param?(sql) && (@opts[:limit] || @opts[:offset])
435
+ sql.skip_auto_param{super}
436
+ else
437
+ super
438
+ end
439
+ end
440
+
441
+ # Skip auto parameterization in ORDER clause if used with
442
+ # integer values indicating ordering by the nth column.
443
+ def select_order_sql(sql)
444
+ if auto_param?(sql) && (order = @opts[:order]) && order.any?{|o| Integer === o || (SQL::OrderedExpression === o && Integer === o.expression)}
445
+ sql.skip_auto_param{super}
446
+ else
447
+ super
448
+ end
449
+ end
450
+
451
+ # Skip auto parameterization in CTE CYCLE clause
452
+ def select_with_sql_cte_search_cycle(sql,cte)
453
+ if auto_param?(sql) && cte[:cycle]
454
+ sql.skip_auto_param{super}
455
+ else
456
+ super
457
+ end
458
+ end
459
+
460
+ # Unless auto parameterization is disabled, use a string that
461
+ # can store the parameterized arguments.
462
+ def sql_string_origin
463
+ @opts[:no_auto_parameterize] ? super : QueryString.new
464
+ end
465
+
466
+ # If subquery uses with_sql with a method name symbol, get the dataset
467
+ # with_sql was called on, and use that as the subquery, recording the
468
+ # arguments to with_sql that will be used to calculate the sql.
469
+ def subselect_sql_dataset(sql, ds)
470
+ if ws_ds = ds.opts[:with_sql_dataset]
471
+ super(sql, ws_ds).clone(:subselect_sql_args=>ds.opts[:with_sql_args])
472
+ else
473
+ super
474
+ end
475
+ end
476
+
477
+ # If subquery used with_sql with a method name symbol, use the arguments to
478
+ # with_sql to determine the sql, so that the subselect can be parameterized.
479
+ def subselect_sql_append_sql(sql, ds)
480
+ if args = ds.opts[:subselect_sql_args]
481
+ ds.send(*args)
482
+ else
483
+ super
484
+ end
485
+ end
486
+
487
+ # Use auto parameterization for datasets with static SQL using placeholders.
488
+ def static_sql(sql)
489
+ if @opts[:append_sql] || @opts[:no_auto_parameterize] || String === sql
490
+ super
491
+ else
492
+ query_string = QueryString.new
493
+ literal_append(query_string, sql)
494
+ query_string
495
+ end
496
+ end
497
+ end
498
+ end
499
+ end
500
+
501
+ module SQL::Builders
502
+ # Skip auto parameterization for the given object when building queries.
503
+ def skip_pg_auto_param(v)
504
+ Postgres::AutoParameterize::SkipAutoParam.new(v)
505
+ end
506
+ end
507
+
508
+ Database.register_extension(:pg_auto_parameterize, Postgres::AutoParameterize::DatabaseMethods)
509
+ end