sequel 5.45.0 → 5.77.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -3,7 +3,8 @@
3
3
  # The pg_json_ops extension adds support to Sequel's DSL to make
4
4
  # it easier to call PostgreSQL JSON functions and operators (added
5
5
  # first in PostgreSQL 9.3). It also supports the JSONB functions
6
- # and operators added in PostgreSQL 9.4.
6
+ # and operators added in PostgreSQL 9.4, as well as additional
7
+ # functions and operators added in later versions.
7
8
  #
8
9
  # To load the extension:
9
10
  #
@@ -101,6 +102,36 @@
101
102
  # substituted in +path+. +silent+ specifies whether errors are suppressed. By default,
102
103
  # errors are not suppressed.
103
104
  #
105
+ # On PostgreSQL 14+, The JSONB <tt>[]</tt> method will use subscripts instead of being
106
+ # the same as +get+, if the value being wrapped is an identifer:
107
+ #
108
+ # Sequel.pg_jsonb_op(:jsonb_column)[1] # jsonb_column[1]
109
+ # Sequel.pg_jsonb_op(:jsonb_column)[1][2] # jsonb_column[1][2]
110
+ # Sequel.pg_jsonb_op(Sequel[:j][:b])[1] # j.b[1]
111
+ #
112
+ # This support allows you to use JSONB subscripts in UPDATE statements to update only
113
+ # part of a column:
114
+ #
115
+ # c = Sequel.pg_jsonb_op(:c)
116
+ # DB[:t].update(c['key1'] => '1', c['key2'] => '"a"')
117
+ # # UPDATE "t" SET "c"['key1'] = '1', "c"['key2'] = '"a"'
118
+ #
119
+ # Note that you have to provide the value of a JSONB subscript as a JSONB value, so this
120
+ # will update +key1+ to use the number <tt>1</tt>, and +key2+ to use the string <tt>a</tt>.
121
+ # For this reason it may be simpler to use +to_json+:
122
+ #
123
+ # c = Sequel.pg_jsonb_op(:c)
124
+ # DB[:t].update(c['key1'] => 1.to_json, c['key2'] => "a".to_json)
125
+ #
126
+ # On PostgreSQL 16+, the <tt>IS [NOT] JSON</tt> operator is supported:
127
+ #
128
+ # j.is_json # j IS JSON
129
+ # j.is_json(type: :object) # j IS JSON OBJECT
130
+ # j.is_json(type: :object, unique: true) # j IS JSON OBJECT WITH UNIQUE
131
+ # j.is_not_json # j IS NOT JSON
132
+ # j.is_not_json(type: :array) # j IS NOT JSON ARRAY
133
+ # j.is_not_json(unique: true) # j IS NOT JSON WITH UNIQUE
134
+ #
104
135
  # If you are also using the pg_json extension, you should load it before
105
136
  # loading this extension. Doing so will allow you to use the #op method on
106
137
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -129,6 +160,18 @@ module Sequel
129
160
  GET_PATH = ["(".freeze, " #> ".freeze, ")".freeze].freeze
130
161
  GET_PATH_TEXT = ["(".freeze, " #>> ".freeze, ")".freeze].freeze
131
162
 
163
+ IS_JSON = ["(".freeze, " IS JSON".freeze, "".freeze, ")".freeze].freeze
164
+ IS_NOT_JSON = ["(".freeze, " IS NOT JSON".freeze, "".freeze, ")".freeze].freeze
165
+ EMPTY_STRING = Sequel::LiteralString.new('').freeze
166
+ WITH_UNIQUE = Sequel::LiteralString.new(' WITH UNIQUE').freeze
167
+ IS_JSON_MAP = {
168
+ nil => EMPTY_STRING,
169
+ :value => Sequel::LiteralString.new(' VALUE').freeze,
170
+ :scalar => Sequel::LiteralString.new(' SCALAR').freeze,
171
+ :object => Sequel::LiteralString.new(' OBJECT').freeze,
172
+ :array => Sequel::LiteralString.new(' ARRAY').freeze
173
+ }.freeze
174
+
132
175
  # Get JSON array element or object field as json. If an array is given,
