superstore 2.4.4 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +4 -6
  3. data/Gemfile +2 -3
  4. data/{Gemfile-rails4.2 → Gemfile.rails6} +2 -3
  5. data/README.md +4 -34
  6. data/lib/superstore.rb +17 -18
  7. data/lib/superstore/adapters/abstract_adapter.rb +1 -27
  8. data/lib/superstore/adapters/jsonb_adapter.rb +4 -132
  9. data/lib/superstore/associations.rb +6 -1
  10. data/lib/superstore/associations/association.rb +6 -0
  11. data/lib/superstore/associations/association_scope.rb +20 -0
  12. data/lib/superstore/associations/belongs_to.rb +3 -1
  13. data/lib/superstore/associations/has_many.rb +15 -2
  14. data/lib/superstore/associations/reflection.rb +8 -2
  15. data/lib/superstore/attribute_assignment.rb +7 -0
  16. data/lib/superstore/attribute_methods.rb +1 -109
  17. data/lib/superstore/attribute_methods/primary_key.rb +20 -11
  18. data/lib/superstore/attributes.rb +13 -0
  19. data/lib/superstore/base.rb +8 -33
  20. data/lib/superstore/core.rb +7 -65
  21. data/lib/superstore/model_schema.rb +35 -0
  22. data/lib/superstore/persistence.rb +31 -115
  23. data/lib/superstore/railtie.rb +3 -11
  24. data/lib/superstore/relation/scrolling.rb +48 -0
  25. data/lib/superstore/timestamp.rb +13 -0
  26. data/lib/superstore/types.rb +11 -9
  27. data/lib/superstore/types/array_type.rb +3 -7
  28. data/lib/superstore/types/boolean_type.rb +7 -12
  29. data/lib/superstore/types/date_range_type.rb +7 -0
  30. data/lib/superstore/types/date_type.rb +7 -10
  31. data/lib/superstore/types/float_type.rb +3 -11
  32. data/lib/superstore/types/geo_point_type.rb +30 -0
  33. data/lib/superstore/types/integer_range_type.rb +19 -0
  34. data/lib/superstore/types/integer_type.rb +8 -14
  35. data/lib/superstore/types/json_type.rb +1 -1
  36. data/lib/superstore/types/range_type.rb +51 -0
  37. data/lib/superstore/types/string_type.rb +4 -4
  38. data/lib/superstore/types/time_type.rb +10 -8
  39. data/superstore.gemspec +4 -3
  40. data/test/support/jsonb.rb +3 -1
  41. data/test/support/models.rb +8 -5
  42. data/test/test_helper.rb +6 -2
  43. data/test/unit/adapters/adapter_test.rb +1 -3
  44. data/test/unit/associations/belongs_to_test.rb +1 -1
  45. data/test/unit/associations/has_many_test.rb +10 -2
  46. data/test/unit/attribute_methods/dirty_test.rb +8 -19
  47. data/test/unit/attribute_methods/primary_key_test.rb +1 -1
  48. data/test/unit/attribute_methods_test.rb +10 -22
  49. data/test/unit/{attribute_methods/typecasting_test.rb → attributes_test.rb} +13 -39
  50. data/test/unit/base_test.rb +4 -0
  51. data/test/unit/caching_test.rb +1 -1
  52. data/test/unit/callbacks_test.rb +4 -4
  53. data/test/unit/core_test.rb +9 -19
  54. data/test/unit/persistence_test.rb +17 -54
  55. data/test/unit/{scope/batches_test.rb → relation/scrolling_test.rb} +9 -5
  56. data/test/unit/serialization_test.rb +10 -2
  57. data/test/unit/{timestamps_test.rb → timestamp_test.rb} +5 -5
  58. data/test/unit/types/array_type_test.rb +3 -18
  59. data/test/unit/types/boolean_type_test.rb +7 -21
  60. data/test/unit/types/date_range_type_test.rb +28 -0
  61. data/test/unit/types/date_type_test.rb +15 -6
  62. data/test/unit/types/float_type_test.rb +4 -19
  63. data/test/unit/types/geo_point_type_test.rb +24 -0
  64. data/test/unit/types/integer_range_type_test.rb +28 -0
  65. data/test/unit/types/integer_type_test.rb +7 -16
  66. data/test/unit/types/string_type_test.rb +9 -13
  67. data/test/unit/types/time_type_test.rb +17 -11
  68. data/test/unit/validations_test.rb +2 -2
  69. metadata +39 -39
  70. data/lib/superstore/attribute_methods/definition.rb +0 -17
  71. data/lib/superstore/attribute_methods/dirty.rb +0 -52
  72. data/lib/superstore/attribute_methods/typecasting.rb +0 -53
  73. data/lib/superstore/caching.rb +0 -13
  74. data/lib/superstore/callbacks.rb +0 -29
  75. data/lib/superstore/connection.rb +0 -24
  76. data/lib/superstore/errors.rb +0 -10
  77. data/lib/superstore/inspect.rb +0 -25
  78. data/lib/superstore/model.rb +0 -38
  79. data/lib/superstore/schema.rb +0 -20
  80. data/lib/superstore/scope.rb +0 -73
  81. data/lib/superstore/scope/batches.rb +0 -27
  82. data/lib/superstore/scope/finder_methods.rb +0 -51
  83. data/lib/superstore/scope/query_methods.rb +0 -52
  84. data/lib/superstore/scoping.rb +0 -30
  85. data/lib/superstore/timestamps.rb +0 -19
  86. data/lib/superstore/type.rb +0 -16
  87. data/lib/superstore/types/base_type.rb +0 -23
  88. data/lib/superstore/validations.rb +0 -44
  89. data/test/unit/attribute_methods/definition_test.rb +0 -16
  90. data/test/unit/inspect_test.rb +0 -26
  91. data/test/unit/schema_test.rb +0 -15
  92. data/test/unit/scope/finder_methods_test.rb +0 -62
  93. data/test/unit/scope/query_methods_test.rb +0 -37
  94. data/test/unit/scoping_test.rb +0 -7
  95. data/test/unit/types/base_type_test.rb +0 -11
