sequel 3.38.0 → 3.39.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 (79) hide show
  1. data/CHANGELOG +62 -0
  2. data/README.rdoc +2 -2
  3. data/bin/sequel +12 -2
  4. data/doc/advanced_associations.rdoc +1 -1
  5. data/doc/association_basics.rdoc +13 -0
  6. data/doc/release_notes/3.39.0.txt +237 -0
  7. data/doc/schema_modification.rdoc +4 -4
  8. data/lib/sequel/adapters/jdbc/derby.rb +1 -0
  9. data/lib/sequel/adapters/mock.rb +5 -0
  10. data/lib/sequel/adapters/mysql.rb +8 -1
  11. data/lib/sequel/adapters/mysql2.rb +10 -3
  12. data/lib/sequel/adapters/postgres.rb +72 -8
  13. data/lib/sequel/adapters/shared/db2.rb +1 -0
  14. data/lib/sequel/adapters/shared/mssql.rb +57 -0
  15. data/lib/sequel/adapters/shared/mysql.rb +95 -19
  16. data/lib/sequel/adapters/shared/oracle.rb +14 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +63 -24
  18. data/lib/sequel/adapters/shared/sqlite.rb +6 -9
  19. data/lib/sequel/connection_pool/sharded_threaded.rb +8 -3
  20. data/lib/sequel/connection_pool/threaded.rb +9 -4
  21. data/lib/sequel/database/query.rb +60 -48
  22. data/lib/sequel/database/schema_generator.rb +13 -6
  23. data/lib/sequel/database/schema_methods.rb +65 -12
  24. data/lib/sequel/dataset/actions.rb +22 -4
  25. data/lib/sequel/dataset/features.rb +5 -0
  26. data/lib/sequel/dataset/graph.rb +2 -3
  27. data/lib/sequel/dataset/misc.rb +2 -2
  28. data/lib/sequel/dataset/query.rb +0 -2
  29. data/lib/sequel/dataset/sql.rb +33 -12
  30. data/lib/sequel/extensions/constraint_validations.rb +451 -0
  31. data/lib/sequel/extensions/eval_inspect.rb +17 -2
  32. data/lib/sequel/extensions/pg_array_ops.rb +15 -5
  33. data/lib/sequel/extensions/pg_interval.rb +2 -2
  34. data/lib/sequel/extensions/pg_row_ops.rb +18 -0
  35. data/lib/sequel/extensions/schema_dumper.rb +3 -11
  36. data/lib/sequel/model/associations.rb +3 -2
  37. data/lib/sequel/model/base.rb +57 -13
  38. data/lib/sequel/model/exceptions.rb +20 -2
  39. data/lib/sequel/plugins/constraint_validations.rb +198 -0
  40. data/lib/sequel/plugins/defaults_setter.rb +15 -1
  41. data/lib/sequel/plugins/dirty.rb +2 -2
  42. data/lib/sequel/plugins/identity_map.rb +12 -8
  43. data/lib/sequel/plugins/subclasses.rb +19 -1
  44. data/lib/sequel/plugins/tree.rb +3 -3
  45. data/lib/sequel/plugins/validation_helpers.rb +24 -4
  46. data/lib/sequel/sql.rb +64 -24
  47. data/lib/sequel/timezones.rb +10 -2
  48. data/lib/sequel/version.rb +1 -1
  49. data/spec/adapters/mssql_spec.rb +26 -25
  50. data/spec/adapters/mysql_spec.rb +57 -23
  51. data/spec/adapters/oracle_spec.rb +34 -49
  52. data/spec/adapters/postgres_spec.rb +226 -128
  53. data/spec/adapters/sqlite_spec.rb +50 -49
  54. data/spec/core/connection_pool_spec.rb +22 -0
  55. data/spec/core/database_spec.rb +53 -47
  56. data/spec/core/dataset_spec.rb +36 -32
  57. data/spec/core/expression_filters_spec.rb +14 -2
  58. data/spec/core/mock_adapter_spec.rb +4 -0
  59. data/spec/core/object_graph_spec.rb +0 -13
  60. data/spec/core/schema_spec.rb +64 -5
  61. data/spec/core_extensions_spec.rb +1 -0
  62. data/spec/extensions/constraint_validations_plugin_spec.rb +196 -0
  63. data/spec/extensions/constraint_validations_spec.rb +316 -0
  64. data/spec/extensions/defaults_setter_spec.rb +24 -0
  65. data/spec/extensions/eval_inspect_spec.rb +9 -0
  66. data/spec/extensions/identity_map_spec.rb +11 -2
  67. data/spec/extensions/pg_array_ops_spec.rb +9 -0
  68. data/spec/extensions/pg_row_ops_spec.rb +11 -1
  69. data/spec/extensions/pg_row_plugin_spec.rb +4 -0
  70. data/spec/extensions/schema_dumper_spec.rb +8 -5
  71. data/spec/extensions/subclasses_spec.rb +14 -0
  72. data/spec/extensions/validation_helpers_spec.rb +15 -2
  73. data/spec/integration/dataset_test.rb +75 -1
  74. data/spec/integration/plugin_test.rb +146 -0
  75. data/spec/integration/schema_test.rb +34 -0
  76. data/spec/model/dataset_methods_spec.rb +38 -0
  77. data/spec/model/hooks_spec.rb +6 -0
  78. data/spec/model/validations_spec.rb +27 -2
  79. metadata +8 -2
@@ -60,10 +60,12 @@ describe "Blockless Ruby Filters" do
60
60
  end
