sequel 5.33.0 → 5.58.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +318 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +40 -9
  5. data/doc/association_basics.rdoc +77 -13
  6. data/doc/cheat_sheet.rdoc +13 -5
  7. data/doc/code_order.rdoc +0 -12
  8. data/doc/dataset_filtering.rdoc +2 -2
  9. data/doc/fork_safety.rdoc +84 -0
  10. data/doc/migration.rdoc +12 -6
  11. data/doc/model_plugins.rdoc +1 -1
  12. data/doc/opening_databases.rdoc +15 -3
  13. data/doc/postgresql.rdoc +9 -1
  14. data/doc/querying.rdoc +7 -5
  15. data/doc/release_notes/5.34.0.txt +40 -0
  16. data/doc/release_notes/5.35.0.txt +56 -0
  17. data/doc/release_notes/5.36.0.txt +60 -0
  18. data/doc/release_notes/5.37.0.txt +30 -0
  19. data/doc/release_notes/5.38.0.txt +28 -0
  20. data/doc/release_notes/5.39.0.txt +19 -0
  21. data/doc/release_notes/5.40.0.txt +40 -0
  22. data/doc/release_notes/5.41.0.txt +25 -0
  23. data/doc/release_notes/5.42.0.txt +136 -0
  24. data/doc/release_notes/5.43.0.txt +98 -0
  25. data/doc/release_notes/5.44.0.txt +32 -0
  26. data/doc/release_notes/5.45.0.txt +34 -0
  27. data/doc/release_notes/5.46.0.txt +87 -0
  28. data/doc/release_notes/5.47.0.txt +59 -0
  29. data/doc/release_notes/5.48.0.txt +14 -0
  30. data/doc/release_notes/5.49.0.txt +59 -0
  31. data/doc/release_notes/5.50.0.txt +78 -0
  32. data/doc/release_notes/5.51.0.txt +47 -0
  33. data/doc/release_notes/5.52.0.txt +87 -0
  34. data/doc/release_notes/5.53.0.txt +23 -0
  35. data/doc/release_notes/5.54.0.txt +27 -0
  36. data/doc/release_notes/5.55.0.txt +21 -0
  37. data/doc/release_notes/5.56.0.txt +51 -0
  38. data/doc/release_notes/5.57.0.txt +23 -0
  39. data/doc/release_notes/5.58.0.txt +31 -0
  40. data/doc/sql.rdoc +14 -2
  41. data/doc/testing.rdoc +10 -1
  42. data/doc/transactions.rdoc +0 -8
  43. data/doc/validations.rdoc +1 -1
  44. data/doc/virtual_rows.rdoc +1 -1
  45. data/lib/sequel/adapters/ado/access.rb +1 -1
  46. data/lib/sequel/adapters/ado.rb +17 -17
  47. data/lib/sequel/adapters/amalgalite.rb +3 -5
  48. data/lib/sequel/adapters/ibmdb.rb +2 -2
  49. data/lib/sequel/adapters/jdbc/derby.rb +8 -0
  50. data/lib/sequel/adapters/jdbc/h2.rb +60 -10
  51. data/lib/sequel/adapters/jdbc/hsqldb.rb +6 -0
  52. data/lib/sequel/adapters/jdbc/mysql.rb +4 -4
  53. data/lib/sequel/adapters/jdbc/postgresql.rb +4 -4
  54. data/lib/sequel/adapters/jdbc.rb +29 -19
  55. data/lib/sequel/adapters/mysql.rb +80 -67
  56. data/lib/sequel/adapters/mysql2.rb +54 -49
  57. data/lib/sequel/adapters/odbc.rb +8 -6
  58. data/lib/sequel/adapters/oracle.rb +5 -4
  59. data/lib/sequel/adapters/postgres.rb +27 -29
  60. data/lib/sequel/adapters/shared/access.rb +2 -0
  61. data/lib/sequel/adapters/shared/db2.rb +30 -0
  62. data/lib/sequel/adapters/shared/mssql.rb +84 -7
  63. data/lib/sequel/adapters/shared/mysql.rb +33 -2
  64. data/lib/sequel/adapters/shared/oracle.rb +82 -7
  65. data/lib/sequel/adapters/shared/postgres.rb +158 -20
  66. data/lib/sequel/adapters/shared/sqlanywhere.rb +3 -0
  67. data/lib/sequel/adapters/shared/sqlite.rb +102 -10
  68. data/lib/sequel/adapters/sqlanywhere.rb +1 -1
  69. data/lib/sequel/adapters/sqlite.rb +60 -18
  70. data/lib/sequel/adapters/tinytds.rb +2 -1
  71. data/lib/sequel/adapters/utils/columns_limit_1.rb +22 -0
  72. data/lib/sequel/adapters/utils/mysql_mysql2.rb +2 -1
  73. data/lib/sequel/ast_transformer.rb +6 -0
  74. data/lib/sequel/connection_pool/sharded_single.rb +9 -8
  75. data/lib/sequel/connection_pool/sharded_threaded.rb +10 -10
  76. data/lib/sequel/connection_pool/single.rb +7 -9
  77. data/lib/sequel/connection_pool/threaded.rb +1 -1
  78. data/lib/sequel/core.rb +33 -24
  79. data/lib/sequel/database/connecting.rb +3 -4
  80. data/lib/sequel/database/misc.rb +37 -12
  81. data/lib/sequel/database/query.rb +3 -1
  82. data/lib/sequel/database/schema_generator.rb +50 -53
  83. data/lib/sequel/database/schema_methods.rb +45 -23
  84. data/lib/sequel/database/transactions.rb +9 -6
  85. data/lib/sequel/dataset/actions.rb +61 -8
  86. data/lib/sequel/dataset/features.rb +15 -0
  87. data/lib/sequel/dataset/placeholder_literalizer.rb +3 -7
  88. data/lib/sequel/dataset/prepared_statements.rb +2 -0
  89. data/lib/sequel/dataset/query.rb +114 -11
  90. data/lib/sequel/dataset/sql.rb +172 -46
  91. data/lib/sequel/deprecated.rb +3 -1
  92. data/lib/sequel/exceptions.rb +2 -0
  93. data/lib/sequel/extensions/_pretty_table.rb +1 -2
  94. data/lib/sequel/extensions/any_not_empty.rb +1 -1
  95. data/lib/sequel/extensions/async_thread_pool.rb +438 -0
  96. data/lib/sequel/extensions/blank.rb +8 -0
  97. data/lib/sequel/extensions/columns_introspection.rb +1 -2
  98. data/lib/sequel/extensions/core_refinements.rb +38 -11
  99. data/lib/sequel/extensions/date_arithmetic.rb +36 -24
  100. data/lib/sequel/extensions/date_parse_input_handler.rb +67 -0
  101. data/lib/sequel/extensions/datetime_parse_to_time.rb +5 -1
  102. data/lib/sequel/extensions/duplicate_columns_handler.rb +3 -1
  103. data/lib/sequel/extensions/eval_inspect.rb +2 -0
  104. data/lib/sequel/extensions/inflector.rb +9 -1
  105. data/lib/sequel/extensions/is_distinct_from.rb +139 -0
  106. data/lib/sequel/extensions/migration.rb +13 -2
  107. data/lib/sequel/extensions/named_timezones.rb +5 -1
  108. data/lib/sequel/extensions/pagination.rb +1 -1
  109. data/lib/sequel/extensions/pg_array.rb +1 -0
  110. data/lib/sequel/extensions/pg_array_ops.rb +6 -2
  111. data/lib/sequel/extensions/pg_enum.rb +3 -1
  112. data/lib/sequel/extensions/pg_extended_date_support.rb +2 -2
  113. data/lib/sequel/extensions/pg_hstore.rb +1 -1
  114. data/lib/sequel/extensions/pg_hstore_ops.rb +55 -3
  115. data/lib/sequel/extensions/pg_inet.rb +2 -0
  116. data/lib/sequel/extensions/pg_inet_ops.rb +1 -1
  117. data/lib/sequel/extensions/pg_interval.rb +35 -8
  118. data/lib/sequel/extensions/pg_json.rb +3 -5
  119. data/lib/sequel/extensions/pg_json_ops.rb +119 -4
  120. data/lib/sequel/extensions/pg_loose_count.rb +3 -1
  121. data/lib/sequel/extensions/pg_multirange.rb +372 -0
  122. data/lib/sequel/extensions/pg_range.rb +7 -19
  123. data/lib/sequel/extensions/pg_range_ops.rb +39 -9
  124. data/lib/sequel/extensions/pg_row.rb +1 -1
  125. data/lib/sequel/extensions/pg_row_ops.rb +25 -1
  126. data/lib/sequel/extensions/query.rb +3 -0
  127. data/lib/sequel/extensions/run_transaction_hooks.rb +1 -1
  128. data/lib/sequel/extensions/s.rb +4 -1
  129. data/lib/sequel/extensions/schema_dumper.rb +16 -5
  130. data/lib/sequel/extensions/server_block.rb +8 -12
  131. data/lib/sequel/extensions/sql_comments.rb +110 -3
  132. data/lib/sequel/extensions/sql_log_normalizer.rb +108 -0
  133. data/lib/sequel/extensions/sqlite_json_ops.rb +255 -0
  134. data/lib/sequel/extensions/string_agg.rb +1 -1
  135. data/lib/sequel/extensions/string_date_time.rb +19 -23
  136. data/lib/sequel/extensions/symbol_aref_refinement.rb +2 -0
  137. data/lib/sequel/extensions/symbol_as_refinement.rb +2 -0
  138. data/lib/sequel/extensions/to_dot.rb +9 -3
  139. data/lib/sequel/model/associations.rb +342 -114
  140. data/lib/sequel/model/base.rb +45 -24
  141. data/lib/sequel/model/errors.rb +10 -1
  142. data/lib/sequel/model/inflections.rb +1 -1
  143. data/lib/sequel/model/plugins.rb +8 -3
  144. data/lib/sequel/model.rb +3 -1
  145. data/lib/sequel/plugins/association_pks.rb +60 -18
  146. data/lib/sequel/plugins/association_proxies.rb +3 -0
  147. data/lib/sequel/plugins/async_thread_pool.rb +39 -0
  148. data/lib/sequel/plugins/auto_restrict_eager_graph.rb +62 -0
  149. data/lib/sequel/plugins/auto_validations.rb +39 -5
  150. data/lib/sequel/plugins/auto_validations_constraint_validations_presence_message.rb +68 -0
  151. data/lib/sequel/plugins/blacklist_security.rb +1 -2
  152. data/lib/sequel/plugins/class_table_inheritance.rb +3 -8
  153. data/lib/sequel/plugins/column_encryption.rb +728 -0
  154. data/lib/sequel/plugins/composition.rb +8 -2
  155. data/lib/sequel/plugins/concurrent_eager_loading.rb +174 -0
  156. data/lib/sequel/plugins/constraint_validations.rb +2 -1
  157. data/lib/sequel/plugins/csv_serializer.rb +2 -0
  158. data/lib/sequel/plugins/dataset_associations.rb +4 -1
  159. data/lib/sequel/plugins/dirty.rb +44 -0
  160. data/lib/sequel/plugins/enum.rb +124 -0
  161. data/lib/sequel/plugins/forbid_lazy_load.rb +2 -0
  162. data/lib/sequel/plugins/insert_conflict.rb +4 -0
  163. data/lib/sequel/plugins/instance_specific_default.rb +113 -0
  164. data/lib/sequel/plugins/json_serializer.rb +39 -24
  165. data/lib/sequel/plugins/lazy_attributes.rb +4 -1
  166. data/lib/sequel/plugins/many_through_many.rb +108 -9
  167. data/lib/sequel/plugins/nested_attributes.rb +8 -3
  168. data/lib/sequel/plugins/pg_array_associations.rb +58 -41
  169. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +2 -0
  170. data/lib/sequel/plugins/prepared_statements.rb +15 -12
  171. data/lib/sequel/plugins/prepared_statements_safe.rb +1 -3
  172. data/lib/sequel/plugins/rcte_tree.rb +37 -35
  173. data/lib/sequel/plugins/serialization.rb +9 -3
  174. data/lib/sequel/plugins/serialization_modification_detection.rb +2 -1
  175. data/lib/sequel/plugins/single_table_inheritance.rb +7 -0
  176. data/lib/sequel/plugins/sql_comments.rb +189 -0
  177. data/lib/sequel/plugins/static_cache.rb +1 -1
  178. data/lib/sequel/plugins/string_stripper.rb +1 -1
  179. data/lib/sequel/plugins/subclasses.rb +28 -11
  180. data/lib/sequel/plugins/tactical_eager_loading.rb +8 -2
  181. data/lib/sequel/plugins/timestamps.rb +1 -1
  182. data/lib/sequel/plugins/tree.rb +9 -4
  183. data/lib/sequel/plugins/unused_associations.rb +521 -0
  184. data/lib/sequel/plugins/update_or_create.rb +1 -1
  185. data/lib/sequel/plugins/validation_class_methods.rb +5 -1
  186. data/lib/sequel/plugins/validation_helpers.rb +18 -11
  187. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  188. data/lib/sequel/sql.rb +1 -1
  189. data/lib/sequel/timezones.rb +20 -17
  190. data/lib/sequel/version.rb +1 -1
  191. metadata +93 -39
