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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c54c89523b089da35cf81ac327720dc7747477e4
4
- data.tar.gz: b6c0284892866070449337290b70c5f481bc1c6b
3
+ metadata.gz: 090f45578b6802cdbdba1069b5d37a9a36936e53
4
+ data.tar.gz: 8804b23a035aaa84fa334caa69fa862971bf0ffc
5
5
  SHA512:
6
- metadata.gz: fa9279114bf9c478f74f52c6753ae91d4829fdf0ba4e076d20981a6c057b016da7de4dad5598ea7a541d3dcfb31d44e93017fe8fe7bbed9fa923b55c21677f2e
7
- data.tar.gz: 8efe103e02f9fdceaea9aea6e8094b9b7a224d605f30eacfad0dffb08221ba48394e96c3adbabea5679d626a0d9d15c01fa0471b0240ebfb95f7d5c4fc5a2beb
6
+ metadata.gz: 48f4ce81a05aa5798f73b24997eea4920027318e4c48cdae4824c806a3cee5c84688058f204af0bf074b29360037108939bca38c0389b52f18cd3d3fd52ab941
7
+ data.tar.gz: 33774245ec628ff1b463559346e3e11699e302b4a41417676caf42ad08b5a8f07f3f613fb0d724c41edd883e7b51b90a660889e75cf4134d9e9526b909c8c438
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module DatabaseValidations
3
- VERSION = "0.1.2"
3
+ VERSION = "0.1.3"
4
4
  end
5
5
  end
@@ -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
@@ -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.string :string, limit: 40
8
- t.text :tinytext, limit: 255
9
- t.binary :blob
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 test_bounded_string_fields_silently_loses_data_when_strict_mode_is_disabled
24
- fitting_string = 'ü' * 40
25
- assert Unicorn.columns_hash['string'].limit >= fitting_string.length
26
- unicorn = Unicorn.create(string: fitting_string)
27
- assert_equal fitting_string, unicorn.reload.string
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
- overflowing_string = 'ü' * 41
30
- assert Unicorn.columns_hash['string'].limit < overflowing_string.length
31
- unicorn = Unicorn.create(string: overflowing_string)
32
- assert unicorn.reload.string != overflowing_string
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 test_text_field_silently_loses_data_when_strict_mode_is_disabled
36
- fitting_tinytext = '写' * 85
37
- assert_equal 255, fitting_tinytext.bytesize
38
- unicorn = Unicorn.create(tinytext: fitting_tinytext) # <= 255, the TINYTEXT limit
39
- assert_equal fitting_tinytext, unicorn.reload.tinytext
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
- overflowing_tinytext = 'ü' * 128
42
- assert_equal 256, overflowing_tinytext.bytesize # > 255, the TINYTEXT limit
43
- unicorn = Unicorn.create(tinytext: overflowing_tinytext)
44
- assert unicorn.reload.tinytext != overflowing_tinytext
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 test_binary_field_silently_loses_data_when_strict_mode_is_disabled
48
- fitting_blob = [].pack('x65535')
49
- assert_equal 65535, fitting_blob.bytesize
50
- unicorn = Unicorn.create(blob: fitting_blob) # <= 65535, the BLOB limit
51
- assert_equal fitting_blob, unicorn.reload.blob
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
- overflowing_blob = [].pack('x65536')
54
- assert_equal 65536, overflowing_blob.bytesize # > 65535, the BLOB limit
55
- unicorn = Unicorn.create(blob: overflowing_blob)
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 test_unchecked_utf8mb3_field_silently_loses_data_when_strict_mode_is_disabled
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
- unicorn = Unicorn.create(string: emoji)
64
- assert unicorn.reload.string != emoji
105
+ assert_data_loss Unicorn.new(string: emoji)
65
106
  end
66
107
  end
@@ -2,5 +2,5 @@ test:
2
2
  adapter: mysql2
3
3
  database: database_validations
4
4
  username: travis
5
- encoding: utf8
5
+ encoding: utf8mb4
6
6
  strict: false
@@ -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, limit: 40
8
- t.text :tinytext, limit: 255
9
- t.binary :varbinary, limit: 255
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, default: 0
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 test_should_not_create_a_validor_for_a_utf8mb4_field
92
- assert Bar.new(mb4_string: '💩').valid?
93
- Bar._validators[:mb4_string].first.attribute_validators(Bar, :mb4_string).empty?
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
@@ -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.2
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-16 00:00:00.000000000 Z
11
+ date: 2014-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord