activerecord-databasevalidations 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/active_record/database_validations/version.rb +1 -1
- data/lib/active_record/validations/database_constraints.rb +25 -1
- data/test/data_loss_test.rb +74 -33
- data/test/database.yml +1 -1
- data/test/database_constraints_validator_test.rb +115 -7
- data/test/test_helper.rb +22 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 090f45578b6802cdbdba1069b5d37a9a36936e53
|
4
|
+
data.tar.gz: 8804b23a035aaa84fa334caa69fa862971bf0ffc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48f4ce81a05aa5798f73b24997eea4920027318e4c48cdae4824c806a3cee5c84688058f204af0bf074b29360037108939bca38c0389b52f18cd3d3fd52ab941
|
7
|
+
data.tar.gz: 33774245ec628ff1b463559346e3e11699e302b4a41417676caf42ad08b5a8f07f3f613fb0d724c41edd883e7b51b90a660889e75cf4134d9e9526b909c8c438
|
@@ -23,7 +23,7 @@ module ActiveRecord
|
|
23
23
|
|
24
24
|
attr_reader :constraints
|
25
25
|
|
26
|
-
VALID_CONSTRAINTS = Set[:size, :basic_multilingual_plane, :not_null]
|
26
|
+
VALID_CONSTRAINTS = Set[:size, :basic_multilingual_plane, :not_null, :range]
|
27
27
|
|
28
28
|
def initialize(options = {})
|
29
29
|
@constraints = Set.new(Array.wrap(options[:in]) + Array.wrap(options[:with]))
|
@@ -60,6 +60,29 @@ module ActiveRecord
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
def range_validator(klass, column)
|
64
|
+
return unless constraints.include?(:range)
|
65
|
+
return unless column.number?
|
66
|
+
|
67
|
+
unsigned = column.sql_type =~ / unsigned\z/
|
68
|
+
case column.type
|
69
|
+
when :decimal
|
70
|
+
args = { attributes: [column.name.to_sym], class: klass, allow_nil: true }
|
71
|
+
args[:less_than] = maximum = 10 ** (column.precision - column.scale)
|
72
|
+
if unsigned
|
73
|
+
args[:greater_than_or_equal_to] = 0
|
74
|
+
else
|
75
|
+
args[:greater_than] = 0 - maximum
|
76
|
+
end
|
77
|
+
ActiveModel::Validations::NumericalityValidator.new(args)
|
78
|
+
|
79
|
+
when :integer
|
80
|
+
maximum = unsigned ? 1 << (column.limit * 8) : 1 << (column.limit * 8 - 1)
|
81
|
+
minimum = unsigned ? 0 : 0 - maximum
|
82
|
+
ActiveModel::Validations::NumericalityValidator.new(attributes: [column.name.to_sym], class: klass, greater_than_or_equal_to: minimum, less_than: maximum, allow_nil: true, only_integer: true)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
63
86
|
def basic_multilingual_plane_validator(klass, column)
|
64
87
|
return unless constraints.include?(:basic_multilingual_plane)
|
65
88
|
return unless column.text? && column.collation =~ /\Autf8(?:mb3)?_/
|
@@ -74,6 +97,7 @@ module ActiveRecord
|
|
74
97
|
not_null_validator(klass, column),
|
75
98
|
size_validator(klass, column),
|
76
99
|
basic_multilingual_plane_validator(klass, column),
|
100
|
+
range_validator(klass, column),
|
77
101
|
].compact
|
78
102
|
end
|
79
103
|
end
|
data/test/data_loss_test.rb
CHANGED
@@ -4,9 +4,19 @@ require 'test_helper'
|
|
4
4
|
|
5
5
|
ActiveRecord::Migration.suppress_messages do
|
6
6
|
ActiveRecord::Migration.create_table("unicorns", force: true, options: "CHARACTER SET utf8mb3") do |t|
|
7
|
-
t.
|
8
|
-
t.
|
9
|
-
t.
|
7
|
+
t.column :string, "VARCHAR(40)"
|
8
|
+
t.column :tinytext, "TINYTEXT"
|
9
|
+
t.column :blob, "BLOB"
|
10
|
+
|
11
|
+
t.column :decimal, "DECIMAL(10, 2)"
|
12
|
+
t.column :unsigned_decimal, "DECIMAL(5, 3) UNSIGNED"
|
13
|
+
|
14
|
+
t.column :tinyint, "TINYINT"
|
15
|
+
t.column :smallint, "SMALLINT"
|
16
|
+
t.column :mediumint, "MEDIUMINT"
|
17
|
+
t.column :int, "INT"
|
18
|
+
t.column :bigint, "BIGINT"
|
19
|
+
t.column :unsigned_tinyint, "TINYINT UNSIGNED"
|
10
20
|
end
|
11
21
|
end
|
12
22
|
|
@@ -15,52 +25,83 @@ class Unicorn < ActiveRecord::Base
|
|
15
25
|
end
|
16
26
|
|
17
27
|
class DataLossTest < Minitest::Test
|
28
|
+
include DataLossAssertions
|
29
|
+
|
18
30
|
def test_strict_mode_is_disabled
|
19
31
|
refute Unicorn.connection.show_variable(:sql_mode).include?('STRICT_ALL_TABLES')
|
20
32
|
refute Unicorn.connection.show_variable(:sql_mode).include?('STRICT_TRANS_TABLES')
|
21
33
|
end
|
22
34
|
|
23
|
-
def
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
35
|
+
def test_decimal_silently_changes_out_of_bound_values
|
36
|
+
maximum = BigDecimal.new(10 ** (Unicorn.columns_hash['decimal'].precision - Unicorn.columns_hash['decimal'].scale))
|
37
|
+
delta = BigDecimal.new(10 ** -(Unicorn.columns_hash['decimal'].scale), Unicorn.columns_hash['decimal'].precision)
|
38
|
+
|
39
|
+
refute_data_loss Unicorn.new(decimal: maximum - delta)
|
40
|
+
assert_data_loss Unicorn.new(decimal: maximum)
|
41
|
+
refute_data_loss Unicorn.new(decimal: 0 - maximum + delta)
|
42
|
+
assert_data_loss Unicorn.new(decimal: 0 - maximum)
|
28
43
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
44
|
+
|
45
|
+
maximum = BigDecimal.new(10 ** (Unicorn.columns_hash['unsigned_decimal'].precision - Unicorn.columns_hash['unsigned_decimal'].scale))
|
46
|
+
delta = BigDecimal.new(10 ** -(Unicorn.columns_hash['unsigned_decimal'].scale), Unicorn.columns_hash['unsigned_decimal'].precision)
|
47
|
+
|
48
|
+
refute_data_loss Unicorn.new(unsigned_decimal: maximum - delta)
|
49
|
+
assert_data_loss Unicorn.new(unsigned_decimal: maximum)
|
50
|
+
refute_data_loss Unicorn.new(unsigned_decimal: 0)
|
51
|
+
assert_data_loss Unicorn.new(unsigned_decimal: 0 - delta)
|
33
52
|
end
|
34
53
|
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
54
|
+
def test_integers_silently_change_value_outside_of_range
|
55
|
+
refute_data_loss Unicorn.new(tinyint: 127)
|
56
|
+
refute_data_loss Unicorn.new(tinyint: -128)
|
57
|
+
assert_data_loss Unicorn.new(tinyint: 128)
|
58
|
+
assert_data_loss Unicorn.new(tinyint: -129)
|
59
|
+
|
60
|
+
refute_data_loss Unicorn.new(smallint: 32_767)
|
61
|
+
refute_data_loss Unicorn.new(smallint: -32_768)
|
62
|
+
assert_data_loss Unicorn.new(smallint: 32_768)
|
63
|
+
assert_data_loss Unicorn.new(smallint: -32_769)
|
40
64
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
65
|
+
refute_data_loss Unicorn.new(mediumint: 8_388_607)
|
66
|
+
refute_data_loss Unicorn.new(mediumint: -8_388_608)
|
67
|
+
assert_data_loss Unicorn.new(mediumint: 8_388_608)
|
68
|
+
assert_data_loss Unicorn.new(mediumint: -8_388_609)
|
69
|
+
|
70
|
+
refute_data_loss Unicorn.new(int: 2_147_483_647)
|
71
|
+
refute_data_loss Unicorn.new(int: -2_147_483_648)
|
72
|
+
assert_data_loss Unicorn.new(int: 2_147_483_648)
|
73
|
+
assert_data_loss Unicorn.new(int: -2_147_483_649)
|
74
|
+
|
75
|
+
refute_data_loss Unicorn.new(bigint: 9_223_372_036_854_775_807)
|
76
|
+
refute_data_loss Unicorn.new(bigint: -9_223_372_036_854_775_808)
|
77
|
+
assert_data_loss Unicorn.new(bigint: 9_223_372_036_854_775_808)
|
78
|
+
assert_data_loss Unicorn.new(bigint: -9_223_372_036_854_775_809)
|
79
|
+
|
80
|
+
refute_data_loss Unicorn.new(unsigned_tinyint: 255)
|
81
|
+
refute_data_loss Unicorn.new(unsigned_tinyint: 0)
|
82
|
+
assert_data_loss Unicorn.new(unsigned_tinyint: 256)
|
83
|
+
assert_data_loss Unicorn.new(unsigned_tinyint: -1)
|
45
84
|
end
|
46
85
|
|
47
|
-
def
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
86
|
+
def test_varchar_field_silently_drops_characters_when_over_character_limit
|
87
|
+
refute_data_loss Unicorn.new(string: 'ü' * 40)
|
88
|
+
assert_data_loss Unicorn.new(string: 'ü' * 41)
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_text_field_silently_drops_charactars_when_when_over_bytesize_limit
|
92
|
+
refute_data_loss Unicorn.new(tinytext: '写' * 85) # 85 * 3 = 255 bytes => fits
|
93
|
+
assert_data_loss Unicorn.new(tinytext: 'ü' * 128) # 128 * 2 = 256 bytes => doesn't fit :()
|
94
|
+
end
|
52
95
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
assert unicorn.reload.blob != overflowing_blob
|
96
|
+
def test_blob_field_silently_drops_bytes_when_when_over_bytesize_limit
|
97
|
+
refute_data_loss Unicorn.new(blob: [].pack('x65535')) # 65535 is bytesize limit of blob field
|
98
|
+
assert_data_loss Unicorn.new(blob: [].pack('x65536'))
|
57
99
|
end
|
58
100
|
|
59
|
-
def
|
101
|
+
def test_utf8mb3_field_sliently_truncates_strings_after_first_4byte_character
|
60
102
|
emoji = '💩'
|
61
103
|
assert_equal 1, emoji.length
|
62
104
|
assert_equal 4, emoji.bytesize
|
63
|
-
|
64
|
-
assert unicorn.reload.string != emoji
|
105
|
+
assert_data_loss Unicorn.new(string: emoji)
|
65
106
|
end
|
66
107
|
end
|
data/test/database.yml
CHANGED
@@ -4,18 +4,29 @@ require 'test_helper'
|
|
4
4
|
|
5
5
|
ActiveRecord::Migration.suppress_messages do
|
6
6
|
ActiveRecord::Migration.create_table("foos", force: true, options: "CHARACTER SET utf8mb3") do |t|
|
7
|
-
t.string :string,
|
8
|
-
t.text :tinytext,
|
9
|
-
t.binary :varbinary,
|
7
|
+
t.string :string, limit: 40
|
8
|
+
t.text :tinytext, limit: 255
|
9
|
+
t.binary :varbinary, limit: 255
|
10
10
|
t.binary :blob
|
11
11
|
t.text :not_null_text, null: false
|
12
|
-
t.integer :checked, null: false,
|
12
|
+
t.integer :checked, null: false, default: 0
|
13
13
|
t.integer :unchecked, null: false
|
14
14
|
end
|
15
15
|
|
16
16
|
ActiveRecord::Migration.create_table("bars", force: true, options: "CHARACTER SET utf8mb4") do |t|
|
17
17
|
t.string :mb4_string
|
18
18
|
end
|
19
|
+
|
20
|
+
ActiveRecord::Migration.create_table("nums", force: true) do |t|
|
21
|
+
t.column :decimal, "DECIMAL(5,2)"
|
22
|
+
t.column :unsigned_decimal, "DECIMAL(5,2) UNSIGNED"
|
23
|
+
t.column :tinyint, "TINYINT"
|
24
|
+
t.column :smallint, "SMALLINT"
|
25
|
+
t.column :mediumint, "MEDIUMINT"
|
26
|
+
t.column :int, "INT"
|
27
|
+
t.column :bigint, "BIGINT"
|
28
|
+
t.column :unsigned_int, "INT UNSIGNED"
|
29
|
+
end
|
19
30
|
end
|
20
31
|
|
21
32
|
class Foo < ActiveRecord::Base
|
@@ -28,7 +39,13 @@ class Bar < ActiveRecord::Base
|
|
28
39
|
validates :mb4_string, database_constraints: :basic_multilingual_plane
|
29
40
|
end
|
30
41
|
|
42
|
+
class Num < ActiveRecord::Base
|
43
|
+
validates :decimal, :unsigned_decimal, :tinyint, :smallint, :mediumint, :int, :bigint, :unsigned_int, database_constraints: :range
|
44
|
+
end
|
45
|
+
|
31
46
|
class DatabaseConstraintsValidatorTest < Minitest::Test
|
47
|
+
include DataLossAssertions
|
48
|
+
|
32
49
|
def test_argument_validation
|
33
50
|
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: []) }
|
34
51
|
assert_raises(ArgumentError) { Bar.validates(:mb4_string, database_constraints: true) }
|
@@ -88,9 +105,99 @@ class DatabaseConstraintsValidatorTest < Minitest::Test
|
|
88
105
|
refute Foo.new(checked: nil).valid?
|
89
106
|
end
|
90
107
|
|
91
|
-
def
|
92
|
-
assert Bar.
|
93
|
-
Bar.
|
108
|
+
def test_should_not_create_a_validator_for_a_utf8mb4_field
|
109
|
+
assert Bar._validators[:mb4_string].first.attribute_validators(Bar, :mb4_string).empty?
|
110
|
+
emoji = Bar.new(mb4_string: '💩')
|
111
|
+
assert emoji.valid?
|
112
|
+
refute_data_loss emoji
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_decimal_range
|
116
|
+
subvalidators = Num._validators[:decimal].first.attribute_validators(Num, :decimal)
|
117
|
+
assert_equal 1, subvalidators.length
|
118
|
+
assert_kind_of ActiveModel::Validations::NumericalityValidator, subvalidators.first
|
119
|
+
|
120
|
+
inside_upper_bound = Num.new(decimal: '999.99')
|
121
|
+
assert inside_upper_bound.valid?
|
122
|
+
refute_data_loss(inside_upper_bound)
|
123
|
+
|
124
|
+
inside_lower_bound = Num.new(decimal: '-999.99')
|
125
|
+
assert inside_lower_bound.valid?
|
126
|
+
refute_data_loss(inside_lower_bound)
|
127
|
+
|
128
|
+
outside_upper_bound = Num.new(decimal: '1000.00')
|
129
|
+
refute outside_upper_bound.valid?
|
130
|
+
assert_data_loss(outside_upper_bound)
|
131
|
+
|
132
|
+
outside_lower_bound = Num.new(decimal: '-1000.00')
|
133
|
+
refute outside_lower_bound.valid?
|
134
|
+
assert_data_loss(outside_lower_bound)
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_unsigned_decimal_range
|
138
|
+
subvalidators = Num._validators[:unsigned_decimal].first.attribute_validators(Num, :unsigned_decimal)
|
139
|
+
assert_equal 1, subvalidators.length
|
140
|
+
assert_kind_of ActiveModel::Validations::NumericalityValidator, subvalidators.first
|
141
|
+
|
142
|
+
inside_upper_bound = Num.new(unsigned_decimal: '999.99')
|
143
|
+
assert inside_upper_bound.valid?
|
144
|
+
refute_data_loss(inside_upper_bound)
|
145
|
+
|
146
|
+
inside_lower_bound = Num.new(unsigned_decimal: '0.00')
|
147
|
+
assert inside_lower_bound.valid?
|
148
|
+
refute_data_loss(inside_lower_bound)
|
149
|
+
|
150
|
+
outside_upper_bound = Num.new(unsigned_decimal: '1000.00')
|
151
|
+
refute outside_upper_bound.valid?
|
152
|
+
assert_data_loss(outside_upper_bound)
|
153
|
+
|
154
|
+
outside_lower_bound = Num.new(unsigned_decimal: '-0.01')
|
155
|
+
refute outside_lower_bound.valid?
|
156
|
+
assert_data_loss(outside_lower_bound)
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_integer_range
|
160
|
+
subvalidators = Num._validators[:bigint].first.attribute_validators(Num, :bigint)
|
161
|
+
assert_equal 1, subvalidators.length
|
162
|
+
assert_kind_of ActiveModel::Validations::NumericalityValidator, range_validator = subvalidators.first
|
163
|
+
|
164
|
+
inside_upper_bound = Num.new(tinyint: 127)
|
165
|
+
assert inside_upper_bound.valid?
|
166
|
+
refute_data_loss(inside_upper_bound)
|
167
|
+
|
168
|
+
inside_lower_bound = Num.new(smallint: -32_768)
|
169
|
+
assert inside_lower_bound.valid?
|
170
|
+
refute_data_loss(inside_lower_bound)
|
171
|
+
|
172
|
+
outside_upper_bound = Num.new(mediumint: 8_388_608)
|
173
|
+
refute outside_upper_bound.valid?
|
174
|
+
assert_data_loss(outside_upper_bound)
|
175
|
+
|
176
|
+
outside_lower_bound = Num.new(int: -2_147_483_649)
|
177
|
+
refute outside_lower_bound.valid?
|
178
|
+
assert_data_loss(outside_lower_bound)
|
179
|
+
end
|
180
|
+
|
181
|
+
def unsigned_integer_range
|
182
|
+
subvalidators = Num._validators[:unsigned_int].first.attribute_validators(Num, :unsigned_int)
|
183
|
+
assert_equal 1, subvalidators.length
|
184
|
+
assert_kind_of ActiveModel::Validations::NumericalityValidator, range_validator = subvalidators.first
|
185
|
+
|
186
|
+
inside_upper_bound = Num.new(unsigned_int: 4_294_967_295)
|
187
|
+
assert inside_upper_bound.valid?
|
188
|
+
refute_data_loss(inside_upper_bound)
|
189
|
+
|
190
|
+
inside_lower_bound = Num.new(unsigned_int: 0)
|
191
|
+
assert inside_lower_bound.valid?
|
192
|
+
refute_data_loss(inside_lower_bound)
|
193
|
+
|
194
|
+
outside_upper_bound = Num.new(unsigned_int: 4_294_967_296)
|
195
|
+
refute outside_upper_bound.valid?
|
196
|
+
assert_data_loss(outside_upper_bound)
|
197
|
+
|
198
|
+
outside_lower_bound = Num.new(unsigned_int: -1)
|
199
|
+
refute outside_lower_bound.valid?
|
200
|
+
assert_data_loss(outside_lower_bound)
|
94
201
|
end
|
95
202
|
|
96
203
|
def test_error_messages
|
@@ -105,5 +212,6 @@ class DatabaseConstraintsValidatorTest < Minitest::Test
|
|
105
212
|
def test_encoding_craziness
|
106
213
|
foo = Foo.new(tinytext: ('ü' * 128).encode('ISO-8859-15'), string: ('ü' * 40).encode('ISO-8859-15'))
|
107
214
|
assert foo.invalid?
|
215
|
+
assert_data_loss foo
|
108
216
|
end
|
109
217
|
end
|
data/test/test_helper.rb
CHANGED
@@ -9,6 +9,28 @@ require "yaml"
|
|
9
9
|
|
10
10
|
require "active_record/database_validations"
|
11
11
|
|
12
|
+
module DataLossAssertions
|
13
|
+
def assert_data_loss(record)
|
14
|
+
attributes = record.changed
|
15
|
+
provided_values = record.attributes.slice(*attributes)
|
16
|
+
|
17
|
+
record.save!(validate: false)
|
18
|
+
|
19
|
+
persisted_values = record.reload.attributes.slice(*attributes)
|
20
|
+
refute_equal provided_values, persisted_values
|
21
|
+
end
|
22
|
+
|
23
|
+
def refute_data_loss(record)
|
24
|
+
attributes = record.changed
|
25
|
+
provided_values = record.attributes.slice(*attributes)
|
26
|
+
|
27
|
+
record.save!(validate: false)
|
28
|
+
|
29
|
+
persisted_values = record.reload.attributes.slice(*attributes)
|
30
|
+
assert_equal provided_values, persisted_values
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
12
34
|
Minitest::Test = MiniTest::Unit::TestCase unless defined?(MiniTest::Test)
|
13
35
|
|
14
36
|
database_yml = YAML.load_file(File.expand_path('../database.yml', __FILE__))
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-databasevalidations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Willem van Bergen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-10-
|
11
|
+
date: 2014-10-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|