61
61
 
62
62
  it "should support ~ via Hash and Regexp (if supported by database)" do
63
+ def @d.supports_regexp?; true end
63
64
  @d.l(:x => /blah/).should == '(x ~ \'blah\')'
64
65
  end
65
66
 
66
67
  it "should support !~ via inverted Hash and Regexp" do
68
+ def @d.supports_regexp?; true end
67
69
  @d.l(~Sequel.expr(:x => /blah/)).should == '(x !~ \'blah\')'
68
70
  end
69
71
 
@@ -141,7 +143,7 @@ describe "Blockless Ruby Filters" do
141
143
  @d.l(Sequel.expr(Sequel.lit('y') => Sequel.lit('z')) & Sequel.lit('x')).should == '((y = z) AND x)'
142
144
  @d.l((Sequel.lit('x') > 200) & (Sequel.lit('y') < 200)).should == '((x > 200) AND (y < 200))'
143
145
  @d.l(~(Sequel.lit('x') + 1 > 100)).should == '((x + 1) <= 100)'
144
- @d.l(Sequel.lit('x').like(/a/)).should == '(x ~ \'a\')'
146
+ @d.l(Sequel.lit('x').like('a')).should == '(x LIKE \'a\')'
145
147
  @d.l(Sequel.lit('x') + 1 > 100).should == '((x + 1) > 100)'
146
148
  @d.l((Sequel.lit('x') * :y) < 100.01).should == '((x * y) < 100.01)'
147
149
  @d.l((Sequel.lit('x') - Sequel.expr(:y)/2) >= 100000000000000000000000000000000000).should == '((x - (y / 2)) >= 100000000000000000000000000000000000)'
@@ -381,6 +383,14 @@ describe "Blockless Ruby Filters" do
381
383
  @d.lit(d.like(:b)).should == '((SELECT a FROM items) LIKE b)'
382
384
  @d.lit(d.ilike(:b)).should == '((SELECT a FROM items) ILIKE b)'
383
385
  end
386
+
387
+ it "should handled emulated char_length function" do
388
+ @d.lit(Sequel.char_length(:a)).should == 'char_length(a)'
389
+ end
390
+
391
+ it "should handled emulated trim function" do
392
+ @d.lit(Sequel.trim(:a)).should == 'trim(a)'
393
+ end
384
394
  end
385
395
 
386
396
  describe Sequel::SQL::VirtualRow do
@@ -523,12 +533,14 @@ end
523
533
  describe "Sequel core extension replacements" do
524
534
  before do
525
535
  @db = Sequel::Database.new
536
+ @ds = @db.dataset
537
+ def @ds.supports_regexp?; true end
526
538
  @o = Object.new
527
539
  def @o.sql_literal(ds) 'foo' end
528
540
  end
529
541
 
530
542
  def l(arg, should)
531
- @db.literal(arg).should == should
543
+ @ds.literal(arg).should == should
532
544
  end
533
545
 
534
546
  it "Sequel.expr should return items wrapped in Sequel objects" do
@@ -446,4 +446,8 @@ describe "Sequel Mock Adapter" do
446
446
  specify "should stub out the primary_key method for postgres" do
447
447
  Sequel.mock(:host=>'postgres').primary_key(:t).should == :id
448
448
  end
449
+
450
+ specify "should handle creating tables on oracle" do
451
+ proc{Sequel.mock(:host=>'oracle').create_table(:a){String :b}}.should_not raise_error
452
+ end
449
453
  end
@@ -92,19 +92,6 @@ describe Sequel::Dataset, " graphing" do
92
92
  ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
93
93
  end
94
94
 
95
- it "#graph should accept an object that responds to dataset as the dataset" do
96
- oc = Class.new
97
- o = oc.new
98
- ds = @ds2
99
- oc.send(:define_method, :dataset){ds}
100
- ds = @ds1.graph(o, :x=>:id)
101
- ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
102
- ds = :lines
103
- oc.send(:define_method, :dataset){ds}
104
- ds = @ds1.graph(o, :x=>:id)
105
- ds.sql.should == 'SELECT points.id, points.x, points.y, lines.id AS lines_id, lines.x AS lines_x, lines.y AS lines_y, lines.graph_id FROM points LEFT OUTER JOIN lines ON (lines.x = points.id)'
106
- end
107
-
108
95
  it "#graph should raise an error if a symbol, dataset, or model is not used" do
109
96
  proc{@ds1.graph(Object.new, :x=>:id)}.should raise_error(Sequel::Error)
110
97
  end
@@ -757,20 +757,34 @@ describe "DB#alter_table" do
757
757
  @db = Sequel.mock
758
758
  end
759
759
 
760
- specify "should allow adding not null constraint" do
760
+ specify "should allow adding not null constraint via set_column_allow_null with false argument" do
761
761
  @db.alter_table(:cats) do
762
762
  set_column_allow_null :score, false
763
763
  end
764
764
  @db.sqls.should == ["ALTER TABLE cats ALTER COLUMN score SET NOT NULL"]
765
765
  end
766
766
 
767
- specify "should allow droping not null constraint" do
767
+ specify "should allow removing not null constraint via set_column_allow_null with true argument" do
768
768
  @db.alter_table(:cats) do
769
769
  set_column_allow_null :score, true
770
770
  end
771
771
  @db.sqls.should == ["ALTER TABLE cats ALTER COLUMN score DROP NOT NULL"]
772
772
  end
773
773
 
