sequel 5.19.0 → 5.24.0

Sign up to get free protection for your applications and to get access to all the features.
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)