@@ -2,140 +2,56 @@ module Superstore
2
2
  module Persistence
3
3
  extend ActiveSupport::Concern
4
4
 
5
- included do
6
- class_attribute :batch_statements
7
- end
8
-
9
5
  module ClassMethods
10
- def delete(ids)
11
- adapter.delete table_name, ids
12
- end
13
-
14
- def delete_all
15
- adapter.execute "TRUNCATE #{table_name}"
6
+ def find_by_id(id)
7
+ find_by(id: id)
16
8
  end
17
9
 
18
- def insert_record(id, attributes)
19
- adapter.insert table_name, id, encode_attributes(attributes)
20
- end
10
+ def _insert_record(attributes)
11
+ id = attributes.fetch(primary_key)
21
12
 
22
- def update_record(id, attributes)
23
- adapter.update table_name, id, encode_attributes(attributes)
13
+ adapter.insert table_name, id, serialize_attributes(attributes)
24
14
  end
25
15
 
26
- def batching?
27
- adapter.batching?
28
- end
16
+ def _update_record(attributes, constraints)
17
+ id = constraints.fetch(primary_key)
29
18
 
30
- def batch(&block)
31
- adapter.batch(&block)
19
+ adapter.update table_name, id, serialize_attributes(attributes)
32
20
  end
33
21
 
34
- def instantiate(id, attributes)
35
- allocate.tap do |object|
36
- object.instance_variable_set("@id", id) if id
37
- object.instance_variable_set("@new_record", false)
38
- object.instance_variable_set("@destroyed", false)
39
- object.instance_variable_set("@attributes", typecast_persisted_attributes(attributes))
40
- object.instance_variable_set("@association_cache", {})
41
- end
42
- end
43
-
44
- def encode_attributes(attributes)
45
- encoded = {}
46
- attributes.each do |column_name, value|
47
- if value.nil?
48
- encoded[column_name] = nil
49
- else
50
- encoded[column_name] = attribute_definitions[column_name].type.encode(value)
22
+ if Rails.version >= '6.0'
23
+ def instantiate_instance_of(klass, attributes, column_types = {}, &block)
24
+ if attributes[superstore_column].is_a?(String)
25
+ attributes = JSON.parse(attributes[superstore_column]).merge('id' => attributes['id'])
51
26
  end