@@ -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
  #
@@ -73,7 +74,10 @@
73
74
  # j.pretty # jsonb_pretty(jsonb_column)
74
75
  # j.set(%w'0 a', :h) # jsonb_set(jsonb_column, ARRAY['0','a'], h, true)
75
76
  #
76
- # On PostgreSQL 12+ SQL/JSON functions and operators are supported:
77
+ # j.set_lax(%w'0 a', :h, false, 'raise_exception')
78
+ # # jsonb_set_lax(jsonb_column, ARRAY['0','a'], h, false, 'raise_exception')
79
+ #
80
+ # On PostgreSQL 12+ SQL/JSON path functions and operators are supported:
77
81
  #
78
82
  # j.path_exists('$.foo') # (jsonb_column @? '$.foo')
79
83
  # j.path_match('$.foo') # (jsonb_column @@ '$.foo')
@@ -84,12 +88,41 @@
84
88
  # j.path_query_array('$.foo') # jsonb_path_query_array(jsonb_column, '$.foo')
85
89
  # j.path_query_first('$.foo') # jsonb_path_query_first(jsonb_column, '$.foo')
86
90
  #
87
- # For the PostgreSQL 12+ SQL/JSON functions, one argument is required (+path+) and
91
+ # On PostgreSQL 13+ timezone-aware SQL/JSON path functions and operators are supported:
92
+ #
93
+ # j.path_exists_tz!('$.foo') # jsonb_path_exists_tz(jsonb_column, '$.foo')
94
+ # j.path_match_tz!('$.foo') # jsonb_path_match_tz(jsonb_column, '$.foo')
95
+ # j.path_query_tz('$.foo') # jsonb_path_query_tz(jsonb_column, '$.foo')
96
+ # j.path_query_array_tz('$.foo') # jsonb_path_query_array_tz(jsonb_column, '$.foo')
97
+ # j.path_query_first_tz('$.foo') # jsonb_path_query_first_tz(jsonb_column, '$.foo')
98
+ #
99
+ # For the PostgreSQL 12+ SQL/JSON path functions, one argument is required (+path+) and
88
100
  # two more arguments are optional (+vars+ and +silent+). +path+ specifies the JSON path.