133
176
  # gets the object at the specified path.
134
177
  #
@@ -211,6 +254,30 @@ module Sequel
211
254
  end
212
255
  end
213
256
 
257
+ # Return whether the json object can be parsed as JSON.
258
+ #
259
+ # Options:
260
+ # :type :: Check whether the json object can be parsed as a specific type
261
+ # of JSON (:value, :scalar, :object, :array).
262
+ # :unique :: Check JSON objects for unique keys.
263
+ #
264
+ # json_op.is_json # json IS JSON
265
+ # json_op.is_json(type: :object) # json IS JSON OBJECT
266
+ # json_op.is_json(unique: true) # json IS JSON WITH UNIQUE
267
+ def is_json(opts=OPTS)
268
+ _is_json(IS_JSON, opts)
269
+ end
270
+
271
+ # Return whether the json object cannot be parsed as JSON. The opposite
272
+ # of #is_json. See #is_json for options.
273
+ #
274
+ # json_op.is_not_json # json IS NOT JSON
275
+ # json_op.is_not_json(type: :object) # json IS NOT JSON OBJECT
276
+ # json_op.is_not_json(unique: true) # json IS NOT JSON WITH UNIQUE
277
+ def is_not_json(opts=OPTS)
278
+ _is_json(IS_NOT_JSON, opts)
279
+ end
280
+
214
281
  # Returns a set of keys AS text in the json object.
215
282
  #
216
283
  # json_op.keys # json_object_keys(json)
@@ -264,6 +331,13 @@ module Sequel
264
331
 
265
332
  private
266
333
 
334
+ # Internals of IS [NOT] JSON support
335
+ def _is_json(lit_array, opts)
336
+ raise Error, "invalid is_json :type option: #{opts[:type].inspect}" unless type = IS_JSON_MAP[opts[:type]]
337
+ unique = opts[:unique] ? WITH_UNIQUE : EMPTY_STRING
338
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(lit_array, [self, type, unique]))
339
+ end
340
+
267
341
  # Return a placeholder literal with the given str and args, wrapped
268
342
  # in an JSONOp or JSONBOp, used by operators that return json or jsonb.
269
343
  def json_op(str, args)
@@ -323,6 +397,24 @@ module Sequel
323
397
  PATH_EXISTS = ["(".freeze, " @? ".freeze, ")".freeze].freeze
324
398
  PATH_MATCH = ["(".freeze, " @@ ".freeze, ")".freeze].freeze
325
399
 
400
+ # Support subscript syntax for JSONB.
401
+ def [](key)
402
+ if is_array?(key)
403
+ super
404
+ else
405
+ case @value
406
+ when Symbol, SQL::Identifier, SQL::QualifiedIdentifier, JSONBSubscriptOp
407
+ # Only use subscripts for identifiers. In other cases, switching from
408
+ # the -> operator to [] for subscripts causes SQL syntax issues. You
409
+ # only need the [] for subscripting when doing assignment, and
410
+ # assignment is generally done on identifiers.
411
+ self.class.new(JSONBSubscriptOp.new(self, key))
412
+ else
413
+ super
414
+ end
415
+ end
416
+ end
417
+
326
418
  # jsonb expression for deletion of the given argument from the
327
419
  # current jsonb.
328
420
  #
@@ -582,6 +674,37 @@ module Sequel
582
674
  end
583
675
  end
584
676
 
