sequel 5.19.0 → 5.24.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +102 -0
  3. data/doc/dataset_filtering.rdoc +15 -0
  4. data/doc/opening_databases.rdoc +5 -1
  5. data/doc/release_notes/5.20.0.txt +89 -0
  6. data/doc/release_notes/5.21.0.txt +87 -0
  7. data/doc/release_notes/5.22.0.txt +48 -0
  8. data/doc/release_notes/5.23.0.txt +56 -0
  9. data/doc/release_notes/5.24.0.txt +56 -0
  10. data/doc/sharding.rdoc +2 -0
  11. data/doc/testing.rdoc +1 -0
  12. data/doc/transactions.rdoc +38 -0
  13. data/lib/sequel/adapters/ado.rb +27 -19
  14. data/lib/sequel/adapters/jdbc.rb +7 -1
  15. data/lib/sequel/adapters/jdbc/mysql.rb +2 -2
  16. data/lib/sequel/adapters/jdbc/postgresql.rb +1 -13
  17. data/lib/sequel/adapters/jdbc/sqlite.rb +29 -0
  18. data/lib/sequel/adapters/mysql2.rb +2 -3
  19. data/lib/sequel/adapters/shared/mssql.rb +7 -7
  20. data/lib/sequel/adapters/shared/postgres.rb +37 -19
  21. data/lib/sequel/adapters/shared/sqlite.rb +27 -3
  22. data/lib/sequel/adapters/sqlite.rb +1 -1
  23. data/lib/sequel/adapters/tinytds.rb +12 -0
  24. data/lib/sequel/adapters/utils/mysql_mysql2.rb +2 -0
  25. data/lib/sequel/database/logging.rb +7 -1
  26. data/lib/sequel/database/query.rb +1 -1
  27. data/lib/sequel/database/schema_generator.rb +12 -3
  28. data/lib/sequel/database/schema_methods.rb +2 -0
  29. data/lib/sequel/database/transactions.rb +57 -5
  30. data/lib/sequel/dataset.rb +4 -2
  31. data/lib/sequel/dataset/actions.rb +3 -2
  32. data/lib/sequel/dataset/placeholder_literalizer.rb +4 -1
  33. data/lib/sequel/dataset/query.rb +5 -1
  34. data/lib/sequel/dataset/sql.rb +11 -7
  35. data/lib/sequel/extensions/named_timezones.rb +52 -8
  36. data/lib/sequel/extensions/pg_array.rb +4 -0
  37. data/lib/sequel/extensions/pg_json.rb +387 -123
  38. data/lib/sequel/extensions/pg_range.rb +3 -2
  39. data/lib/sequel/extensions/pg_row.rb +3 -1
  40. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  41. data/lib/sequel/extensions/server_block.rb +15 -4
  42. data/lib/sequel/model/associations.rb +35 -9
  43. data/lib/sequel/model/plugins.rb +104 -0
  44. data/lib/sequel/plugins/association_dependencies.rb +3 -3
  45. data/lib/sequel/plugins/association_pks.rb +14 -4
  46. data/lib/sequel/plugins/association_proxies.rb +3 -2
  47. data/lib/sequel/plugins/class_table_inheritance.rb +11 -0
  48. data/lib/sequel/plugins/composition.rb +13 -9
  49. data/lib/sequel/plugins/finder.rb +2 -2
  50. data/lib/sequel/plugins/hook_class_methods.rb +17 -5
  51. data/lib/sequel/plugins/insert_conflict.rb +72 -0
  52. data/lib/sequel/plugins/inverted_subsets.rb +2 -2
  53. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +147 -59
  54. data/lib/sequel/plugins/rcte_tree.rb +6 -0
  55. data/lib/sequel/plugins/static_cache.rb +8 -3
  56. data/lib/sequel/plugins/static_cache_cache.rb +53 -0
  57. data/lib/sequel/plugins/subset_conditions.rb +2 -2
  58. data/lib/sequel/plugins/validation_class_methods.rb +5 -3
  59. data/lib/sequel/sql.rb +15 -3
  60. data/lib/sequel/timezones.rb +50 -11
  61. data/lib/sequel/version.rb +1 -1
  62. data/spec/adapters/mssql_spec.rb +24 -0
  63. data/spec/adapters/mysql_spec.rb +0 -5
  64. data/spec/adapters/postgres_spec.rb +319 -1
  65. data/spec/bin_spec.rb +1 -1
  66. data/spec/core/database_spec.rb +123 -2
  67. data/spec/core/dataset_spec.rb +33 -1
  68. data/spec/core/expression_filters_spec.rb +25 -1
  69. data/spec/core/schema_spec.rb +24 -0
  70. data/spec/extensions/class_table_inheritance_spec.rb +30 -8
  71. data/spec/extensions/core_refinements_spec.rb +1 -1
  72. data/spec/extensions/hook_class_methods_spec.rb +22 -0
  73. data/spec/extensions/insert_conflict_spec.rb +103 -0
  74. data/spec/extensions/migration_spec.rb +13 -0
  75. data/spec/extensions/named_timezones_spec.rb +109 -2
  76. data/spec/extensions/pg_auto_constraint_validations_spec.rb +45 -0
  77. data/spec/extensions/pg_json_spec.rb +218 -29
  78. data/spec/extensions/pg_range_spec.rb +76 -9
  79. data/spec/extensions/rcte_tree_spec.rb +6 -0
  80. data/spec/extensions/s_spec.rb +1 -1
  81. data/spec/extensions/schema_dumper_spec.rb +4 -2
  82. data/spec/extensions/server_block_spec.rb +38 -0
  83. data/spec/extensions/spec_helper.rb +8 -1
  84. data/spec/extensions/static_cache_cache_spec.rb +35 -0
  85. data/spec/integration/dataset_test.rb +25 -9
  86. data/spec/integration/plugin_test.rb +42 -0
  87. data/spec/integration/schema_test.rb +7 -2
  88. data/spec/integration/transaction_test.rb +50 -0
  89. data/spec/model/associations_spec.rb +84 -4
  90. data/spec/model/plugins_spec.rb +111 -0
  91. metadata +16 -2