89
101
  # +vars+ specifies a hash or a string in JSON format of named variables to be
90
102
  # substituted in +path+. +silent+ specifies whether errors are suppressed. By default,
91
103
  # errors are not suppressed.
92
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
+ #
93
126
  # If you are also using the pg_json extension, you should load it before
94
127
  # loading this extension. Doing so will allow you to use the #op method on
95
128
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -312,6 +345,24 @@ module Sequel
312
345
  PATH_EXISTS = ["(".freeze, " @? ".freeze, ")".freeze].freeze
313
346
  PATH_MATCH = ["(".freeze, " @@ ".freeze, ")".freeze].freeze
314
347
 
348
+ # Support subscript syntax for JSONB.
349
+ def [](key)
350
+ if is_array?(key)
351
+ super
352
+ else
353
+ case @value
354
+ when Symbol, SQL::Identifier, SQL::QualifiedIdentifier, JSONBSubscriptOp
355
+ # Only use subscripts for identifiers. In other cases, switching from
356
+ # the -> operator to [] for subscripts causes SQL syntax issues. You
357
+ # only need the [] for subscripting when doing assignment, and
358
+ # assignment is generally done on identifiers.
359
+ self.class.new(JSONBSubscriptOp.new(self, key))
360
+ else
361
+ super
362
+ end
363
+ end
364
+ end
365
+
315
366
  # jsonb expression for deletion of the given argument from the