677
+ # Represents JSONB subscripts. This is abstracted because the
678
+ # subscript support depends on the database version.
679
+ class JSONBSubscriptOp < SQL::Expression
680
+ SUBSCRIPT = ["".freeze, "[".freeze, "]".freeze].freeze
681
+
682
+ # The expression being subscripted
683
+ attr_reader :expression
684
+
685
+ # The subscript to use
686
+ attr_reader :sub
687
+
688
+ # Set the expression and subscript to the given arguments
689
+ def initialize(expression, sub)
690
+ @expression = expression
691
+ @sub = sub
692
+ freeze
693
+ end
694
+
695
+ # Use subscripts instead of -> operator on PostgreSQL 14+
696
+ def to_s_append(ds, sql)
697
+ server_version = ds.db.server_version
698
+ frag = server_version && server_version >= 140000 ? SUBSCRIPT : JSONOp::GET
699
+ ds.literal_append(sql, Sequel::SQL::PlaceholderLiteralString.new(frag, [@expression, @sub]))
700
+ end
701
+
702
+ # Support transforming of jsonb subscripts
703
+ def sequel_ast_transform(transformer)
704
+ self.class.new(transformer.call(@expression), transformer.call(@sub))
705
+ end
706
+ end
707
+
585
708
  module JSONOpMethods
586
709
  # Wrap the receiver in an JSONOp so you can easily use the PostgreSQL
587
710
  # json functions and operators with it.
@@ -674,7 +797,7 @@ end
674
797
  if defined?(Sequel::CoreRefinements)
675
798
  module Sequel::CoreRefinements
676
799
  refine Symbol do
677
- include Sequel::Postgres::JSONOpMethods
800
+ send INCLUDE_METH, Sequel::Postgres::JSONOpMethods
678
801
  end
679
802
  end
680
803
  end