@@ -0,0 +1,53 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The static_cache_cache plugin allows for caching the row content for subclasses
6
+ # that use the static cache plugin (or just the current class). Using this plugin
7
+ # can avoid the need to query the database every time loading the plugin into a
8
+ # model, which can save time when you have a lot of models using the static_cache
9
+ # plugin.
10
+ #
11
+ # Usage:
12
+ #
13
+ # # Make all model subclasses that use the static_cache plugin use
14
+ # # the cached values in the given file
15
+ # Sequel::Model.plugin :static_cache_cache, "static_cache.cache"
16
+ #
17
+ # # Make the AlbumType model the cached values in the given file,
18
+ # # should be loaded before the static_cache plugin
19
+ # AlbumType.plugin :static_cache_cache, "static_cache.cache"
20
+ module StaticCacheCache
21
+ def self.configure(model, file)
22
+ model.instance_variable_set(:@static_cache_cache_file, file)
23
+ model.instance_variable_set(:@static_cache_cache, File.exist?(file) ? Marshal.load(File.read(file)) : {})
24
+ end
25
+
26
+ module ClassMethods
27
+ # Dump the in-memory cached rows to the cache file.
28
+ def dump_static_cache_cache
29
+ File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(@static_cache_cache))}
30
+ nil
31
+ end
32
+
33
+ Plugins.inherited_instance_variables(self, :@static_cache_cache_file=>nil, :@static_cache_cache=>nil)
34
+
35
+ private
36
+
37
+ # Load the rows for the model from the cache if available.
38
+ # If not available, load the rows from the database, and
39
+ # then update the cache with the raw rows.
40
+ def load_static_cache_rows
41
+ if rows = Sequel.synchronize{@static_cache_cache[name]}
42
+ rows.map{|row| call(row)}.freeze
43
+ else
44
+ rows = dataset.all.freeze
45
+ raw_rows = rows.map(&:values)
46
+ Sequel.synchronize{@static_cache_cache[name] = raw_rows}
47
+ rows
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -26,8 +26,8 @@ module Sequel
26
26
  # Album.where(Album.published_conditions | {ready: true}).sql
27
27
  # # SELECT * FROM albums WHERE ((published IS TRUE) OR (ready IS TRUE))
28
28
  module SubsetConditions
29
- def self.apply(mod, &block)
30
- mod.instance_exec do
29
+ def self.apply(model, &block)
30
+ model.instance_exec do
31
31
  @dataset_module_class = Class.new(@dataset_module_class) do
32
32
  include DatasetModuleMethods
33
33
  end
@@ -188,13 +188,17 @@ module Sequel
188
188
  # Sequel will attempt to insert a NULL value into the database, instead of using the
189
189
  # database's default.
190
190
  # :allow_nil :: Whether to skip the validation if the value is nil.
191
- # :if :: A symbol (indicating an instance_method) or proc (which is instance_execed)
191
+ # :if :: A symbol (indicating an instance_method) or proc (which is used to define an instance method)
192
192
  # skipping this validation if it returns nil or false.
193
193
  # :tag :: The tag to use for this validation.
194
194
  def validates_each(*atts, &block)
195
195
  opts = extract_options!(atts)
196
196
  blank_meth = db.method(:blank_object?).to_proc
197
197
  blk = if (i = opts[:if]) || (am = opts[:allow_missing]) || (an = opts[:allow_nil]) || (ab = opts[:allow_blank])
198
+ if i.is_a?(Proc)
199
+ i = Plugins.def_sequel_method(self, "validation_class_methods_if", 0, &i)
200
+ end
201
+
198
202
  proc do |o,a,v|
199
203
  next if i && !validation_if_proc(o, i)
200
204
  next if an && Array(v).all?(&:nil?)
@@ -434,8 +438,6 @@ module Sequel
434
438
  case i
435
439
  when Symbol
436
440
  o.get_column_value(i)
437
- when Proc
438
- o.instance_exec(&i)
439
441
  else
440
442
  raise(::Sequel::Error, "invalid value for :if validation option")
441
443
  end
@@ -1086,11 +1086,23 @@ module Sequel
1086
1086
  def self.from_value_pair(l, r)
1087
1087
  case r
1088
1088
  when Range
1089
- expr = new(:>=, l, r.begin)
1089
+ unless r.begin.nil?
1090
+ begin_expr = new(:>=, l, r.begin)
1091
+ end
1090
1092
  unless r.end.nil?