52
- end
53
- encoded
54
- end
55
27
 
56
- private
57
-
58
- def quote_columns(column_names)
59
- column_names.map { |name| "'#{name}'" }
28
+ super(klass, attributes, column_types, &block)
60
29
  end
61
-
62
- def typecast_persisted_attributes(attributes)
63
- result = {}
64
-
65
- attributes.each do |key, value|
66
- if definition = attribute_definitions[key]
67
- result[key] = definition.instantiate(value)
68
- end
30
+ private :instantiate_instance_of
31
+ else
32
+ def instantiate(attributes, column_types = {}, &block)
33
+ if attributes[superstore_column].is_a?(String)
34
+ attributes = JSON.parse(attributes[superstore_column]).merge('id' => attributes['id'])
69
35
  end
70
36
 
71
- result
37
+ super(attributes, column_types, &block)
72
38
  end
73
- end
74
-
75
- def new_record?
76
- @new_record
77
- end
78
-
79
- def destroyed?
80
- @destroyed
81
- end
82
-
83
- def persisted?
84
- !(new_record? || destroyed?)
85
- end
86
-
87
- def save(*)
88
- create_or_update
89
- end
90
-
91
- def destroy
92
- self.class.delete(id)
93
- @destroyed = true
94
- end
95
-
96
- def update_attribute(name, value)
97
- name = name.to_s
98
- send("#{name}=", value)
99
- save(validate: false)
100
- end
101
-
102
- def update(attributes)
103
- self.attributes = attributes
104
- save
105
- end
106
-
107
- alias update_attributes update
108
-
109
- def update!(attributes)
110
- self.attributes = attributes
111
- save!
112
- end
113
-
114
- alias update_attributes! update!
115
-
116
- def reload
117
- clear_association_cache
118
- @attributes = self.class.find(id).instance_variable_get('@attributes')
119
- self
120
- end
121
-
122
- private
123
-
124
- def create_or_update
125
- new_record? ? create_self : update_self
126
39
  end
127
40
 
128
- def create_self
129
- write :insert_record
41
+ def serialize_attributes(attributes)
42
+ serialized = {}
43
+ attributes.each do |attr_name, value|
44
+ next if attr_name == primary_key
45
+ serialized[attr_name] = attribute_types[attr_name].serialize(value)
46
+ end
47
+ serialized
130
48
  end
131
49
 
132
- def update_self
133
- write :update_record
134
- end
50
+ private
135
51
 
136
- def write(method)
137
- @new_record = false
138
- self.class.send(method, id, unapplied_changes)
139
- end
52
+ def adapter
53
+ @adapter ||= Superstore::Adapters::JsonbAdapter.new
54
+ end
55
+ end
140
56
  end
141
57
  end
@@ -1,17 +1,9 @@
1
1
  module Superstore
2
2
  class Railtie < Rails::Railtie
3
3
  initializer "superstore.config" do |app|
4
- ActiveSupport.on_load :superstore do
5
- pathname = Rails.root.join('config', 'superstore.yml')
6
- if pathname.exist?
7
- config = ERB.new(pathname.read).result
8
- config = YAML.load(config)
9
-
10
- if config = config[Rails.env]
11
- self.config = config.symbolize_keys!
12
- else
13
- raise "Missing environment #{Rails.env} in superstore.yml"
14
- end
4
+ ActiveSupport.on_load :active_record do
5
+ ActiveRecord::Relation.class_eval do
6
+ include Superstore::Relation::Scrolling
15
7
  end
16
8
  end
17
9
  end