@@ -0,0 +1,367 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_multirange extension adds support for the PostgreSQL 14+ multirange
4
+ # types to Sequel. PostgreSQL multirange types are similar to an array of
5
+ # ranges, where a match against the multirange is a match against any of the
6
+ # ranges in the multirange.
7
+ #
8
+ # When PostgreSQL multirange values are retrieved, they are parsed and returned
9
+ # as instances of Sequel::Postgres::PGMultiRange. PGMultiRange mostly acts
10
+ # like an array of Sequel::Postgres::PGRange (see the pg_range extension).
11
+ #
12
+ # In addition to the parser, this extension comes with literalizers
13
+ # for PGMultiRanges, so they can be used in queries and as bound variables.
14
+ #
15
+ # To turn an existing array of Ranges into a PGMultiRange, use Sequel.pg_multirange.
16
+ # You must provide the type of multirange when creating the multirange:
17
+ #
18
+ # Sequel.pg_multirange(array_of_date_ranges, :datemultirange)
19
+ #
20
+ # To use this extension, load it into the Database instance:
21
+ #
22
+ # DB.extension :pg_multirange
23
+ #
24
+ # See the {schema modification guide}[rdoc-ref:doc/schema_modification.rdoc]
25
+ # for details on using multirange type columns in CREATE/ALTER TABLE statements.
26
+ #
27
+ # This extension makes it easy to add support for other multirange types. In
28
+ # general, you just need to make sure that the subtype is handled and has the
29
+ # appropriate converter installed. For user defined
30
+ # types, you can do this via:
31
+ #
32
+ # DB.add_conversion_proc(subtype_oid){|string| }
33
+ #
34
+ # Then you can call
35
+ # Sequel::Postgres::PGMultiRange::DatabaseMethods#register_multirange_type
36
+ # to automatically set up a handler for the range type. So if you
37
+ # want to support the timemultirange type (assuming the time type is already
38
+ # supported):
39
+ #
40
+ # DB.register_multirange_type('timerange')
41
+ #
42
+ # This extension integrates with the pg_array extension. If you plan
43
+ # to use arrays of multirange types, load the pg_array extension before the
44
+ # pg_multirange extension:
45
+ #
46
+ # DB.extension :pg_array, :pg_multirange
47
+ #
48
+ # The pg_multirange extension will automatically load the pg_range extension.
49
+ #
50
+ # Related module: Sequel::Postgres::PGMultiRange
51
+
52
+ require 'delegate'
53
+ require 'strscan'
54
+
55
+ module Sequel
56
+ module Postgres
57
+ class PGMultiRange < DelegateClass(Array)
58
+ include Sequel::SQL::AliasMethods
59
+
60
+ # Converts strings into PGMultiRange instances.
61
+ class Parser < StringScanner
62
+ def initialize(source, converter)
63
+ super(source)
64
+ @converter = converter
65
+ end
66
+
67
+ # Parse the multirange type input string into a PGMultiRange value.
68
+ def parse
69
+ raise Sequel::Error, "invalid multirange, doesn't start with {" unless get_byte == '{'
70
+ ranges = []
71
+
72
+ unless scan(/\}/)
73
+ while true
74
+ raise Sequel::Error, "unfinished multirange" unless range_string = scan_until(/[\]\)]/)
75
+ ranges << @converter.call(range_string)
76
+
77
+ case sep = get_byte
78
+ when '}'
79
+ break
80
+ when ','
81
+ # nothing
82
+ else
83
+ raise Sequel::Error, "invalid multirange separator: #{sep.inspect}"
84
+ end
85
+ end
86
+ end
87
+
88
+ raise Sequel::Error, "invalid multirange, remaining data after }" unless eos?
89
+ ranges
90
+ end
91
+ end
92
+
93
+ # Callable object that takes the input string and parses it using Parser.
94
+ class Creator
95
+ # The database type to set on the PGMultiRange instances returned.
96
+ attr_reader :type
97
+
98
+ def initialize(type, converter=nil)
99
+ @type = type
100
+ @converter = converter
101
+ end
102
+
103
+ # Parse the string using Parser with the appropriate
104
+ # converter, and return a PGMultiRange with the appropriate database
105
+ # type.
106
+ def call(string)
107
+ PGMultiRange.new(Parser.new(string, @converter).parse, @type)
108
+ end
109
+ end
110
+
111
+ module DatabaseMethods
112
+ # Add the default multirange conversion procs to the database
113
+ def self.extended(db)
114
+ db.instance_exec do
115
+ raise Error, "multiranges not supported on this database" unless server_version >= 140000
116
+
117
+ extension :pg_range
118
+ @pg_multirange_schema_types ||= {}
119
+
120
+ register_multirange_type('int4multirange', :range_oid=>3904, :oid=>4451)
121
+ register_multirange_type('nummultirange', :range_oid=>3906, :oid=>4532)
122
+ register_multirange_type('tsmultirange', :range_oid=>3908, :oid=>4533)
123
+ register_multirange_type('tstzmultirange', :range_oid=>3910, :oid=>4534)
124
+ register_multirange_type('datemultirange', :range_oid=>3912, :oid=>4535)
125
+ register_multirange_type('int8multirange', :range_oid=>3926, :oid=>4536)
126
+
127
+ if respond_to?(:register_array_type)
128
+ register_array_type('int4multirange', :oid=>6150, :scalar_oid=>4451, :scalar_typecast=>:int4multirange)
129
+ register_array_type('nummultirange', :oid=>6151, :scalar_oid=>4532, :scalar_typecast=>:nummultirange)
130
+ register_array_type('tsmultirange', :oid=>6152, :scalar_oid=>4533, :scalar_typecast=>:tsmultirange)
131
+ register_array_type('tstzmultirange', :oid=>6153, :scalar_oid=>4534, :scalar_typecast=>:tstzmultirange)
132
+ register_array_type('datemultirange', :oid=>6155, :scalar_oid=>4535, :scalar_typecast=>:datemultirange)
133
+ register_array_type('int8multirange', :oid=>6157, :scalar_oid=>4536, :scalar_typecast=>:int8multirange)
134
+ end
135
+
136
+ [:int4multirange, :nummultirange, :tsmultirange, :tstzmultirange, :datemultirange, :int8multirange].each do |v|
137
+ @schema_type_classes[v] = PGMultiRange
138
+ end
139
+
140
+ procs = conversion_procs
141
+ add_conversion_proc(4533, PGMultiRange::Creator.new("tsmultirange", procs[3908]))
142
+ add_conversion_proc(4534, PGMultiRange::Creator.new("tstzmultirange", procs[3910]))
143
+
144
+ if respond_to?(:register_array_type) && defined?(PGArray::Creator)
145
+ add_conversion_proc(6152, PGArray::Creator.new("tsmultirange", procs[4533]))
146
+ add_conversion_proc(6153, PGArray::Creator.new("tstzmultirange", procs[4534]))
147
+ end
148
+ end
149
+ end
150
+
151
+ # Handle PGMultiRange values in bound variables
152
+ def bound_variable_arg(arg, conn)
153
+ case arg
154
+ when PGMultiRange
155
+ arg.unquoted_literal(schema_utility_dataset)
156
+ else
157
+ super
158
+ end
159
+ end
160
+
161
+ # Freeze the pg multirange schema types to prevent adding new ones.
162
+ def freeze
163
+ @pg_multirange_schema_types.freeze
164
+ super
165
+ end
166
+
167
+ # Register a database specific multirange type. This can be used to support
168
+ # different multirange types per Database. Options:
169
+ #
170
+ # :converter :: A callable object (e.g. Proc), that is called with the PostgreSQL range string,
171
+ # and should return a PGRange instance.
172
+ # :oid :: The PostgreSQL OID for the multirange type. This is used by Sequel to set up automatic type
173
+ # conversion on retrieval from the database.
174
+ # :range_oid :: Should be the PostgreSQL OID for the multirange subtype (the range type). If given,
175
+ # automatically sets the :converter option by looking for scalar conversion
176
+ # proc.
177
+ #
178
+ # If a block is given, it is treated as the :converter option.
179
+ def register_multirange_type(db_type, opts=OPTS, &block)
180
+ oid = opts[:oid]
181
+ soid = opts[:range_oid]
182
+
183
+ if has_converter = opts.has_key?(:converter)
184
+ raise Error, "can't provide both a block and :converter option to register_multirange_type" if block
185
+ converter = opts[:converter]
186
+ else
187
+ has_converter = true if block
188
+ converter = block
189
+ end
190
+
191
+ unless (soid || has_converter) && oid
192
+ range_oid, subtype_oid = from(:pg_range).join(:pg_type, :oid=>:rngmultitypid).where(:typname=>db_type.to_s).get([:rngmultitypid, :rngtypid])
193
+ soid ||= subtype_oid unless has_converter
194
+ oid ||= range_oid
195
+ end
196
+
197
+ db_type = db_type.to_s.dup.freeze
198
+
199
+ if soid
200
+ raise Error, "can't provide both a converter and :range_oid option to register" if has_converter
201
+ raise Error, "no conversion proc for :range_oid=>#{soid.inspect} in conversion_procs" unless converter = conversion_procs[soid]
202
+ end
203
+
204
+ raise Error, "cannot add a multirange type without a convertor (use :converter or :range_oid option or pass block)" unless converter
205
+ creator = Creator.new(db_type, converter)
206
+ add_conversion_proc(oid, creator)
207
+
208
+ @pg_multirange_schema_types[db_type] = db_type.to_sym
209
+
210
+ singleton_class.class_eval do
211
+ meth = :"typecast_value_#{db_type}"
212
+ scalar_typecast_method = :"typecast_value_#{opts.fetch(:scalar_typecast, db_type.sub('multirange', 'range'))}"
213
+ define_method(meth){|v| typecast_value_pg_multirange(v, creator, scalar_typecast_method)}
214
+ private meth
215
+ end
216
+
217
+ @schema_type_classes[db_type] = PGMultiRange
218
+ nil
219
+ end
220
+
221
+ private
222
+
223
+ # Recognize the registered database multirange types.
224
+ def schema_multirange_type(db_type)
225
+ @pg_multirange_schema_types[db_type] || super
226
+ end
227
+
228
+ # Set the :ruby_default value if the default value is recognized as a multirange.
229
+ def schema_post_process(_)
230
+ super.each do |a|
231
+ h = a[1]
232
+ db_type = h[:db_type]
233
+ if @pg_multirange_schema_types[db_type] && h[:default] =~ /\A#{db_type}\(.*\)\z/
234
+ h[:ruby_default] = get(Sequel.lit(h[:default]))
235
+ end
236
+ end
237
+ end
238
+
239
+ # Given a value to typecast and the type of PGMultiRange subclass:
240
+ # * If given a PGMultiRange with a matching type, use it directly.
241
+ # * If given a PGMultiRange with a different type, return a PGMultiRange
242
+ # with the creator's type.
243
+ # * If given an Array, create a new PGMultiRange instance for it, typecasting
244
+ # each instance using the scalar_typecast_method.
245
+ def typecast_value_pg_multirange(value, creator, scalar_typecast_method=nil)
246
+ case value
247
+ when PGMultiRange
248
+ return value if value.db_type == creator.type
249
+ when Array
250
+ # nothing
251
+ else
252
+ raise Sequel::InvalidValue, "invalid value for multirange type: #{value.inspect}"
253
+ end
254
+
255
+ if scalar_typecast_method && respond_to?(scalar_typecast_method, true)
256
+ value = value.map{|v| send(scalar_typecast_method, v)}
257
+ end
258
+ PGMultiRange.new(value, creator.type)
259
+ end
260
+ end
261
+
262
+ # The type of this multirange (e.g. 'int4multirange').
263
+ attr_accessor :db_type
264
+
265
+ # Set the array of ranges to delegate to, and the database type.
266
+ def initialize(ranges, db_type)
267
+ super(ranges)
268
+ @db_type = db_type.to_s
269
+ end
270
+
271
+ # Append the multirange SQL to the given sql string.
272
+ def sql_literal_append(ds, sql)
273
+ sql << db_type << '('
274
+ joiner = nil
275
+ conversion_meth = nil
276
+ each do |range|
277
+ if joiner
278
+ sql << joiner
279
+ else
280
+ joiner = ', '
281
+ end
282
+
283
+ unless range.is_a?(PGRange)
284
+ conversion_meth ||= :"typecast_value_#{db_type.sub('multi', '')}"
285
+ range = ds.db.send(conversion_meth, range)
286
+ end
287
+
288
+ ds.literal_append(sql, range)
289
+ end
290
+ sql << ')'
291
+ end
292
+
293
+ # Return whether the value is inside any of the ranges in the multirange.
294
+ def cover?(value)
295
+ any?{|range| range.cover?(value)}
296
+ end
297
+ alias === cover?
298
+
299
+ # Don't consider multiranges with different database types equal.
300
+ def eql?(other)
301
+ if PGMultiRange === other
302
+ return false unless other.db_type == db_type
303
+ other = other.__getobj__
304
+ end
305
+ __getobj__.eql?(other)
306
+ end
307
+
308
+ # Don't consider multiranges with different database types equal.
309
+ def ==(other)
310
+ return false if PGMultiRange === other && other.db_type != db_type
311
+ super
312
+ end
313
+
314
+ # Return a string containing the unescaped version of the multirange.
315
+ # Separated out for use by the bound argument code.
316
+ def unquoted_literal(ds)
317
+ val = String.new
318
+ val << "{"
319
+
320
+ joiner = nil
321
+ conversion_meth = nil
322
+ each do |range|
323
+ if joiner
324
+ val << joiner
325
+ else
326
+ joiner = ', '
327
+ end
328
+
329
+ unless range.is_a?(PGRange)
330
+ conversion_meth ||= :"typecast_value_#{db_type.sub('multi', '')}"
331
+ range = ds.db.send(conversion_meth, range)
332
+ end
333
+
334
+ val << range.unquoted_literal(ds)
335
+ end
336
+
337
+ val << "}"
338
+ end
339
+
340
+ # Allow automatic parameterization.
341
+ def sequel_auto_param_type(ds)
342
+ "::#{db_type}"
343
+ end
344
+ end
345
+ end
346
+
347
+ module SQL::Builders
348
+ # Convert the object to a Postgres::PGMultiRange.
349
+ def pg_multirange(v, db_type)
350
+ case v
351
+ when Postgres::PGMultiRange
352
+ if v.db_type == db_type
353
+ v
354
+ else
355
+ Postgres::PGMultiRange.new(v, db_type)
356
+ end
357
+ when Array
358
+ Postgres::PGMultiRange.new(v, db_type)
359
+ else
360
+ # May not be defined unless the pg_range_ops extension is used
361
+ pg_range_op(v)
362
+ end
363
+ end
364
+ end
365
+
366
+ Database.register_extension(:pg_multirange, Postgres::PGMultiRange::DatabaseMethods)
367
+ end
@@ -4,12 +4,9 @@
4
4
  # types to Sequel. PostgreSQL range types are similar to ruby's