1091
- expr = new(:AND, expr, new(r.exclude_end? ? :< : :<=, l, r.end))
1093
+ end_expr = new(r.exclude_end? ? :< : :<=, l, r.end)
1094
+ end
1095
+ if begin_expr
1096
+ if end_expr
1097
+ new(:AND, begin_expr, end_expr)
1098
+ else
1099
+ begin_expr
1100
+ end
1101
+ elsif end_expr
1102
+ end_expr
1103
+ else
1104
+ new(:'=', 1, 1)
1092
1105
  end
1093
- expr
1094
1106
  when ::Array
1095
1107
  r = r.dup.freeze unless r.frozen?
1096
1108
  new(:IN, l, r)
@@ -54,7 +54,14 @@ module Sequel
54
54
  convert_output_datetime_other(v, output_timezone)
55
55
  end
56
56
  else
57
- v.public_send(output_timezone == :utc ? :getutc : :getlocal)
57
+ case output_timezone
58
+ when :utc
59
+ v.getutc
60
+ when :local
61
+ v.getlocal
62
+ else
63
+ convert_output_time_other(v, output_timezone)
64
+ end
58
65
  end
59
66
  else
60
67
  v
@@ -110,7 +117,7 @@ module Sequel
110
117
  # same time and just modifying the timezone.
111
118
  def convert_input_datetime_no_offset(v, input_timezone)
112
119
  case input_timezone
113
- when :utc, nil
120
+ when nil, :utc
114
121
  v # DateTime assumes UTC if no offset is given
115
122
  when :local
116
123
  offset = local_offset_for_datetime(v)
@@ -119,7 +126,7 @@ module Sequel
119
126
  convert_input_datetime_other(v, input_timezone)
120
127
  end
121
128
  end
122
-
129
+
123
130
  # Convert the given +DateTime+ to the given input_timezone that is not supported
124
131
  # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
125
132
  # Can be overridden in extensions.
@@ -127,6 +134,13 @@ module Sequel
127
134
  raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}"
128
135
  end
129
136
 
137
+ # Convert the given +Time+ to the given input_timezone that is not supported
138
+ # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
139
+ # Can be overridden in extensions.
140
+ def convert_input_time_other(v, input_timezone)
141
+ raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}"
142
+ end
143
+
130
144
  # Converts the object from a +String+, +Array+, +Date+, +DateTime+, or +Time+ into an
131
145
  # instance of <tt>Sequel.datetime_class</tt>. If given an array or a string that doesn't
132
146
  # contain an offset, assume that the array/string is already in the given +input_timezone+.
@@ -139,12 +153,17 @@ module Sequel
139
153
  else
140
154
  # Correct for potentially wrong offset if string doesn't include offset
141
155
  if v2.is_a?(DateTime)
142
- v2 = convert_input_datetime_no_offset(v2, input_timezone)
156
+ convert_input_datetime_no_offset(v2, input_timezone)
143
157
  else
144
- # Time assumes local time if no offset is given
145
- v2 = v2.getutc + v2.utc_offset if input_timezone == :utc
158
+ case input_timezone
159
+ when nil, :local
160
+ v2
161
+ when :utc
162
+ (v2 + v2.utc_offset).utc
163
+ else
164
+ convert_input_time_other((v2 + v2.utc_offset).utc, input_timezone)
165
+ end
146
166
  end
147
- v2
148
167
  end
149
168
  when Array
150
169
  y, mo, d, h, mi, s, ns, off = v
@@ -155,8 +174,18 @@ module Sequel
155
174
  else
156
175
  convert_input_datetime_no_offset(DateTime.civil(y, mo, d, h, mi, s), input_timezone)
157
176
  end
177
+ elsif off
178
+ s += Rational(ns, 1000000000) if ns
179
+ Time.new(y, mo, d, h, mi, s, (off*86400).to_i)
158
180
  else
159
- Time.public_send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
181
+ case input_timezone
182
+ when nil, :local
183
+ Time.local(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
184
+ when :utc
185
+ Time.utc(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
186
+ else
187
+ convert_input_time_other(Time.utc(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0)), input_timezone)
188
+ end
160
189
  end
161
190
  when Hash
162
191
  ary = [:year, :month, :day, :hour, :minute, :second, :nanos].map{|x| (v[x] || v[x.to_s]).to_i}
@@ -188,23 +217,33 @@ module Sequel
188
217
  raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}"
189
218
  end
190
219
 
220
+ # Convert the given +Time+ to the given output_timezone that is not supported
221
+ # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
222
+ # Can be overridden in extensions.
223
+ def convert_output_time_other(v, output_timezone)
224
+ raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}"
225
+ end
226
+
191
227
  # Convert the timezone setter argument. Returns argument given by default,
192
228
  # exists for easier overriding in extensions.
193
229
  def convert_timezone_setter_arg(tz)
194
230
  tz
195
231
  end
196
232
 
197
- # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included.
233
+ # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included, in fraction of a day.
198
234
  def local_offset_for_datetime(dt)
199
235
  time_offset_to_datetime_offset Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec).utc_offset
200
236
  end
201
237
 
202
238
  # Caches offset conversions to avoid excess Rational math.
203
239
  def time_offset_to_datetime_offset(offset_secs)