316
367
  # current jsonb.
317
368
  #
@@ -402,6 +453,11 @@ module Sequel
402
453
  Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_exists, path, vars, silent))
403
454
  end
404
455
 
456
+ # The same as #path_exists!, except that timezone-aware conversions are used for date/time values.
457
+ def path_exists_tz!(path, vars=nil, silent=nil)
458
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_exists_tz, path, vars, silent))
459
+ end
460
+
405
461
  # Returns the first item of the result of JSON path predicate check for the json object.
406
462
  # Returns nil if the first item is not true or false.
407
463
  #
@@ -425,6 +481,11 @@ module Sequel
425
481
  Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_match, path, vars, silent))
426
482
  end
427
483
 
484
+ # The same as #path_match!, except that timezone-aware conversions are used for date/time values.
485
+ def path_match_tz!(path, vars=nil, silent=nil)
486
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_match_tz, path, vars, silent))
487
+ end
488
+
428
489
  # Returns a set of all jsonb values specified by the JSON path
429
490
  # for the json object.
430
491
  #
@@ -440,6 +501,11 @@ module Sequel
440
501
  _path_function(:jsonb_path_query, path, vars, silent)
441
502
  end
442
503
 
504
+ # The same as #path_query, except that timezone-aware conversions are used for date/time values.
505
+ def path_query_tz(path, vars=nil, silent=nil)
506
+ _path_function(:jsonb_path_query_tz, path, vars, silent)
507
+ end
508
+
443
509
  # Returns a jsonb array of all values specified by the JSON path
