activerecord-databasevalidations 0.1.2 → 0.1.3
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.
- 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
|