204
- @local_offsets ||= {}
205
- @local_offsets[offset_secs] ||= Rational(offset_secs, 86400)
240
+ if offset = Sequel.synchronize{@local_offsets[offset_secs]}
241
+ return offset
242
+ end
243
+ Sequel.synchronize{@local_offsets[offset_secs] = Rational(offset_secs, 86400)}
206
244
  end
207
245
  end
208
246
 
247
+ @local_offsets = {}
209
248
  extend Timezones
210
249
  end
@@ -6,7 +6,7 @@ module Sequel
6
6
 
7
7
  # The minor version of Sequel. Bumped for every non-patch level
8
8
  # release, generally around once a month.
9
- MINOR = 19
9
+ MINOR = 24
10
10
 
11
11
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
12
12
  # releases that fix regressions from previous versions.
@@ -40,6 +40,30 @@ describe "A MSSQL database" do
40
40
  end
41
41
  end
42
42
 
43
+ describe "MSSQL decimal locale handling" do
44
+ before do
45
+ @locale = WIN32OLE.locale
46
+ @decimal = BigDecimal('1234.56')
47
+ end
48
+ after do
49
+ WIN32OLE.locale = @locale
50
+ end
51
+
52
+ it "should work with current locale" do
53
+ DB.get(Sequel.cast(@decimal, 'decimal(16,4)').as(:v)).must_equal @decimal
54
+ end
55
+
56
+ it "should work with 1031 locale" do
57
+ WIN32OLE.locale = 1031
58
+ DB.get(Sequel.cast(@decimal, 'decimal(16,4)').as(:v)).must_equal @decimal
59
+ end
60
+
61
+ it "should work with 1033 locale" do
62
+ WIN32OLE.locale = 1033
63
+ DB.get(Sequel.cast(@decimal, 'decimal(16,4)').as(:v)).must_equal @decimal
64
+ end
65
+ end if DB.adapter_scheme == :ado
66
+
43
67
  describe "MSSQL" do
44
68
  before(:all) do
45
69
  @db = DB
@@ -551,11 +551,6 @@ describe "A MySQL database" do
551
551
  db = Sequel.connect(DB.opts.merge(:read_timeout=>22342))
552
552
  db.test_connection
553
553
  end
554
-
555
- it "should accept a connect_timeout option when connecting" do
556
- db = Sequel.connect(DB.opts.merge(:connect_timeout=>22342))
557
- db.test_connection
558
- end
559
554
  end
560
555
 
561
556
  describe "MySQL foreign key support" do
@@ -104,6 +104,24 @@ describe "PostgreSQL", '#create_table' do
104
104
  @db.check_constraints(:tmp_dolls).must_equal(:ic=>{:definition=>"CHECK ((i > 2))", :columns=>[:i]}, :jc=>{:definition=>"CHECK ((j > 2))", :columns=>[:j]}, :ijc=>{:definition=>"CHECK (((i - j) > 2))", :columns=>[:i, :j]})
105
105
  end
106
106
 
107
+ it "should have #check_constraints return check constraints where columns are unknown" do
108
+ begin
109
+ @db.create_table(:tmp_dolls) do
110
+ Integer :i
111
+ Integer :j
112
+ end
113
+ @db.run "CREATE OR REPLACE FUNCTION valid_tmp_dolls(t1 tmp_dolls) RETURNS boolean AS 'SELECT false' LANGUAGE SQL;"
114
+ @db.alter_table(:tmp_dolls) do
115
+ add_constraint(:valid_tmp_dolls, Sequel.function(:valid_tmp_dolls, :tmp_dolls))
116
+ end
117
+
118
+ @db.check_constraints(:tmp_dolls).must_equal(:valid_tmp_dolls=>{:definition=>"CHECK (valid_tmp_dolls(tmp_dolls.*))", :columns=>[]})
119
+ ensure
120
+ @db.run "ALTER TABLE tmp_dolls DROP CONSTRAINT IF EXISTS valid_tmp_dolls"
121
+ @db.run "DROP FUNCTION IF EXISTS valid_tmp_dolls(tmp_dolls)"
122
+ end
123
+ end if DB.server_version >= 90000
124
+
107
125
  it "should not allow to pass both :temp and :unlogged" do
108
126
  proc do
109
127
  @db.create_table(:temp_unlogged_dolls, :temp => true, :unlogged => true){text :name}
@@ -209,6 +227,24 @@ describe "PostgreSQL", '#create_table' do
209
227
  @db.convert_serial_to_identity(:tmp_dolls, :column=>:id)
210
228
  end if DB.server_version >= 100002 && DB.get{current_setting('is_superuser')} == 'on'
211
229
 