444
510
  # for the json object.
445
511
  #
@@ -455,6 +521,11 @@ module Sequel
455
521
  JSONBOp.new(_path_function(:jsonb_path_query_array, path, vars, silent))
456
522
  end
457
523
 
524
+ # The same as #path_query_array, except that timezone-aware conversions are used for date/time values.
525
+ def path_query_array_tz(path, vars=nil, silent=nil)
526
+ JSONBOp.new(_path_function(:jsonb_path_query_array_tz, path, vars, silent))
527
+ end
528
+
458
529
  # Returns the first item of the result specified by the JSON path
459
530
  # for the json object.
460
531
  #
@@ -470,6 +541,11 @@ module Sequel
470
541
  JSONBOp.new(_path_function(:jsonb_path_query_first, path, vars, silent))
471
542
  end
472
543
 
544
+ # The same as #path_query_first, except that timezone-aware conversions are used for date/time values.
545
+ def path_query_first_tz(path, vars=nil, silent=nil)
546
+ JSONBOp.new(_path_function(:jsonb_path_query_first_tz, path, vars, silent))
547
+ end
548
+
473
549
  # Return the receiver, since it is already a JSONBOp.
474
550
  def pg_jsonb
475
551
  self
@@ -492,6 +568,12 @@ module Sequel
492
568
  self.class.new(function(:set, wrap_input_array(path), wrap_input_jsonb(other), create_missing))