@@ -0,0 +1,48 @@
1
+ module Superstore
2
+ module Relation
3
+ module Scrolling
4
+ def scroll_each(options = {})
5
+ batch_size = options[:batch_size] || 1000
6
+
7
+ scroll_results(batch_size) do |attributes|
8
+ yield klass.instantiate(attributes)
9
+ end
10
+ end
11
+
12
+ def scroll_in_batches(options = {})
13
+ batch_size = options[:batch_size] || 1000
14
+ batch = []
15
+
16
+ scroll_each(options) do |record|
17
+ batch << record
18
+
19
+ if batch.size == batch_size
20
+ yield batch
21
+ batch = []
22
+ end
23
+ end
24
+
25
+ yield(batch) if batch.any?
26
+ end
27
+
28
+ private
29
+
30
+ def scroll_results(batch_size)
31
+ statement = to_sql
32
+ cursor_name = "cursor_#{SecureRandom.hex(6)}"
33
+ fetch_sql = "FETCH FORWARD #{batch_size} FROM #{cursor_name}"
34
+
35
+ connection.transaction do
36
+ connection.execute "DECLARE #{cursor_name} NO SCROLL CURSOR FOR (#{statement})"
37
+
38
+ while (batch = connection.execute(fetch_sql)).any?
39
+ batch.each do |result|
40
+ yield result
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module Superstore
2
+ module Timestamp
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def inherited(child)
7
+ super
8
+ child.attribute :created_at, type: :time
9
+ child.attribute :updated_at, type: :time
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,9 +1,11 @@
1
- Superstore::Type.register(:array, Superstore::Types::ArrayType)
2
- Superstore::Type.register(:boolean, Superstore::Types::BooleanType)
3
- Superstore::Type.register(:date, Superstore::Types::DateType)
4
- Superstore::Type.register(:float, Superstore::Types::FloatType)
5
- Superstore::Type.register(:geo_point, Superstore::Types::JsonType)
6
- Superstore::Type.register(:integer, Superstore::Types::IntegerType)
7
- Superstore::Type.register(:json, Superstore::Types::JsonType)
8
- Superstore::Type.register(:time, Superstore::Types::TimeType)
9
- Superstore::Type.register(:string, Superstore::Types::StringType)
1
+ ActiveRecord::Type.register(:superstore_array, Superstore::Types::ArrayType)
2
+ ActiveRecord::Type.register(:superstore_boolean, Superstore::Types::BooleanType)
3
+ ActiveRecord::Type.register(:superstore_date, Superstore::Types::DateType)
4
+ ActiveRecord::Type.register(:superstore_date_range, Superstore::Types::DateRangeType)
5
+ ActiveRecord::Type.register(:superstore_float, Superstore::Types::FloatType)
6
+ ActiveRecord::Type.register(:superstore_geo_point, Superstore::Types::GeoPointType)
7
+ ActiveRecord::Type.register(:superstore_integer, Superstore::Types::IntegerType)
8
+ ActiveRecord::Type.register(:superstore_integer_range, Superstore::Types::IntegerRangeType)
9
+ ActiveRecord::Type.register(:superstore_json, Superstore::Types::JsonType)
10
+ ActiveRecord::Type.register(:superstore_time, Superstore::Types::TimeType)
11
+ ActiveRecord::Type.register(:superstore_string, Superstore::Types::StringType)
@@ -1,12 +1,8 @@
1
1
  module Superstore
2
2
  module Types
3
- class ArrayType < BaseType
4
- def decode(val)
5
- val unless val.blank?
6
- end
7
-
8
- def typecast(value)
9
- value.to_a
3
+ class ArrayType < ActiveModel::Type::Value
4
+ def cast_value(value)
5
+ Array(value)
10
6
  end
11
7
  end
12
8
  end
@@ -1,20 +1,15 @@
1
1
  module Superstore
2
2
  module Types
3
- class BooleanType < BaseType
3
+ class BooleanType < ActiveModel::Type::Value
4
4
  TRUE_VALS = [true, 'true', '1']
5
- FALSE_VALS = [false, 'false', '0', '', nil]
6
- VALID_VALS = TRUE_VALS + FALSE_VALS
5
+ FALSE_VALS = [false, 'false', '0']
7
6
 
8
- def encode(bool)
9
- unless VALID_VALS.include?(bool)
10
- raise ArgumentError.new("#{bool.inspect} is not a Boolean")
7
+ def cast_value(value)
8
+ if TRUE_VALS.include?(value)
9
+ true
10
+ elsif FALSE_VALS.include?(value)
11
+ false
11
12
  end