230
+ it "should support creating generated columns" do
231
+ @db.create_table(:tmp_dolls){Integer :a; Integer :b; Integer :c, :generated_always_as=>Sequel[:a] * 2 + :b + 1}
232
+ @db[:tmp_dolls].insert(:a=>100, :b=>10)
233
+ @db[:tmp_dolls].select_order_map([:a, :b, :c]).must_equal [[100, 10, 211]]
234
+ end if DB.server_version >= 120000
235
+
236
+ it "should support deferred primary key and unique constraints on columns" do
237
+ @db.create_table(:tmp_dolls){primary_key :id, :primary_key_deferrable=>true; Integer :i, :unique=>true, :unique_deferrable=>true}
238
+ @db[:tmp_dolls].insert(:i=>10)
239
+ DB.transaction do
240
+ @db[:tmp_dolls].insert(:id=>1, :i=>1)
241
+ @db[:tmp_dolls].insert(:id=>10, :i=>10)
242
+ @db[:tmp_dolls].where(:i=>1).update(:id=>2)
243
+ @db[:tmp_dolls].where(:id=>10).update(:i=>2)
244
+ end
245
+ @db[:tmp_dolls].select_order_map([:id, :i]).must_equal [[1, 10], [2, 1], [10, 2]]
246
+ end if DB.server_version >= 90000
247
+
212
248
  it "should support pg_loose_count extension" do
213
249
  @db.extension :pg_loose_count
214
250
  @db.create_table(:tmp_dolls){text :name}
@@ -334,6 +370,26 @@ describe "PostgreSQL", 'INSERT ON CONFLICT' do
334
370
  end
335
371
  end if DB.server_version >= 90500
336
372
 
373
+ describe "A PostgreSQL database" do
374
+ before do
375
+ @db = DB
376
+ @db.create_table!(:cte_test){Integer :id}
377
+ end
378
+ after do
379
+ @db.drop_table?(:cte_test)
380
+ end
381
+
382
+ it "should give correct results for WITH AS [NOT] MATERIALIZED" do
383
+ @ds = @db[:cte_test]
384
+ @ds.insert(1)
385
+ @ds.insert(2)
386
+
387
+ @db[:t].with(:t, @ds, :materialized=>nil).order(:id).map(:id).must_equal [1, 2]
388
+ @db[:t].with(:t, @ds, :materialized=>true).order(:id).map(:id).must_equal [1, 2]
389
+ @db[:t].with(:t, @ds, :materialized=>false).order(:id).map(:id).must_equal [1, 2]
390
+ end
391
+ end if DB.server_version >= 120000
392
+
337
393
  describe "A PostgreSQL database" do
338
394
  before(:all) do
339
395
  @db = DB
@@ -2926,6 +2982,8 @@ describe 'PostgreSQL json type' do
2926
2982
  @h = {'a'=>'b', '1'=>[3, 4, 5]}
2927
2983
  end
2928
2984
  after do
2985
+ @db.wrap_json_primitives = nil
2986
+ @db.typecast_json_strings = nil
2929
2987
  @db.drop_table?(:items)
2930
2988
  end
2931
2989
 
@@ -2933,9 +2991,12 @@ describe 'PostgreSQL json type' do
2933
2991
  json_types << :jsonb if DB.server_version >= 90400
2934
2992
  json_types.each do |json_type|
2935
2993
  json_array_type = "#{json_type}[]"
2936
- pg_json = lambda{|v| Sequel.send(:"pg_#{json_type}", v)}
2994
+ pg_json = Sequel.method(:"pg_#{json_type}")
2995
+ pg_json_wrap = Sequel.method(:"pg_#{json_type}_wrap")
2937
2996
  hash_class = json_type == :jsonb ? Sequel::Postgres::JSONBHash : Sequel::Postgres::JSONHash
2938
2997
  array_class = json_type == :jsonb ? Sequel::Postgres::JSONBArray : Sequel::Postgres::JSONArray
2998
+ str_class = json_type == :jsonb ? Sequel::Postgres::JSONBString : Sequel::Postgres::JSONString
2999
+ object_class = json_type == :jsonb ? Sequel::Postgres::JSONBObject : Sequel::Postgres::JSONObject
2939
3000
 
2940
3001
  it 'insert and retrieve json values' do
2941
3002
  @db.create_table!(:items){column :j, json_type}
@@ -2965,6 +3026,44 @@ describe 'PostgreSQL json type' do
2965
3026
  @ds.all.must_equal rs
2966
3027
  end
2967
3028
 
3029
+ it 'insert and retrieve json primitive values' do
3030
+ @db.create_table!(:items){column :j, json_type}
3031
+ ['str', 1, 2.5, nil, true, false].each do |rv|
3032
+ @ds.delete
3033
+ @ds.insert(pg_json_wrap.call(rv))
3034
+ @ds.count.must_equal 1
3035
+ rs = @ds.all
3036
+ v = rs.first[:j]
3037
+ v.class.must_equal(rv.class)
3038
+ if rv.nil?
3039
+ v.must_be_nil
3040
+ else
3041
+ v.must_equal rv
3042
+ end
3043
+ end
3044
+
3045
+ @db.wrap_json_primitives = true
3046
+ ['str', 1, 2.5, nil, true, false].each do |rv|
3047
+ @ds.delete
3048
+ @ds.insert(pg_json_wrap.call(rv))
3049
+ @ds.count.must_equal 1
3050
+ rs = @ds.all
3051
+ v = rs.first[:j]
3052
+ v.class.ancestors.must_include(object_class)
3053
+ v.__getobj__.must_be_kind_of(rv.class)
3054
+ if rv.nil?
3055
+ v.must_be_nil
3056
+ v.__getobj__.must_be_nil
3057
+ else
3058
+ v.must_equal rv
3059
+ v.__getobj__.must_equal rv
3060
+ end
3061
+ @ds.delete
3062
+ @ds.insert(rs.first)
3063
+ @ds.all[0][:j].must_equal rs[0][:j]
3064
+ end
3065
+ end
3066
+
2968
3067
  it 'insert and retrieve json[] values' do