493
569
  end
494
570
 
571
+ # The same as #set, except if +other+ is +nil+, then behaves according to +null_value_treatment+,
572
+ # which can be one of 'raise_exception', 'use_json_null' (default), 'delete_key', or 'return_target'.
573
+ def set_lax(path, other, create_missing=true, null_value_treatment='use_json_null')
574
+ self.class.new(function(:set_lax, wrap_input_array(path), wrap_input_jsonb(other), create_missing, null_value_treatment))
575
+ end
576
+
495
577
  private
496
578
 
497
579
  # Internals of the jsonb SQL/JSON path functions.
@@ -540,6 +622,37 @@ module Sequel
540
622
  end
541
623
  end
542
624
 
625
+ # Represents JSONB subscripts. This is abstracted because the
626
+ # subscript support depends on the database version.
627
+ class JSONBSubscriptOp < SQL::Expression
628
+ SUBSCRIPT = ["".freeze, "[".freeze, "]".freeze].freeze
629
+
630
+ # The expression being subscripted
631
+ attr_reader :expression
632
+
633
+ # The subscript to use
634
+ attr_reader :sub
635
+
636
+ # Set the expression and subscript to the given arguments
637
+ def initialize(expression, sub)
638
+ @expression = expression
639
+ @sub = sub
640
+ freeze
641
+ end
642
+
643
+ # Use subscripts instead of -> operator on PostgreSQL 14+
644
+ def to_s_append(ds, sql)
645
+ server_version = ds.db.server_version
646
+ frag = server_version && server_version >= 140000 ? SUBSCRIPT : JSONOp::GET
647
+ ds.literal_append(sql, Sequel::SQL::PlaceholderLiteralString.new(frag, [@expression, @sub]))
648
+ end
649
+
650
+ # Support transforming of jsonb subscripts
651
+ def sequel_ast_transform(transformer)
652
+ self.class.new(transformer.call(@expression), transformer.call(@sub))
653
+ end
654
+ end
655
+
543
656
  module JSONOpMethods
544
657
  # Wrap the receiver in an JSONOp so you can easily use the PostgreSQL
545
658
  # json functions and operators with it.
@@ -554,7 +667,9 @@ module Sequel
554
667
  end
555
668
  end
556
669
 
670
+ # :nocov:
557
671
  if defined?(JSONArray)
672
+ # :nocov:
558
673
  class JSONArray
559
674
  # Wrap the JSONArray instance in an JSONOp, allowing you to easily use
560
675
  # the PostgreSQL json functions and operators with literal jsons.
@@ -630,7 +745,7 @@ end
630
745
  if defined?(Sequel::CoreRefinements)
631
746
  module Sequel::CoreRefinements
632
747
  refine Symbol do
633
- include Sequel::Postgres::JSONOpMethods
748
+ send INCLUDE_METH, Sequel::Postgres::JSONOpMethods
634
749
  end
635
750
  end
636
751
  end
@@ -12,7 +12,9 @@
12
12
  #
13
13
  # How accurate this count is depends on the number of rows
14
14
  # added/deleted from the table since the last time it was
15
- # analyzed.
15
+ # analyzed. If the table has not been vacuumed or analyzed
16
+ # yet, this can return 0 or -1 depending on the PostgreSQL
17
+ # version in use.
16
18
  #
