sequel 5.33.0 → 5.58.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 (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