2969
3068
  @db.create_table!(:items){column :j, json_array_type}
2970
3069
  j = Sequel.pg_array([pg_json.call('a'=>1), pg_json.call(['b', 2])])
@@ -2981,16 +3080,122 @@ describe 'PostgreSQL json type' do
2981
3080
  @ds.all.must_equal rs
2982
3081
  end
2983
3082
 
3083
+ it 'insert and retrieve json[] values with json primitives' do
3084
+ @db.create_table!(:items){column :j, json_array_type}
3085
+ raw = ['str', 1, 2.5, nil, true, false]
3086
+ j = Sequel.pg_array(raw.map(&pg_json_wrap), json_type)
3087
+ @ds.insert(j)
3088
+ @ds.count.must_equal 1
3089
+ rs = @ds.all
3090
+ v = rs.first[:j]
3091
+ v.class.must_equal(Sequel::Postgres::PGArray)
3092
+ v.to_a.must_be_kind_of(Array)
3093
+ v.map(&:class).must_equal raw.map(&:class)
3094
+ v.must_equal raw
3095
+ v.to_a.must_equal raw
3096
+
3097
+ @db.wrap_json_primitives = true
3098
+ j = Sequel.pg_array(raw.map(&pg_json_wrap), json_type)
3099
+ @ds.insert(j)
3100
+ rs = @ds.all
3101
+ v = rs.first[:j]
3102
+ v.class.must_equal(Sequel::Postgres::PGArray)
3103
+ v.to_a.must_be_kind_of(Array)
3104
+ v.map(&:class).each{|c| c.ancestors.must_include(object_class)}
3105
+ [v, v.to_a].each do |v0|
3106
+ v0.zip(raw) do |v1, r1|
3107
+ if r1.nil?
3108
+ v1.must_be_nil
3109
+ v1.__getobj__.must_be_nil
3110
+ else
3111
+ v1.must_equal r1
3112
+ v1.__getobj__.must_equal r1
3113
+ end
3114
+ end
3115
+ end
3116
+ @ds.delete
3117
+ @ds.insert(rs.first)
3118
+ @ds.all[0][:j].zip(rs[0][:j]) do |v1, r1|
3119
+ if v1.__getobj__.nil?
3120
+ v1.must_be_nil
3121
+ v1.__getobj__.must_be_nil
3122
+ else
3123
+ v1.must_equal r1
3124
+ v1.must_equal r1.__getobj__
3125
+ v1.__getobj__.must_equal r1
3126
+ v1.__getobj__.must_equal r1.__getobj__
3127
+ end
3128
+ end
3129
+ end
3130
+
2984
3131
  it 'with models' do
2985
3132
  @db.create_table!(:items) do
2986
3133
  primary_key :id
2987
3134
  column :h, json_type
2988
3135
  end
2989
3136
  c = Class.new(Sequel::Model(@db[:items]))
3137
+ c.create(:h=>@h).h.must_equal @h
3138
+ c.create(:h=>@a).h.must_equal @a
2990
3139
  c.create(:h=>pg_json.call(@h)).h.must_equal @h
2991
3140
  c.create(:h=>pg_json.call(@a)).h.must_equal @a
2992
3141
  end
2993
3142
 
3143
+ it 'with models with json primitives' do
3144
+ @db.create_table!(:items) do
3145
+ primary_key :id
3146
+ column :h, json_type
3147
+ end
3148
+ c = Class.new(Sequel::Model(@db[:items]))
3149
+
3150
+ ['str', 1, 2.5, nil, true, false].each do |v|
3151
+ @db.wrap_json_primitives = nil
3152
+ cv = c[c.insert(:h=>pg_json_wrap.call(v))]
3153
+ cv.h.class.ancestors.wont_include(object_class)
3154
+ if v.nil?
3155
+ cv.h.must_be_nil
3156
+ else
3157
+ cv.h.must_equal v
3158
+ end
3159
+
3160
+ @db.wrap_json_primitives = true
3161
+ cv.refresh
3162
+ cv.h.class.ancestors.must_include(object_class)
3163
+ cv.save
3164
+ cv.refresh
3165
+ cv.h.class
3166
+
3167
+ if v.nil?
3168
+ cv.h.must_be_nil
3169
+ else
3170
+ cv.h.must_equal v
3171
+ end
3172
+
3173
+ c.new(:h=>cv.h).h.class.ancestors.must_include(object_class)
3174
+ end
3175
+
3176
+ v = c.new(:h=>'{}').h
3177
+ v.class.must_equal hash_class
3178
+ v.must_equal({})
3179
+ @db.typecast_json_strings = true
3180
+ v = c.new(:h=>'{}').h
3181
+ v.class.must_equal str_class
3182
+ v.must_equal '{}'
3183
+
3184
+ c.new(:h=>'str').h.class.ancestors.must_include(object_class)
3185
+ c.new(:h=>'str').h.must_equal 'str'
3186
+ c.new(:h=>1).h.class.ancestors.must_include(object_class)
3187
+ c.new(:h=>1).h.must_equal 1
3188
+ c.new(:h=>2.5).h.class.ancestors.must_include(object_class)
3189
+ c.new(:h=>2.5).h.must_equal 2.5
3190
+ c.new(:h=>true).h.class.ancestors.must_include(object_class)
3191
+ c.new(:h=>true).h.must_equal true
3192
+ c.new(:h=>false).h.class.ancestors.must_include(object_class)
3193
+ c.new(:h=>false).h.must_equal false
3194
+
3195
+ c.new(:h=>nil).h.class.ancestors.wont_include(object_class)
3196
+ c.new(:h=>nil).h.must_be_nil
3197
+ end
3198
+
2994
3199
  it 'with empty json default values and defaults_setter plugin' do