17
19
  # To load the extension into the database:
18
20
  #
@@ -0,0 +1,372 @@
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
+ # Handle arrays of multirange types in bound variables.
224
+ def bound_variable_array(a)
225
+ case a
226
+ when PGMultiRange
227
+ "\"#{bound_variable_arg(a, nil)}\""
228
+ else
229
+ super
230
+ end
231
+ end
232
+
233
+ # Recognize the registered database multirange types.
234
+ def schema_column_type(db_type)
235
+ @pg_multirange_schema_types[db_type] || super
236
+ end
237
+
238
+ # Set the :ruby_default value if the default value is recognized as a multirange.
239
+ def schema_post_process(_)
240
+ super.each do |a|
241
+ h = a[1]
242
+ db_type = h[:db_type]
243
+ if @pg_multirange_schema_types[db_type] && h[:default] =~ /\A#{db_type}\(.*\)\z/
244
+ h[:ruby_default] = get(Sequel.lit(h[:default]))
245
+ end
246
+ end
247
+ end
248
+
249
+ # Given a value to typecast and the type of PGMultiRange subclass:
250
+ # * If given a PGMultiRange with a matching type, use it directly.
251
+ # * If given a PGMultiRange with a different type, return a PGMultiRange
252
+ # with the creator's type.
253
+ # * If given an Array, create a new PGMultiRange instance for it, typecasting
254
+ # each instance using the scalar_typecast_method.
255
+ def typecast_value_pg_multirange(value, creator, scalar_typecast_method=nil)
256
+ case value
257
+ when PGMultiRange
258
+ return value if value.db_type == creator.type
259
+ when Array
260
+ # nothing
261
+ else
262
+ raise Sequel::InvalidValue, "invalid value for multirange type: #{value.inspect}"
263
+ end
264
+
265
+ if scalar_typecast_method && respond_to?(scalar_typecast_method, true)
266
+ value = value.map{|v| send(scalar_typecast_method, v)}
267
+ end
268
+ PGMultiRange.new(value, creator.type)
269
+ end
270
+ end
271
+
272
+ # The type of this multirange (e.g. 'int4multirange').
273
+ attr_accessor :db_type
274
+
275
+ # Set the array of ranges to delegate to, and the database type.
276
+ def initialize(ranges, db_type)
277
+ super(ranges)
278
+ @db_type = db_type.to_s
279
+ end
280
+
281
+ # Append the multirange SQL to the given sql string.
282
+ def sql_literal_append(ds, sql)
283
+ sql << db_type << '('
284
+ joiner = nil
285
+ conversion_meth = nil
286
+ each do |range|
287
+ if joiner
288
+ sql << joiner
289
+ else
290
+ joiner = ', '
291
+ end
292
+
293
+ unless range.is_a?(PGRange)
294
+ conversion_meth ||= :"typecast_value_#{db_type.sub('multi', '')}"
295
+ range = ds.db.send(conversion_meth, range)
296
+ end
297
+
298
+ ds.literal_append(sql, range)
299
+ end
300
+ sql << ')'
301
+ end
302
+
303
+ # Return whether the value is inside any of the ranges in the multirange.
304
+ def cover?(value)
305
+ any?{|range| range.cover?(value)}
306
+ end
307
+ alias === cover?
308
+
309
+ # Don't consider multiranges with different database types equal.
310
+ def eql?(other)
311
+ if PGMultiRange === other
312
+ return false unless other.db_type == db_type
313
+ other = other.__getobj__
314
+ end
315
+ __getobj__.eql?(other)
316
+ end
317
+
318
+ # Don't consider multiranges with different database types equal.
319
+ def ==(other)
320
+ return false if PGMultiRange === other && other.db_type != db_type
321
+ super
322
+ end
323
+
324
+ # Return a string containing the unescaped version of the multirange.
325
+ # Separated out for use by the bound argument code.
326
+ def unquoted_literal(ds)
327
+ val = String.new
328
+ val << "{"
329
+
330
+ joiner = nil
331
+ conversion_meth = nil
332
+ each do |range|
333
+ if joiner
334
+ val << joiner
335
+ else
336
+ joiner = ', '
337
+ end
338
+
339
+ unless range.is_a?(PGRange)
340
+ conversion_meth ||= :"typecast_value_#{db_type.sub('multi', '')}"
341
+ range = ds.db.send(conversion_meth, range)
342
+ end
343
+
344
+ val << range.unquoted_literal(ds)
345
+ end
346
+
347
+ val << "}"
348
+ end
349
+ end
350
+ end
351
+
352
+ module SQL::Builders
353
+ # Convert the object to a Postgres::PGMultiRange.
354
+ def pg_multirange(v, db_type)
355
+ case v
356
+ when Postgres::PGMultiRange
357
+ if v.db_type == db_type
358
+ v
359
+ else
360
+ Postgres::PGMultiRange.new(v, db_type)
361
+ end
362
+ when Array
363
+ Postgres::PGMultiRange.new(v, db_type)
364
+ else
365
+ # May not be defined unless the pg_range_ops extension is used
366
+ pg_range_op(v)
367
+ end
368
+ end
369
+ end
370
+
371
+ Database.register_extension(:pg_multirange, Postgres::PGMultiRange::DatabaseMethods)
372
+ 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
  #