5
5
  # Range class, representating an array of values. However, they
6
6
  # are more flexible than ruby's ranges, allowing exclusive beginnings
7
- # and endings (ruby's range only allows exclusive endings), and
8
- # unbounded beginnings and endings (which ruby's range does not
9
- # support).
7
+ # and endings (ruby's range only allows exclusive endings).
10
8
  #
11
- # This extension integrates with Sequel's native postgres and jdbc/postgresql adapters, so
12
- # that when range type values are retrieved, they are parsed and returned
9
+ # When PostgreSQL range values are retreived, they are parsed and returned
13
10
  # as instances of Sequel::Postgres::PGRange. PGRange mostly acts
14
11
  # like a Range, but it's not a Range as not all PostgreSQL range
15
12
  # type values would be valid ruby ranges. If the range type value
@@ -19,8 +16,7 @@
19
16
  # exception will be raised.
20
17
  #
21
18
  # In addition to the parser, this extension comes with literalizers
22
- # for both PGRange and Range that use the standard Sequel literalization
23
- # callbacks, so they work on all adapters.
19
+ # for PGRange and Range, so they can be used in queries and as bound variables.
24
20
  #
25
21
  # To turn an existing Range into a PGRange, use Sequel.pg_range:
26
22
  #
@@ -237,23 +233,9 @@ module Sequel
237
233
 