12
-
13
- TRUE_VALS.include?(bool)
14
- end
15
-
16
- def decode(str)
17
- TRUE_VALS.include?(str)
18
13
  end
19
14
  end
20
15
  end
@@ -0,0 +1,7 @@
1
+ module Superstore
2
+ module Types
3
+ class DateRangeType < RangeType
4
+ self.subtype = DateType.new
5
+ end
6
+ end
7
+ end
@@ -1,21 +1,18 @@
1
1
  module Superstore
2
2
  module Types
3
- class DateType < BaseType
3
+ class DateType < ActiveModel::Type::Value
4
4
  FORMAT = '%Y-%m-%d'
5
- REGEX = /\A\d{4}-\d{2}-\d{2}\Z/
6
5
 
7
- def encode(value)
8
- raise ArgumentError.new("#{value.inspect} is not a Date") unless value.kind_of?(Date)
9
- value.strftime(FORMAT)
6
+ def serialize(value)
7
+ value.strftime(FORMAT) if value
10
8
  end
11
9
 
12
- def decode(str)
13
- return nil if str.empty?
14
- Date.parse(str)
10
+ def deserialize(str)
11
+ Date.strptime(str, FORMAT) if str
15
12
  end
16
13
 
17
- def typecast(value)
18
- value.to_date
14
+ def cast_value(value)
15
+ value.to_date rescue nil
19
16
  end
20
17
  end
21
18
  end
@@ -1,16 +1,8 @@
1
1
  module Superstore
2
2
  module Types
3
- class FloatType < BaseType
4
- def encode(float)
5
- float
6
- end
7
-
8
- def decode(str)
9
- str.to_f unless str.empty?
10
- end
11
-
12
- def typecast(value)
13
- value.to_f
3
+ class FloatType < ActiveModel::Type::Value
4
+ def cast_value(value)
5
+ Float(value) rescue nil
14
6
  end
15
7
  end
16
8
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Superstore
4
+ module Types
5
+ class GeoPointType < ActiveModel::Type::Value
6
+ def deserialize(value)
7
+ {lat: value[:lat] || value['lat'], lon: value[:lon] || value['lon']} if value
8
+ end
9
+
10
+ def cast_value(value)
11
+ case value
12
+ when String
13
+ cast_value value.split(/[,\s]+/)
14
+ when Array
15
+ to_float_or_nil(lat: value[0], lon: value[1])
16
+ when Hash
17
+ to_float_or_nil(lat: value[:lat] || value['lat'], lon: value[:lon] || value['lon'])
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def to_float_or_nil(coords)
24
+ if coords[:lat] && coords[:lon]
25
+ coords.transform_values!(&:to_f)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ module Superstore
2
+ module Types
3
+ class IntegerRangeType < RangeType
4
+ self.subtype = IntegerType.new
5
+
6
+ def serialize_for_open_ended(value)
7
+ value.abs == Float::INFINITY ? nil : super
8
+ end
9
+
10
+ def convert_min(method, value)
11
+ value.nil? ? -Float::INFINITY : super
12
+ end
13
+
14
+ def convert_max(method, value)
15
+ value.nil? ? Float::INFINITY : super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,19 +1,13 @@
1
1
  module Superstore
2
2
  module Types
3
- class IntegerType < BaseType
4
- REGEX = /\A[-+]?\d+\Z/
5
- def encode(int)
6
- raise ArgumentError.new("#{int.inspect} is not an Integer.") unless int.kind_of?(Integer)
7
-
8
- int
9
- end
10
-
11
- def decode(str)
12
- str.to_i unless str.empty?
13
- end
14
-
15
- def typecast(value)
16
- value.to_i
3
+ class IntegerType < ActiveModel::Type::Value
4
+ def cast_value(value)
5
+ if value.is_a?(String)
6
+ Integer(value, 10)
7
+ else
8
+ Integer(value)
9
+ end
10
+ rescue
17
11
  end
18
12
  end
19
13
  end