774
+ specify "should allow adding not null constraint via set_column_not_null" do
775
+ @db.alter_table(:cats) do
776
+ set_column_not_null :score
777
+ end
778
+ @db.sqls.should == ["ALTER TABLE cats ALTER COLUMN score SET NOT NULL"]
779
+ end
780
+
781
+ specify "should allow removing not null constraint via set_column_allow_null without argument" do
782
+ @db.alter_table(:cats) do
783
+ set_column_allow_null :score
784
+ end
785
+ @db.sqls.should == ["ALTER TABLE cats ALTER COLUMN score DROP NOT NULL"]
786
+ end
787
+
774
788
  specify "should support add_column" do
775
789
  @db.alter_table(:cats) do
776
790
  add_column :score, :integer
@@ -939,6 +953,40 @@ describe "DB#alter_table" do
939
953
  "ALTER TABLE cats ALTER COLUMN score TYPE varchar(30)",
940
954
  "ALTER TABLE cats ALTER COLUMN score TYPE enum('a', 'b')"]
941
955
  end
956
+
957
+ specify "should combine operations into a single query if the database supports it" do
958
+ @db.meta_def(:supports_combining_alter_table_ops?){true}
959
+ @db.alter_table(:cats) do
960
+ add_column :a, Integer
961
+ drop_column :b
962
+ set_column_not_null :c
963
+ rename_column :d, :e
964
+ set_column_default :f, 'g'
965
+ set_column_type :h, Integer
966
+ add_constraint(:i){a > 1}
967
+ drop_constraint :j
968
+ end
969
+ @db.sqls.should == ["ALTER TABLE cats ADD COLUMN a integer, DROP COLUMN b, ALTER COLUMN c SET NOT NULL, RENAME COLUMN d TO e, ALTER COLUMN f SET DEFAULT 'g', ALTER COLUMN h TYPE integer, ADD CONSTRAINT i CHECK (a > 1), DROP CONSTRAINT j"]
970
+ end
971
+
972
+ specify "should combine operations into consecutive groups of combinable operations if the database supports combining operations" do
973
+ @db.meta_def(:supports_combining_alter_table_ops?){true}
974
+ @db.alter_table(:cats) do
975
+ add_column :a, Integer
976
+ drop_column :b
977
+ set_column_not_null :c
978
+ rename_column :d, :e
979
+ add_index :e
980
+ set_column_default :f, 'g'
981
+ set_column_type :h, Integer
982
+ add_constraint(:i){a > 1}
983
+ drop_constraint :j
984
+ end
985
+ @db.sqls.should == ["ALTER TABLE cats ADD COLUMN a integer, DROP COLUMN b, ALTER COLUMN c SET NOT NULL, RENAME COLUMN d TO e",
986
+ "CREATE INDEX cats_e_index ON cats (e)",
987
+ "ALTER TABLE cats ALTER COLUMN f SET DEFAULT 'g', ALTER COLUMN h TYPE integer, ADD CONSTRAINT i CHECK (a > 1), DROP CONSTRAINT j"]
988
+ end
989
+
942
990
  end
943
991
 
944
992
  describe "Database#create_table" do
@@ -1251,11 +1299,13 @@ describe "Schema Parser" do
1251
1299
  end
1252
1300
 
1253
1301
  specify "should correctly parse all supported data types" do
1254
- @db.meta_def(:schema_parse_table) do |t, opts|
1255
- [[:x, {:type=>schema_column_type(t.to_s)}]]
1302
+ sm = Module.new do
1303
+ def schema_parse_table(t, opts)
1304
+ [[:x, {:type=>schema_column_type(t.to_s)}]]
1305
+ end
1256
1306
  end
1307
+ @db.extend(sm)
1257
1308
  @db.schema(:tinyint).first.last[:type].should == :integer
1258
- @db.schema(:interval).first.last[:type].should == :interval
1259
1309
  @db.schema(:int).first.last[:type].should == :integer
1260
1310
  @db.schema(:integer).first.last[:type].should == :integer
1261
1311
  @db.schema(:bigint).first.last[:type].should == :integer
@@ -1277,6 +1327,7 @@ describe "Schema Parser" do
1277
1327
  @db.schema(:real).first.last[:type].should == :float
1278
1328
  @db.schema(:float).first.last[:type].should == :float
1279
1329
  @db.schema(:double).first.last[:type].should == :float
1330
+ @db.schema(:"double(1,2)").first.last[:type].should == :float
1280
1331
  @db.schema(:"double precision").first.last[:type].should == :float
1281
1332
  @db.schema(:number).first.last[:type].should == :decimal
1282
1333
  @db.schema(:numeric).first.last[:type].should == :decimal
@@ -1296,5 +1347,13 @@ describe "Schema Parser" do
1296
1347
  @db.schema(:binary).first.last[:type].should == :blob
1297
1348
  @db.schema(:varbinary).first.last[:type].should == :blob
1298
1349
  @db.schema(:enum).first.last[:type].should == :enum
1350
+
1351
+ @db = Sequel.mock(:host=>'postgres')
1352
+ @db.extend(sm)
1353
+ @db.schema(:interval).first.last[:type].should == :interval
1354
+
1355
+ @db = Sequel.mock(:host=>'mysql')
1356
+ @db.extend(sm)
1357
+ @db.schema(:set).first.last[:type].should == :set
1299
1358
  end
1300
1359
  end
@@ -23,6 +23,7 @@ describe "Core extensions" do
23
23
  before do