238
234
  private
239
235
 
240
- # Handle arrays of range types in bound variables.
241
- def bound_variable_array(a)
242
- case a
243
- when PGRange, Range
244
- "\"#{bound_variable_arg(a, nil)}\""
245
- else
246
- super
247
- end
248
- end
249
-
250
236
  # Recognize the registered database range types.
251
- def schema_column_type(db_type)
252
- if type = @pg_range_schema_types[db_type]
253
- type
254
- else
255
- super
256
- end
237
+ def schema_range_type(db_type)
238
+ @pg_range_schema_types[db_type] || super
257
239
  end
258
240
 
259
241
  # Set the :ruby_default value if the default value is recognized as a range.
@@ -290,7 +272,7 @@ module Sequel
290
272
  when Range
291
273
  PGRange.from_range(value, parser.db_type)
292
274
  when String
293
- parser.call(value)
275
+ parser.call(typecast_check_string_length(value, 100))
294
276
  else
295
277
  raise Sequel::InvalidValue, "invalid value for range type: #{value.inspect}"
296
278
  end
@@ -499,6 +481,11 @@ module Sequel
499
481
  end
500
482
  end
501
483
 
484
+ # Allow automatic parameterization for ranges with types.
485
+ def sequel_auto_param_type(ds)
486
+ "::#{db_type}" if db_type
487
+ end
488
+
502
489
  private
503
490
 
504
491
  # Escape common range types. Instead of quoting, just backslash escape all
@@ -507,8 +494,8 @@ module Sequel
507
494
  case k
508
495
  when nil
509
496
  ''
510
- when Date, Time
511
- ds.literal(k)[1...-1]
497
+ when Time, Date
498
+ ds.literal_date_or_time(k, true)
512
499
  when Integer, Float
513
500
  k.to_s
514
501
  when BigDecimal