2995
3200
  @db.create_table!(:items) do
2996
3201
  column :h, json_type, :default=>hash_class.new({})
@@ -3025,6 +3230,36 @@ describe 'PostgreSQL json type' do
3025
3230
  @ds.get(:i).must_equal j
3026
3231
  end if uses_pg_or_jdbc
3027
3232
 
3233
+ it 'use json primitives in bound variables' do
3234
+ @db.create_table!(:items){column :i, json_type}
3235
+ @db.wrap_json_primitives = true
3236
+ raw = ['str', 1, 2.5, nil, true, false]
3237
+ raw.each do |v|
3238
+ @ds.delete
3239
+ @ds.call(:insert, {:i=>@db.get(pg_json_wrap.call(v))}, {:i=>:$i})
3240
+ rv = @ds.get(:i)
3241
+ rv.class.ancestors.must_include(object_class)
3242
+ if v.nil?
3243
+ rv.must_be_nil
3244
+ else
3245
+ rv.must_equal v
3246
+ end
3247
+ end
3248
+
3249
+ @db.create_table!(:items){column :i, json_array_type}
3250
+ j = Sequel.pg_array(raw.map(&pg_json_wrap), json_type)
3251
+ @ds.call(:insert, {:i=>j}, {:i=>:$i})
3252
+ @ds.all[0][:i].zip(raw) do |v1, r1|
3253
+ if v1.__getobj__.nil?
3254
+ v1.must_be_nil
3255
+ v1.__getobj__.must_be_nil
3256
+ else
3257
+ v1.must_equal r1
3258
+ v1.__getobj__.must_equal r1
3259
+ end
3260
+ end
3261
+ end if uses_pg_or_jdbc
3262
+
3028
3263
  it 'operations/functions with pg_json_ops' do
3029
3264
  Sequel.extension :pg_json_ops
3030
3265
  jo = pg_json.call('a'=>1, 'b'=>{'c'=>2, 'd'=>{'e'=>3}}).op
@@ -3384,6 +3619,30 @@ describe 'PostgreSQL range types' do
3384
3619
  @ds.filter(h).call(:delete, @ra).must_equal 1
3385
3620
  end if uses_pg_or_jdbc
3386
3621
 