24
24
  db = Sequel::Database.new
25
25
  @d = db[:items]
26
+ def @d.supports_regexp?; true end
26
27
  def @d.l(*args, &block)
27
28
  literal(filter_expr(*args, &block))
28
29
  end
@@ -0,0 +1,196 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "Sequel::Plugins::ConstraintValidations" do
4
+ def model_class(opts={})
5
+ return @c if @c
6
+ @c = Class.new(Sequel::Model(@db[:items]))
7
+ @c.columns :name
8
+ @db.sqls
9
+ set_fetch(opts)
10
+ @c.plugin :constraint_validations
11
+ @c
12
+ end
13
+
14
+ def set_fetch(opts)
15
+ @db.fetch = {:table=>'items', :message=>nil, :allow_nil=>nil, :constraint_name=>nil, :validation_type=>'presence', :argument=>nil, :column=>'name'}.merge(opts)
16
+ end
17
+
18
+ before do
19
+ @db = Sequel.mock
20
+ set_fetch({})
21
+ @ds = @db[:items]
22
+ @ds.instance_variable_set(:@columns, [:name])
23
+ @ds2 = @db.dup[:items2]
24
+ @ds2.instance_variable_set(:@columns, [:name])
25
+ end
26
+
27
+ it "should load the validation_helpers plugin into the class" do
28
+ model_class.new.should respond_to(:validates_presence)
29
+ end
30
+
31
+ it "should parse constraint validations when loading plugin" do
32
+ @c = model_class
33
+ @db.sqls.should == ["SELECT * FROM sequel_constraint_validations"]
34
+ @db.constraint_validations.should == {'items'=>[[:validates_presence, :name]]}
35
+ @c.constraint_validations.should == [[:validates_presence, :name]]
36
+ end
37
+
38
+ it "should parse constraint validations with a custom constraint validations table" do
39
+ c = Class.new(Sequel::Model(@db[:items]))
40
+ @db.sqls
41
+ c.plugin :constraint_validations, :constraint_validations_table=>:foo
42
+ @db.sqls.should == ["SELECT * FROM foo"]
43
+ @db.constraint_validations.should == {'items'=>[[:validates_presence, :name]]}
44
+ c.constraint_validations.should == [[:validates_presence, :name]]
45
+ end
46
+
47
+ it "should populate constraint_validations when subclassing" do
48
+ c = Class.new(Sequel::Model(@db))
49
+ c.plugin :constraint_validations
50
+ @db.sqls.should == ["SELECT * FROM sequel_constraint_validations"]
51
+ sc = Class.new(c)
52
+ sc.set_dataset @ds
53
+ @db.sqls.should == []
54
+ sc.constraint_validations.should == [[:validates_presence, :name]]
55
+ end
56
+
57
+ it "should populate constraint_validations when changing the model's dataset" do
58
+ c = Class.new(Sequel::Model(@db[:foo]))
59
+ c.columns :name
60
+ @db.sqls
61
+ c.plugin :constraint_validations
62
+ @db.sqls.should == ["SELECT * FROM sequel_constraint_validations"]
63
+ sc = Class.new(c)
64
+ sc.set_dataset @ds
65
+ @db.sqls.should == []
66
+ sc.constraint_validations.should == [[:validates_presence, :name]]
67
+ end
68
+
69
+ it "should reparse constraint validations when changing the model's database" do
70
+ c = Class.new(Sequel::Model(@ds2))
71
+ c.plugin :constraint_validations
72
+ @db.sqls.should == ["SELECT * FROM sequel_constraint_validations"]
73
+ sc = Class.new(c)
74
+ sc.set_dataset @ds
75
+ @db.sqls.should == ["SELECT * FROM sequel_constraint_validations"]
76
+ sc.constraint_validations.should == [[:validates_presence, :name]]
77
+ end
78
+
79
+ it "should reparse constraint validations when changing the model's database with a custom constraint validations table" do
80
+ c = Class.new(Sequel::Model(@ds2))
81
+ c.plugin :constraint_validations, :constraint_validations_table=>:foo
82
+ @db.sqls.should == ["SELECT * FROM foo"]
83
+ sc = Class.new(c)
84
+ sc.set_dataset @ds
85
+ @db.sqls.should == ["SELECT * FROM foo"]
86
+ sc.constraint_validations.should == [[:validates_presence, :name]]
87
+ end
88
+
89
+ it "should correctly retrieve :message option from constraint validations table" do
90
+ model_class(:message=>'foo').constraint_validations.should == [[:validates_presence, :name, {:message=>'foo'}]]
91
+ end
92
+
93
+ it "should correctly retrieve :allow_nil option from constraint validations table" do
94
+ model_class(:allow_nil=>true).constraint_validations.should == [[:validates_presence, :name, {:allow_nil=>true}]]
95
+ end
96
+
97
+ it "should handle presence validation" do
98
+ model_class(:validation_type=>'presence').constraint_validations.should == [[:validates_presence, :name]]
99
+ end
100
+
101
+ it "should handle exact_length validation" do
102
+ model_class(:validation_type=>'exact_length', :argument=>'5').constraint_validations.should == [[:validates_exact_length, 5, :name]]
103
+ end
104
+
105
+ it "should handle min_length validation" do
106
+ model_class(:validation_type=>'min_length', :argument=>'5').constraint_validations.should == [[:validates_min_length, 5, :name]]
107
+ end
108
+
109
+ it "should handle max_length validation" do
110
+ model_class(:validation_type=>'max_length', :argument=>'5').constraint_validations.should == [[:validates_max_length, 5, :name]]
111
+ end
112
+
113
+ it "should handle length_range validation" do
114
+ model_class(:validation_type=>'length_range', :argument=>'3..5').constraint_validations.should == [[:validates_length_range, 3..5, :name]]
115
+ end
116
+
117
+ it "should handle length_range validation with an exclusive end" do
118
+ model_class(:validation_type=>'length_range', :argument=>'3...5').constraint_validations.should == [[:validates_length_range, 3...5, :name]]
119
+ end
120
+
121
+ it "should handle format validation" do
122
+ model_class(:validation_type=>'format', :argument=>'^foo.*').constraint_validations.should == [[:validates_format, /^foo.*/, :name]]
123
+ end
124
+
125
+ it "should handle format validation with case insensitive format" do
126
+ model_class(:validation_type=>'iformat', :argument=>'^foo.*').constraint_validations.should == [[:validates_format, /^foo.*/i, :name]]
127
+ end
128
+
129
+ it "should handle includes validation with array of strings" do
130
+ model_class(:validation_type=>'includes_str_array', :argument=>'a,b,c').constraint_validations.should == [[:validates_includes, %w'a b c', :name]]
131
+ end
132
+
133
+ it "should handle includes validation with array of integers" do
134
+ model_class(:validation_type=>'includes_int_array', :argument=>'1,2,3').constraint_validations.should == [[:validates_includes, [1, 2, 3], :name]]
135
+ end
136
+
137
+ it "should handle includes validation with inclusive range of integers" do
138
+ model_class(:validation_type=>'includes_int_range', :argument=>'3..5').constraint_validations.should == [[:validates_includes, 3..5, :name]]
139
+ end
140
+
141
+ it "should handle includes validation with exclusive range of integers" do
142
+ model_class(:validation_type=>'includes_int_range', :argument=>'3...5').constraint_validations.should == [[:validates_includes, 3...5, :name]]
143
+ end
144
+
145
+ it "should handle like validation" do
146
+ model_class(:validation_type=>'like', :argument=>'foo').constraint_validations.should == [[:validates_format, /\Afoo\z/, :name]]
147
+ end
148
+
149
+ it "should handle ilike validation" do
150
+ model_class(:validation_type=>'ilike', :argument=>'foo').constraint_validations.should == [[:validates_format, /\Afoo\z/i, :name]]
151
+ end
152
+
153
+ it "should handle like validation with % metacharacter" do
154
+ model_class(:validation_type=>'like', :argument=>'%foo%').constraint_validations.should == [[:validates_format, /\A.*foo.*\z/, :name]]
155
+ end
156
+
157
+ it "should handle like validation with %% metacharacter" do
158
+ model_class(:validation_type=>'like', :argument=>'%%foo%%').constraint_validations.should == [[:validates_format, /\A%foo%\z/, :name]]
159
+ end
160
+
161
+ it "should handle like validation with _ metacharacter" do
162
+ model_class(:validation_type=>'like', :argument=>'f_o').constraint_validations.should == [[:validates_format, /\Af.o\z/, :name]]
163
+ end
164
+
165
+ it "should handle like validation with Regexp metacharacter" do
166
+ model_class(:validation_type=>'like', :argument=>'\wfoo\d').constraint_validations.should == [[:validates_format, /\A\\wfoo\\d\z/, :name]]
167
+ end
168
+
169
+ it "should handle unique validation" do
170
+ model_class(:validation_type=>'unique').constraint_validations.should == [[:validates_unique, [:name]]]
171
+ end
172
+
173
+ it "should handle unique validation with multiple columns" do
174
+ model_class(:validation_type=>'unique', :column=>'name,id').constraint_validations.should == [[:validates_unique, [:name, :id]]]
175
+ end
176
+
177
+ it "should used parsed constraint validations when validating" do
178
+ o = model_class.new
179
+ o.valid?.should == false
180
+ o.errors.full_messages.should == ['name is not present']
181
+ end
182
+
183
+ it "should handle a table name specified as SQL::Identifier" do
184
+ set_fetch(:table=>'sch__items')
185
+ c = Class.new(Sequel::Model(@db[Sequel.identifier(:sch__items)]))
186
+ c.plugin :constraint_validations
187
+ c.constraint_validations.should == [[:validates_presence, :name]]
188
+ end
189
+
190
+ it "should handle a table name specified as SQL::QualifiedIdentifier" do
191
+ set_fetch(:table=>'sch.items')
192
+ c = Class.new(Sequel::Model(@db[Sequel.qualify(:sch, :items)]))
193
+ c.plugin :constraint_validations
194
+ c.constraint_validations.should == [[:validates_presence, :name]]
195
+ end
196
+ end
@@ -0,0 +1,316 @@
1
+ require File.join(File.dirname(File.expand_path(__FILE__)), "spec_helper")
2
+
3
+ describe "constraint_validations extension" do
4
+ def parse_insert(s)
5
+ m = /\AINSERT INTO sequel_constraint_validations \((.*)\) VALUES \((.*)\)\z/.match(s)
6
+ Hash[*m[1].split(', ').map{|v| v.to_sym}.zip(m[2].split(', ').map{|v| parse_insert_value(v)}).reject{|k, v| v.nil?}.flatten]
7
+ end
8
+
9
+ def parse_insert_value(s)
10
+ case s
11
+ when 'NULL'
12
+ nil
13
+ when /\A'(.*)'\z/
14
+ $1
15
+ else
16
+ raise Sequel::Error, "unhandled insert value: #{s.inspect}"
17
+ end
18
+ end
19
+
20
+ before do
21
+ @db = Sequel.mock
22
+ @db.extend(Module.new{attr_writer :schema; def schema(table, *) execute("parse schema for #{table}"); @schema; end})
23
+ @db.extension(:constraint_validations)
24
+ end
25
+
26
+ it "should allow creating the sequel_constraint_validations table" do
27
+ @db.create_constraint_validations_table
28
+ @db.sqls.should == ["CREATE TABLE sequel_constraint_validations (table varchar(255) NOT NULL, constraint_name varchar(255), validation_type varchar(255) NOT NULL, column varchar(255) NOT NULL, argument varchar(255), message varchar(255), allow_nil boolean)"]
29
+ end
30
+
31
+ it "should allow creating the sequel_constraint_validations table with a non-default table name" do
32
+ @db.constraint_validations_table = :foo
33
+ @db.create_constraint_validations_table
34
+ @db.sqls.should == ["CREATE TABLE foo (table varchar(255) NOT NULL, constraint_name varchar(255), validation_type varchar(255) NOT NULL, column varchar(255) NOT NULL, argument varchar(255), message varchar(255), allow_nil boolean)"]
35
+ end
36
+
37
+ it "should allow dropping the sequel_constraint_validations table" do
38
+ @db.drop_constraint_validations_table
39
+ @db.sqls.should == ["DROP TABLE sequel_constraint_validations"]
40
+ end
41
+
42
+ it "should allow dropping the sequel_constraint_validations table with a non-default table name" do
43
+ @db.constraint_validations_table = :foo
44
+ @db.drop_constraint_validations_table
45
+ @db.sqls.should == ["DROP TABLE foo"]
46
+ end
47
+
48
+ it "should allow dropping validations for a given table" do
49
+ @db.drop_constraint_validations_for(:table=>:foo)
50
+ @db.sqls.should == ["DELETE FROM sequel_constraint_validations WHERE (table = 'foo')"]
51
+ end
52
+
53
+ it "should allow dropping validations for a given table and column" do
54
+ @db.drop_constraint_validations_for(:table=>:foo, :column=>:bar)
55
+ @db.sqls.should == ["DELETE FROM sequel_constraint_validations WHERE ((table = 'foo') AND (column = 'bar'))"]
56
+ end
57
+
58
+ it "should allow dropping validations for a given table and constraint" do
59
+ @db.drop_constraint_validations_for(:table=>:foo, :constraint=>:bar)
60
+ @db.sqls.should == ["DELETE FROM sequel_constraint_validations WHERE ((table = 'foo') AND (constraint_name = 'bar'))"]
61
+ end
62
+
63
+ it "should allow dropping validations for a non-default constraint_validations table" do
64
+ @db.constraint_validations_table = :cv
65
+ @db.drop_constraint_validations_for(:table=>:foo)
66
+ @db.sqls.should == ["DELETE FROM cv WHERE (table = 'foo')"]
67
+ end
68
+
69
+ it "should raise an error without deleting if attempting to drop validations with table, column, or constraint" do
70
+ proc{@db.drop_constraint_validations_for({})}.should raise_error(Sequel::Error)
71
+ @db.sqls.should == []
72
+ end
73
+
74
+ it "should allow adding constraint validations via create_table validate" do
75
+ @db.create_table(:foo){String :name; validate{presence :name}}
76
+ sqls = @db.sqls
77
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
78
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (trim(name) != '')))"]
79
+ end
80
+
81
+ it "should allow adding constraint validations via alter_table validate" do
82
+ @db.schema = [[:name, {:type=>:string}]]
83
+ @db.alter_table(:foo){validate{presence :name}}
84
+ sqls = @db.sqls
85
+ parse_insert(sqls.slice!(2)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
86
+ sqls.should == ["parse schema for foo", "BEGIN", "COMMIT", "ALTER TABLE foo ADD CHECK ((name IS NOT NULL) AND (trim(name) != ''))"]
87
+ end
88
+
89
+ it "should handle :message option when adding validations" do
90
+ @db.create_table(:foo){String :name; validate{presence :name, :message=>'not there'}}
91
+ sqls = @db.sqls
92
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo", :message=>'not there'}
93
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (trim(name) != '')))"]
94
+ end
95
+
96
+ it "should handle :allow_nil option when adding validations" do
97
+ @db.create_table(:foo){String :name; validate{presence :name, :allow_nil=>true}}
98
+ sqls = @db.sqls
99
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo", :allow_nil=>'t'}
100
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK (trim(name) != ''))"]
101
+ end
102
+
103
+ it "should handle :name option when adding validations" do
104
+ @db.create_table(:foo){String :name; validate{presence :name, :name=>'cons'}}
105
+ sqls = @db.sqls
106
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo", :constraint_name=>'cons'}
107
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CONSTRAINT cons CHECK ((name IS NOT NULL) AND (trim(name) != '')))"]
108
+ end
109
+
110
+ it "should handle multiple columns when adding validations" do
111
+ @db.create_table(:foo){String :name; String :bar; validate{presence [:name, :bar]}}
112
+ sqls = @db.sqls
113
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
114
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"bar", :table=>"foo"}
115
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), bar varchar(255), CHECK ((name IS NOT NULL) AND (bar IS NOT NULL) AND (trim(name) != '') AND (trim(bar) != '')))"]
116
+ end
117
+
118
+ it "should handle presence validation on non-String columns" do
119
+ @db.create_table(:foo){Integer :name; validate{presence :name}}
120
+ sqls = @db.sqls
121
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
122
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name integer, CHECK (name IS NOT NULL))"]
123
+
124
+ @db.schema = [[:name, {:type=>:integer}]]
125
+ @db.alter_table(:foo){validate{presence :name}}
126
+ sqls = @db.sqls
127
+ parse_insert(sqls.slice!(2)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
128
+ sqls.should == ["parse schema for foo", "BEGIN", "COMMIT", "ALTER TABLE foo ADD CHECK (name IS NOT NULL)"]
129
+ end
130
+
131
+ it "should handle presence validation on Oracle with IS NOT NULL instead of != ''" do
132
+ @db = Sequel.mock(:host=>'oracle')
133
+ @db.extension(:constraint_validations)
134
+ @db.create_table(:foo){String :name; validate{presence :name}}
135
+ sqls = @db.sqls
136
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo"}
137
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (trim(name) IS NOT NULL)))"]
138
+ end
139
+
140
+ it "should assume column is not a String if it can't determine the type" do
141
+ @db.create_table(:foo){Integer :name; validate{presence :bar}}
142
+ sqls = @db.sqls
143
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"bar", :table=>"foo"}
144
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name integer, CHECK (bar IS NOT NULL))"]
145
+
146
+ @db.schema = [[:name, {:type=>:integer}]]
147
+ @db.alter_table(:foo){validate{presence :bar}}
148
+ sqls = @db.sqls
149
+ parse_insert(sqls.slice!(2)).should == {:validation_type=>"presence", :column=>"bar", :table=>"foo"}
150
+ sqls.should == ["parse schema for foo", "BEGIN", "COMMIT", "ALTER TABLE foo ADD CHECK (bar IS NOT NULL)"]
151
+ end
152
+
153
+ it "should handle presence validation on non-String columns with :allow_nil option" do
154
+ @db.create_table(:foo){Integer :name; validate{presence :name, :allow_nil=>true}}
155
+ sqls = @db.sqls
156
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"foo", :allow_nil=>'t'}
157
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name integer)"]
158
+ end
159
+
160
+ it "should support :exact_length constraint validation" do
161
+ @db.create_table(:foo){String :name; validate{exact_length 5, :name}}
162
+ sqls = @db.sqls
163
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"exact_length", :column=>"name", :table=>"foo", :argument=>'5'}
164
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (char_length(name) = 5)))"]
165
+ end
166
+
167
+ it "should support :min_length constraint validation" do
168
+ @db.create_table(:foo){String :name; validate{min_length 5, :name}}
169
+ sqls = @db.sqls
170
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"min_length", :column=>"name", :table=>"foo", :argument=>'5'}
171
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (char_length(name) >= 5)))"]
172
+ end
173
+
174
+ it "should support :max_length constraint validation" do
175
+ @db.create_table(:foo){String :name; validate{max_length 5, :name}}
176
+ sqls = @db.sqls
177
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"max_length", :column=>"name", :table=>"foo", :argument=>'5'}
178
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (char_length(name) <= 5)))"]
179
+ end
180
+
181
+ it "should support :length_range constraint validation" do
182
+ @db.create_table(:foo){String :name; validate{length_range 3..5, :name}}
183
+ sqls = @db.sqls
184
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"length_range", :column=>"name", :table=>"foo", :argument=>'3..5'}
185
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (char_length(name) >= 3) AND (char_length(name) <= 5)))"]
186
+
187
+ @db.create_table(:foo){String :name; validate{length_range 3...5, :name}}
188
+ sqls = @db.sqls
189
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"length_range", :column=>"name", :table=>"foo", :argument=>'3...5'}
190
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (char_length(name) >= 3) AND (char_length(name) < 5)))"]
191
+ end
192
+
193
+ it "should support :format constraint validation" do
194
+ @db = Sequel.mock(:host=>'postgres')
195
+ @db.extension(:constraint_validations)
196
+ @db.create_table(:foo){String :name; validate{format /^foo.*/, :name}}
197
+ sqls = @db.sqls
198
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"format", :column=>"name", :table=>"foo", :argument=>'^foo.*'}
199
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name text, CHECK ((name IS NOT NULL) AND (name ~ '^foo.*')))"]
200
+ end
201
+
202
+ it "should support :format constraint validation with case insensitive format" do
203
+ @db = Sequel.mock(:host=>'postgres')
204
+ @db.extension(:constraint_validations)
205
+ @db.create_table(:foo){String :name; validate{format /^foo.*/i, :name}}
206
+ sqls = @db.sqls
207
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"iformat", :column=>"name", :table=>"foo", :argument=>'^foo.*'}
208
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name text, CHECK ((name IS NOT NULL) AND (name ~* '^foo.*')))"]
209
+ end
210
+
211
+ it "should support :includes constraint validation with an array of strings" do
212
+ @db.create_table(:foo){String :name; validate{includes %w'a b c', :name}}
213
+ sqls = @db.sqls
214
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"includes_str_array", :column=>"name", :table=>"foo", :argument=>'a,b,c'}
215
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name IN ('a', 'b', 'c'))))"]
216
+ end
217
+
218
+ it "should support :includes constraint validation with an array of integers" do
219
+ @db.create_table(:foo){String :name; validate{includes [1, 2, 3], :name}}
220
+ sqls = @db.sqls
221
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"includes_int_array", :column=>"name", :table=>"foo", :argument=>'1,2,3'}
222
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name IN (1, 2, 3))))"]
223
+ end
224
+
225
+ it "should support :includes constraint validation with a inclusive range of integers" do
226
+ @db.create_table(:foo){String :name; validate{includes 3..5, :name}}
227
+ sqls = @db.sqls
228
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"includes_int_range", :column=>"name", :table=>"foo", :argument=>'3..5'}
229
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name >= 3) AND (name <= 5)))"]
230
+ end
231
+
232
+ it "should support :includes constraint validation with a exclusive range of integers" do
233
+ @db.create_table(:foo){String :name; validate{includes 3...5, :name}}
234
+ sqls = @db.sqls
235
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"includes_int_range", :column=>"name", :table=>"foo", :argument=>'3...5'}
236
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name >= 3) AND (name < 5)))"]
237
+ end
238
+
239
+ it "should support :like constraint validation" do
240
+ @db.create_table(:foo){String :name; validate{like 'foo%', :name}}
241
+ sqls = @db.sqls
242
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"like", :column=>"name", :table=>"foo", :argument=>'foo%'}
243
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name LIKE 'foo%')))"]
244
+ end
245
+
246
+ it "should support :ilike constraint validation" do
247
+ @db.create_table(:foo){String :name; validate{ilike 'foo%', :name}}
248
+ sqls = @db.sqls
249
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"ilike", :column=>"name", :table=>"foo", :argument=>'foo%'}
250
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), CHECK ((name IS NOT NULL) AND (name ILIKE 'foo%')))"]
251
+ end
252
+
253
+ it "should support :unique constraint validation" do
254
+ @db.create_table(:foo){String :name; validate{unique :name}}
255
+ sqls = @db.sqls
256
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"unique", :column=>"name", :table=>"foo"}
257
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), UNIQUE (name))"]
258
+ end
259
+
260
+ it "should support :unique constraint validation with multiple columns" do
261
+ @db.create_table(:foo){String :name; Integer :id; validate{unique [:name, :id]}}
262
+ sqls = @db.sqls
263
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"unique", :column=>"name,id", :table=>"foo"}
264
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE foo (name varchar(255), id integer, UNIQUE (name, id))"]
265
+ end
266
+
267
+ it "should support :unique constraint validation in alter_table" do
268
+ @db.alter_table(:foo){validate{unique :name}}
269
+ sqls = @db.sqls
270
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"unique", :column=>"name", :table=>"foo"}
271
+ sqls.should == ["BEGIN", "COMMIT", "ALTER TABLE foo ADD UNIQUE (name)"]
272
+ end
273
+
274
+ it "should drop constraints and validations when dropping a constraint validation" do
275
+ @db.alter_table(:foo){String :name; validate{drop :bar}}
276
+ @db.sqls.should == ["DELETE FROM sequel_constraint_validations WHERE ((table, constraint_name) IN (('foo', 'bar')))", "ALTER TABLE foo DROP CONSTRAINT bar"]
277
+ end
278
+
279
+ it "should raise an error if attempting to validate inclusion with a range of non-integers" do
280
+ proc{@db.create_table(:foo){String :name; validate{includes 'a'..'z', :name}}}.should raise_error(Sequel::Error)
281
+ end
282
+
283
+ it "should raise an error if attempting to validate inclusion with a range of non-integers or strings" do
284
+ proc{@db.create_table(:foo){String :name; validate{includes [1.0, 2.0], :name}}}.should raise_error(Sequel::Error)
285
+ end
286
+
287
+ it "should raise an error if attempting to validate inclusion with a unsupported object" do
288
+ proc{@db.create_table(:foo){String :name; validate{includes 'a', :name}}}.should raise_error(Sequel::Error)
289
+ end
290
+
291
+ it "should raise an error if attempting to drop a constraint validation in a create_table generator" do
292
+ proc{@db.create_table(:foo){String :name; validate{drop :foo}}}.should raise_error(Sequel::Error)
293
+ end
294
+
295
+ it "should raise an error if attempting to drop a constraint validation without a name" do
296
+ proc{@db.alter_table(:foo){String :name; validate{drop nil}}}.should raise_error(Sequel::Error)
297
+ end
298
+
299
+ it "should raise an error if attempting attempting to process a constraint validation with an unsupported type" do
300
+ proc{@db.alter_table(:foo){String :name; validations << {:type=>:foo}}}.should raise_error(Sequel::Error)
301
+ end
302
+
303
+ it "should allow adding constraint validations for tables specified as a SQL::Identifier" do
304
+ @db.create_table(Sequel.identifier(:sch__foo)){String :name; validate{presence :name}}
305
+ sqls = @db.sqls
306
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"sch__foo"}
307
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE sch__foo (name varchar(255), CHECK ((name IS NOT NULL) AND (trim(name) != '')))"]
308
+ end
309
+
310
+ it "should allow adding constraint validations for tables specified as a SQL::QualifiedIdentifier" do
311
+ @db.create_table(Sequel.qualify(:sch, :foo)){String :name; validate{presence :name}}
312
+ sqls = @db.sqls
313
+ parse_insert(sqls.slice!(1)).should == {:validation_type=>"presence", :column=>"name", :table=>"sch.foo"}
314
+ sqls.should == ["BEGIN", "COMMIT", "CREATE TABLE sch.foo (name varchar(255), CHECK ((name IS NOT NULL) AND (trim(name) != '')))"]
315
+ end
316
+ end