@@ -158,7 +154,7 @@ module Sequel
158
154
  procs = conversion_procs
159
155
  add_conversion_proc(3908, Parser.new("tsrange", procs[1114]))
160
156
  add_conversion_proc(3910, Parser.new("tstzrange", procs[1184]))
161
- if defined?(PGArray::Creator)
157
+ if respond_to?(:register_array_type) && defined?(PGArray::Creator)
162
158
  add_conversion_proc(3909, PGArray::Creator.new("tsrange", procs[3908]))
163
159
  add_conversion_proc(3911, PGArray::Creator.new("tstzrange", procs[3910]))
164
160
  end
@@ -215,12 +211,6 @@ module Sequel
215
211
 
216
212
  db_type = db_type.to_s.dup.freeze
217
213
 
218
- if converter = opts[:converter]
219
- raise Error, "can't provide both a block and :converter option to register" if block
220
- else
221
- converter = block
222
- end
223
-
224
214
  if soid
225
215
  raise Error, "can't provide both a converter and :subtype_oid option to register" if has_converter
226
216
  raise Error, "no conversion proc for :subtype_oid=>#{soid.inspect} in conversion_procs" unless converter = conversion_procs[soid]
@@ -255,11 +245,7 @@ module Sequel
255
245
 
256
246
  # Recognize the registered database range types.
257
247
  def schema_column_type(db_type)
258
- if type = @pg_range_schema_types[db_type]
259
- type
260
- else
261
- super
262
- end
248
+ @pg_range_schema_types[db_type] || super
263
249
  end
264
250
 
265
251
  # Set the :ruby_default value if the default value is recognized as a range.
@@ -471,8 +457,10 @@ module Sequel
471
457
  return @range if @range
472
458
  raise(Error, "cannot create ruby range for an empty PostgreSQL range") if empty?
473
459
  raise(Error, "cannot create ruby range when PostgreSQL range excludes beginning element") if exclude_begin?
460
+ # :nocov:
474
461
  raise(Error, "cannot create ruby range when PostgreSQL range has unbounded beginning") if STARTLESS_RANGE_NOT_SUPPORTED && !self.begin
475
462
  raise(Error, "cannot create ruby range when PostgreSQL range has unbounded ending") if ENDLESS_RANGE_NOT_SUPPORTED && !self.end
463
+ # :nocov:
476
464
  @range = Range.new(self.begin, self.end, exclude_end?)
477
465
  end
478
466