3622
+ it 'handle endless ranges' do
3623
+ @db.get(Sequel.cast(eval('1...'), :int4range)).must_be :==, eval('1...')
3624
+ @db.get(Sequel.cast(eval('1...'), :int4range)).wont_be :==, eval('2...')
3625
+ @db.get(Sequel.cast(eval('1...'), :int4range)).wont_be :==, eval('1..')
3626
+ @db.get(Sequel.cast(eval('2...'), :int4range)).must_be :==, eval('2...')
3627
+ @db.get(Sequel.cast(eval('2...'), :int4range)).wont_be :==, eval('2..')
3628
+ @db.get(Sequel.cast(eval('2...'), :int4range)).wont_be :==, eval('1...')
3629
+ end if RUBY_VERSION >= '2.6'
3630
+
3631
+ it 'handle startless ranges' do
3632
+ @db.get(Sequel.cast(eval('...1'), :int4range)).must_be :==, Sequel::Postgres::PGRange.new(nil, 1, :exclude_begin=>true, :exclude_end=>true, :db_type=>"int4range")
3633
+ @db.get(Sequel.cast(eval('...1'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, 2, :exclude_begin=>true, :exclude_end=>true, :db_type=>"int4range")
3634
+ @db.get(Sequel.cast(eval('...1'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, 1, :exclude_end=>true, :db_type=>"int4range")
3635
+ @db.get(Sequel.cast(eval('...1'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, 1, :exclude_begin=>true, :db_type=>"int4range")
3636
+ end if RUBY_VERSION >= '2.7'
3637
+
3638
+ it 'handle startless ranges' do
3639
+ @db.get(Sequel.cast(eval('nil...nil'), :int4range)).must_be :==, Sequel::Postgres::PGRange.new(nil, nil, :exclude_begin=>true, :exclude_end=>true, :db_type=>"int4range")
3640
+ @db.get(Sequel.cast(eval('nil...nil'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, nil, :exclude_begin=>true, :db_type=>"int4range")
3641
+ @db.get(Sequel.cast(eval('nil...nil'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, nil, :exclude_end=>true, :db_type=>"int4range")
3642
+ @db.get(Sequel.cast(eval('nil...nil'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(1, nil, :exclude_begin=>true, :db_type=>"int4range")
3643
+ @db.get(Sequel.cast(eval('nil...nil'), :int4range)).wont_be :==, Sequel::Postgres::PGRange.new(nil, 1, :exclude_begin=>true, :db_type=>"int4range")
3644
+ end if RUBY_VERSION >= '2.7'
3645
+
3387
3646
  it 'parse default values for schema' do
3388
3647
  @db.create_table!(:items) do
3389
3648
  Integer :j
@@ -3988,6 +4247,10 @@ describe "pg_auto_constraint_validations plugin" do
3988
4247
  constraint :valid_i, Sequel[:i] < 10
3989
4248
  constraint(:valid_i_id, Sequel[:i] + Sequel[:id] < 20)
3990
4249
  end
4250
+ @db.run "CREATE OR REPLACE FUNCTION valid_test1(t1 test1) RETURNS boolean AS 'SELECT t1.i != -100' LANGUAGE SQL;"
4251
+ @db.alter_table(:test1) do
4252
+ add_constraint(:valid_test1, Sequel.function(:valid_test1, :test1))
4253
+ end
3991
4254
  @db.create_table!(:test2) do
3992
4255
  Integer :test2_id, :primary_key=>true
3993
4256
  foreign_key :test1_id, :test1
@@ -4008,6 +4271,8 @@ describe "pg_auto_constraint_validations plugin" do
4008
4271
  @c2.insert(:test2_id=>3, :test1_id=>1)
4009
4272
  end
4010
4273
  after(:all) do
4274
+ @db.run "ALTER TABLE test1 DROP CONSTRAINT IF EXISTS valid_test1"
4275
+ @db.run "DROP FUNCTION IF EXISTS valid_test1(test1)"
4011
4276
  @db.drop_table?(:test2, :test1)
4012
4277
  end
4013
4278
 
@@ -4017,6 +4282,14 @@ describe "pg_auto_constraint_validations plugin" do
4017
4282
  o.errors.must_equal(:i=>['is invalid'])
4018
4283
  end
4019
4284
 
4285
+ it "should handle check constraint failures where the columns are unknown, if columns are explicitly specified" do
4286
+ o = @c1.new(:id=>5, :i=>-100)
4287
+ proc{o.save}.must_raise Sequel::CheckConstraintViolation
4288
+ @c1.pg_auto_constraint_validation_override(:valid_test1, :i, "should not be -100")
4289
+ proc{o.save}.must_raise Sequel::ValidationFailed
4290
+ o.errors.must_equal(:i=>['should not be -100'])
4291
+ end
4292
+
4020
4293
  it "should handle check constraint failures as validation errors when updating" do
4021
4294
  o = @c1.new(:id=>5, :i=>3)
4022
4295
  o.save
@@ -4090,4 +4363,49 @@ describe "pg_auto_constraint_validations plugin" do
4090
4363
  proc{o.save}.must_raise Sequel::ValidationFailed
4091
4364
  o.errors.must_equal(:i=>['is invalid'], :id=>['is invalid'])
4092
4365
  end
4366
+
4367
+ it "should handle dumping cached metadata and loading metadata from cache" do
4368
+ cache_file = "spec/files/pgacv-#{$$}.cache"
4369
+ begin
4370
+ c = Class.new(Sequel::Model)
4371
+ c.plugin :pg_auto_constraint_validations, :cache_file=>cache_file
4372
+ c1 = Class.new(c)
4373
+ def c1.name; 'Foo' end
4374
+ c1.dataset = DB[:test1]
4375
+ c2 = Class.new(c)
4376
+ def c2.name; 'Bar' end
4377
+ c2.dataset = DB[:test2]
4378
+ c1.unrestrict_primary_key
4379
+ c2.unrestrict_primary_key
4380
+
4381
+ o = c1.new(:id=>5, :i=>12)
4382
+ proc{o.save}.must_raise Sequel::ValidationFailed
4383
+ o.errors.must_equal(:i=>['is invalid'])
4384
+ o = c2.new(:test2_id=>4, :test1_id=>2)
4385
+ proc{o.save}.must_raise Sequel::ValidationFailed
4386
+ o.errors.must_equal(:test1_id=>['is invalid'])
4387
+
4388
+ c.dump_pg_auto_constraint_validations_cache
4389
+
4390
+ c = Class.new(Sequel::Model)
4391
+ c.plugin :pg_auto_constraint_validations, :cache_file=>cache_file
4392
+ c1 = Class.new(c)
4393
+ def c1.name; 'Foo' end
4394
+ c1.dataset = DB[:test1]
4395
+ c2 = Class.new(c)
4396
+ def c2.name; 'Bar' end
4397
+ c2.dataset = DB[:test2]
4398
+ c1.unrestrict_primary_key
4399
+ c2.unrestrict_primary_key
4400
+
4401
+ o = c1.new(:id=>5, :i=>12)
4402
+ proc{o.save}.must_raise Sequel::ValidationFailed
4403
+ o.errors.must_equal(:i=>['is invalid'])
4404
+ o = c2.new(:test2_id=>4, :test1_id=>2)
4405
+ proc{o.save}.must_raise Sequel::ValidationFailed
4406
+ o.errors.must_equal(:test1_id=>['is invalid'])
4407
+ ensure
4408
+ File.delete(cache_file) if File.file?(cache_file)
4409
+ end
4410
+ end
4093
4411
  end if DB.respond_to?(:error_info)