sequel 3.38.0 → 3.39.0

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