activerecord-typedstore 1.5.0 → 1.6.0
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/.github/workflows/ruby.yml +14 -5
- data/README.md +1 -1
- data/activerecord-typedstore.gemspec +1 -1
- data/gemfiles/Gemfile.ar-7.1 +5 -0
- data/lib/active_record/typed_store/behavior.rb +28 -0
- data/lib/active_record/typed_store/extension.rb +6 -1
- data/lib/active_record/typed_store/field.rb +3 -1
- data/lib/active_record/typed_store/type.rb +1 -6
- data/lib/active_record/typed_store/version.rb +1 -1
- data/spec/active_record/typed_store_spec.rb +39 -3
- data/spec/spec_helper.rb +6 -0
- data/spec/support/models.rb +36 -10
- metadata +9 -8
- /data/gemfiles/{Gemfile.ar-master → Gemfile.ar-edge} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9cbd38a8c2df073a4576501953ef2e9debb1d4c36c673c7e5467c04a247ba9cd
|
4
|
+
data.tar.gz: f12be53e1013b2302706e6471bebec9abd0d9edd74efe660d0fcf6d4859368c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e9ff18057af10d974dc456f3ff66633dd2483aae664f6acab881a895ef341b8006a25641441b216189dd61f972fefad067e41351e3cd04020e51d9a7d974272
|
7
|
+
data.tar.gz: 230a6686e806dac2ad28892f2f2f2344f1f7b6b7627f1eb048a1e0e2cce4e4b5f6336e162aa23f37de52197ca41909fa9ac43476a828b4d3fcf5e7ffbdb0dc96
|
data/.github/workflows/ruby.yml
CHANGED
@@ -5,22 +5,31 @@ on: [push, pull_request]
|
|
5
5
|
jobs:
|
6
6
|
build:
|
7
7
|
runs-on: ubuntu-latest
|
8
|
-
name:
|
8
|
+
name: ${{ matrix.ruby }} / Rails ${{ matrix.rails }} / TZ ${{ matrix.timezone_aware }}
|
9
9
|
strategy:
|
10
10
|
fail-fast: false
|
11
11
|
matrix:
|
12
|
-
ruby: ['2.7', '3.0', '3.1']
|
13
|
-
|
12
|
+
ruby: ['2.7', '3.0', '3.1', '3.2', '3.3']
|
13
|
+
rails: ['6.1', '7.0', '7.1', 'edge']
|
14
14
|
timezone_aware: [0, 1]
|
15
|
+
exclude:
|
16
|
+
- ruby: '3.2'
|
17
|
+
rails: '6.1'
|
18
|
+
- ruby: '3.3'
|
19
|
+
rails: '6.1'
|
20
|
+
- ruby: '2.7'
|
21
|
+
rails: 'edge'
|
22
|
+
- ruby: '3.0'
|
23
|
+
rails: 'edge'
|
15
24
|
env:
|
16
|
-
BUNDLE_GEMFILE: gemfiles
|
25
|
+
BUNDLE_GEMFILE: gemfiles/Gemfile.ar-${{ matrix.rails }}
|
17
26
|
TIMEZONE_AWARE: ${{ matrix.timezone_aware }}
|
18
27
|
POSTGRES: 1
|
19
28
|
MYSQL: 1
|
20
29
|
POSTGRES_JSON: 1
|
21
30
|
steps:
|
22
31
|
- name: Check out code
|
23
|
-
uses: actions/checkout@
|
32
|
+
uses: actions/checkout@v3
|
24
33
|
- name: Set up Ruby ${{ matrix.ruby }}
|
25
34
|
uses: ruby/setup-ruby@v1
|
26
35
|
with:
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# ActiveRecord::TypedStore
|
2
2
|
|
3
|
-
[](http://badge.fury.io/rb/activerecord-typedstore)
|
4
4
|
|
5
5
|
[ActiveRecord::Store](http://api.rubyonrails.org/classes/ActiveRecord/Store.html) but with typed attributes.
|
6
6
|
|
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.add_dependency 'activerecord', '>= 6.1'
|
22
22
|
|
23
23
|
spec.add_development_dependency 'bundler'
|
24
|
-
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
25
|
spec.add_development_dependency 'rspec', '~> 3'
|
26
26
|
spec.add_development_dependency 'sqlite3', '~> 1'
|
27
27
|
spec.add_development_dependency 'database_cleaner', '~> 1'
|
@@ -71,5 +71,33 @@ module ActiveRecord::TypedStore
|
|
71
71
|
super
|
72
72
|
end
|
73
73
|
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
if ActiveRecord.version.segments.first >= 7
|
78
|
+
def attribute_names_for_partial_inserts
|
79
|
+
# Contrary to all vanilla Rails types, typedstore attribute have an inherent default
|
80
|
+
# value that doesn't match the database column default.
|
81
|
+
# As such we need to insert them on partial inserts even if they weren't changed.
|
82
|
+
super | self.class.typed_stores.keys.map(&:to_s)
|
83
|
+
end
|
84
|
+
|
85
|
+
def attribute_names_for_partial_updates
|
86
|
+
# On partial updates we shouldn't need to force stores to be persisted. However since
|
87
|
+
# we weren't persisting them for a while on insertion, we now need to gracefully deal
|
88
|
+
# with existing records that may have been persisted with a `NULL` store
|
89
|
+
# We use `blank?` as an heuristic to detect these.
|
90
|
+
super | self.class.typed_stores.keys.map(&:to_s).select do |store|
|
91
|
+
has_attribute?(store) && read_attribute_before_type_cast(store).blank?
|
92
|
+
end
|
93
|
+
end
|
94
|
+
else
|
95
|
+
# Rails 6.1 capability
|
96
|
+
def attribute_names_for_partial_writes
|
97
|
+
super | self.class.typed_stores.keys.map(&:to_s).select do |store|
|
98
|
+
has_attribute?(store) && read_attribute_before_type_cast(store).blank?
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
74
102
|
end
|
75
103
|
end
|
@@ -22,7 +22,12 @@ module ActiveRecord::TypedStore
|
|
22
22
|
typed_klass = TypedHash.create(dsl.fields.values)
|
23
23
|
const_set("#{store_attribute}_hash".camelize, typed_klass)
|
24
24
|
|
25
|
-
if ActiveRecord.version >= Gem::Version.new('
|
25
|
+
if ActiveRecord.version >= Gem::Version.new('7.2.0.alpha')
|
26
|
+
decorate_attributes([store_attribute]) do |name, subtype|
|
27
|
+
subtype = subtype.subtype if subtype.is_a?(Type)
|
28
|
+
Type.new(typed_klass, dsl.coder, subtype)
|
29
|
+
end
|
30
|
+
elsif ActiveRecord.version >= Gem::Version.new('6.1.0.alpha')
|
26
31
|
attribute(store_attribute) do |subtype|
|
27
32
|
subtype = subtype.subtype if subtype.is_a?(Type)
|
28
33
|
Type.new(typed_klass, dsl.coder, subtype)
|
@@ -11,12 +11,12 @@ module ActiveRecord::TypedStore
|
|
11
11
|
|
12
12
|
@accessor = options.fetch(:accessor, true)
|
13
13
|
@name = name
|
14
|
+
@array = options.fetch(:array, false)
|
14
15
|
if options.key?(:default)
|
15
16
|
@default = extract_default(options[:default])
|
16
17
|
end
|
17
18
|
@null = options.fetch(:null, true)
|
18
19
|
@blank = options.fetch(:blank, true)
|
19
|
-
@array = options.fetch(:array, false)
|
20
20
|
end
|
21
21
|
|
22
22
|
def has_default?
|
@@ -27,6 +27,8 @@ module ActiveRecord::TypedStore
|
|
27
27
|
casted_value = type_cast(value)
|
28
28
|
if !blank
|
29
29
|
casted_value = default if casted_value.blank?
|
30
|
+
elsif array && has_default?
|
31
|
+
casted_value = default if value.nil?
|
30
32
|
elsif !null
|
31
33
|
casted_value = default if casted_value.nil?
|
32
34
|
end
|
@@ -41,12 +41,7 @@ module ActiveRecord::TypedStore
|
|
41
41
|
|
42
42
|
def changed_in_place?(raw_old_value, value)
|
43
43
|
return false if value.nil?
|
44
|
-
|
45
|
-
raw_new_value = serialize(value)
|
46
|
-
else
|
47
|
-
# 4.2 capability
|
48
|
-
raw_new_value = type_cast_for_database(value)
|
49
|
-
end
|
44
|
+
raw_new_value = serialize(value)
|
50
45
|
raw_old_value.nil? != raw_new_value.nil? || raw_old_value != raw_new_value
|
51
46
|
end
|
52
47
|
end
|
@@ -524,7 +524,7 @@ shared_examples 'a store' do |retain_type = true, settings_type = :text|
|
|
524
524
|
describe 'model.typed_stores' do
|
525
525
|
it "can access keys" do
|
526
526
|
stores = model.class.typed_stores
|
527
|
-
expect(stores[:settings].keys).to eq [:no_default, :name, :email, :cell_phone, :public, :enabled, :age, :max_length, :rate, :price, :published_on, :remind_on, :published_at_time, :remind_at_time, :published_at, :remind_at, :total_price, :shipping_cost, :grades, :tags, :nickname, :author, :source, :signup, :country]
|
527
|
+
expect(stores[:settings].keys).to eq [:no_default, :name, :email, :cell_phone, :public, :enabled, :age, :max_length, :rate, :price, :published_on, :remind_on, :published_at_time, :remind_at_time, :published_at, :remind_at, :total_price, :shipping_cost, :grades, :tags, :subjects, :nickname, :author, :source, :signup, :country]
|
528
528
|
end
|
529
529
|
|
530
530
|
it "can access keys even when accessors are not defined" do
|
@@ -889,9 +889,15 @@ shared_examples 'a model supporting arrays' do |pg_native=false|
|
|
889
889
|
expect(model.reload.grades).to be == [[1, 2], [3, 4, 5]]
|
890
890
|
end
|
891
891
|
|
892
|
+
it 'defaults to [] if provided default is not an array' do
|
893
|
+
model.update(subjects: nil)
|
894
|
+
expect(model.reload.subjects).to be == []
|
895
|
+
end
|
896
|
+
|
897
|
+
# Not sure about pg_native and if this test should be outside of this block.
|
892
898
|
it 'retreive default if assigned null' do
|
893
899
|
model.update(tags: nil)
|
894
|
-
expect(model.reload.tags).to be == []
|
900
|
+
expect(model.reload.tags).to be == ['article']
|
895
901
|
end
|
896
902
|
end
|
897
903
|
end
|
@@ -925,7 +931,7 @@ describe YamlTypedStoreModel do
|
|
925
931
|
|
926
932
|
it 'nested hashes are not serialized as HashWithIndifferentAccess' do
|
927
933
|
model = described_class.create!
|
928
|
-
expect(model.settings_before_type_cast).not_to include('HashWithIndifferentAccess')
|
934
|
+
expect(model.settings_before_type_cast.to_s).not_to include('HashWithIndifferentAccess')
|
929
935
|
end
|
930
936
|
end
|
931
937
|
|
@@ -954,3 +960,33 @@ describe InheritedTypedStoreModel do
|
|
954
960
|
expect(model.settings[:new_attribute]).to be == '42'
|
955
961
|
end
|
956
962
|
end
|
963
|
+
|
964
|
+
describe DirtyTrackingModel do
|
965
|
+
it 'stores the default on creation' do
|
966
|
+
model = DirtyTrackingModel.create!
|
967
|
+
model = DirtyTrackingModel.find(model.id)
|
968
|
+
expect(model.settings_before_type_cast).to_not be_blank
|
969
|
+
end
|
970
|
+
|
971
|
+
it 'handles loaded records having uninitialized defaults' do
|
972
|
+
model = DirtyTrackingModel.create!
|
973
|
+
DirtyTrackingModel.update_all("settings = NULL") # bypass validation
|
974
|
+
model = DirtyTrackingModel.find(model.id)
|
975
|
+
expect(model.settings_changed?).to be false
|
976
|
+
expect(model.changes).to be_empty
|
977
|
+
|
978
|
+
model.update!(title: "Hello")
|
979
|
+
|
980
|
+
expect(model.settings_changed?).to be false
|
981
|
+
expect(model.changes).to be_empty
|
982
|
+
end
|
983
|
+
|
984
|
+
it 'does not update missing attributes in partially loaded records' do
|
985
|
+
model = DirtyTrackingModel.create!(active: true)
|
986
|
+
model = DirtyTrackingModel.select(:id, :title).find(model.id)
|
987
|
+
model.update!(title: "Hello")
|
988
|
+
|
989
|
+
model = DirtyTrackingModel.find(model.id)
|
990
|
+
expect(model.active).to be true
|
991
|
+
end
|
992
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -8,6 +8,12 @@ Dir[File.expand_path(File.join(File.dirname(__FILE__), 'support', '**', '*.rb'))
|
|
8
8
|
|
9
9
|
Time.zone = 'UTC'
|
10
10
|
|
11
|
+
if ActiveRecord.respond_to?(:yaml_column_permitted_classes)
|
12
|
+
ActiveRecord.yaml_column_permitted_classes |= [Date, Time, BigDecimal]
|
13
|
+
elsif ActiveRecord::Base.respond_to?(:yaml_column_permitted_classes)
|
14
|
+
ActiveRecord::Base.yaml_column_permitted_classes |= [Date, Time, BigDecimal]
|
15
|
+
end
|
16
|
+
|
11
17
|
RSpec.configure do |config|
|
12
18
|
config.order = 'random'
|
13
19
|
end
|
data/spec/support/models.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'active_record'
|
2
|
+
require 'base64'
|
2
3
|
require 'json'
|
3
4
|
require 'yaml'
|
4
5
|
|
@@ -12,7 +13,7 @@ ActiveRecord::Base.configurations = {
|
|
12
13
|
}
|
13
14
|
}
|
14
15
|
|
15
|
-
def define_columns(t)
|
16
|
+
def define_columns(t, array: false)
|
16
17
|
t.integer :no_default
|
17
18
|
|
18
19
|
t.string :name, default: '', null: false
|
@@ -40,11 +41,13 @@ def define_columns(t)
|
|
40
41
|
t.decimal :total_price, default: 4.2, null: false, precision: 16, scale: 2
|
41
42
|
t.decimal :shipping_cost, precision: 16, scale: 2
|
42
43
|
|
43
|
-
t.
|
44
|
+
if t.is_a?(ActiveRecord::TypedStore::DSL)
|
45
|
+
t.integer :grades, array: true
|
46
|
+
t.string :tags, array: true, null: false, default: ['article']
|
47
|
+
t.string :subjects, array: true, null: false, default: ['mathematics'].to_yaml
|
44
48
|
|
45
|
-
|
46
|
-
|
47
|
-
t.string :nickname, blank: false, default: 'Please enter your nickname'
|
49
|
+
t.string :nickname, blank: false, default: 'Please enter your nickname'
|
50
|
+
end
|
48
51
|
end
|
49
52
|
|
50
53
|
def define_store_with_no_attributes(**options)
|
@@ -79,15 +82,21 @@ def define_stores_with_prefix_and_suffix(**options)
|
|
79
82
|
typed_store(:custom_suffixed_settings, suffix: :custom, **options) { |t| t.any :language }
|
80
83
|
end
|
81
84
|
|
82
|
-
MigrationClass = ActiveRecord::Migration["
|
85
|
+
MigrationClass = ActiveRecord::Migration["#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}"]
|
83
86
|
class CreateAllTables < MigrationClass
|
84
|
-
|
85
87
|
def self.up
|
86
88
|
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.configs_for(env_name: "test", name: :test_sqlite3))
|
87
89
|
create_table(:sqlite3_regular_ar_models, force: true) { |t| define_columns(t); t.text :untyped_settings }
|
88
|
-
create_table(:yaml_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column} }
|
89
|
-
create_table(:json_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column} }
|
90
|
-
create_table(:marshal_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column} }
|
90
|
+
create_table(:yaml_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column}; t.string :regular_column }
|
91
|
+
create_table(:json_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column}; t.string :regular_column }
|
92
|
+
create_table(:marshal_typed_store_models, force: true) { |t| %i[settings explicit_settings partial_settings untyped_settings prefixed_settings suffixed_settings custom_prefixed_settings custom_suffixed_settings].each { |column| t.text column}; t.string :regular_column }
|
93
|
+
|
94
|
+
create_table(:dirty_tracking_models, force: true) do |t|
|
95
|
+
t.string :title
|
96
|
+
t.text :settings
|
97
|
+
|
98
|
+
t.timestamps
|
99
|
+
end
|
91
100
|
end
|
92
101
|
end
|
93
102
|
ActiveRecord::Migration.verbose = true
|
@@ -121,6 +130,11 @@ class YamlTypedStoreModel < ActiveRecord::Base
|
|
121
130
|
establish_connection :test_sqlite3
|
122
131
|
store :untyped_settings, accessors: [:title]
|
123
132
|
|
133
|
+
after_update :read_active
|
134
|
+
def read_active
|
135
|
+
enabled
|
136
|
+
end
|
137
|
+
|
124
138
|
define_store_with_attributes
|
125
139
|
define_store_with_no_attributes
|
126
140
|
define_store_with_partial_attributes
|
@@ -175,3 +189,15 @@ Models = [
|
|
175
189
|
JsonTypedStoreModel,
|
176
190
|
MarshalTypedStoreModel
|
177
191
|
]
|
192
|
+
|
193
|
+
class DirtyTrackingModel < ActiveRecord::Base
|
194
|
+
after_update :read_active, if: -> { has_attribute?(:settings) }
|
195
|
+
|
196
|
+
typed_store(:settings) do |f|
|
197
|
+
f.boolean :active, default: false, null: false
|
198
|
+
end
|
199
|
+
|
200
|
+
def read_active
|
201
|
+
active
|
202
|
+
end
|
203
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-typedstore
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-02-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -42,16 +42,16 @@ dependencies:
|
|
42
42
|
name: rake
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - ">="
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '0'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rspec
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -111,7 +111,8 @@ files:
|
|
111
111
|
- activerecord-typedstore.gemspec
|
112
112
|
- gemfiles/Gemfile.ar-6.1
|
113
113
|
- gemfiles/Gemfile.ar-7.0
|
114
|
-
- gemfiles/Gemfile.ar-
|
114
|
+
- gemfiles/Gemfile.ar-7.1
|
115
|
+
- gemfiles/Gemfile.ar-edge
|
115
116
|
- lib/active_record/typed_store.rb
|
116
117
|
- lib/active_record/typed_store/behavior.rb
|
117
118
|
- lib/active_record/typed_store/dsl.rb
|
@@ -146,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
146
147
|
- !ruby/object:Gem::Version
|
147
148
|
version: '0'
|
148
149
|
requirements: []
|
149
|
-
rubygems_version: 3.
|
150
|
+
rubygems_version: 3.5.5
|
150
151
|
signing_key:
|
151
152
|
specification_version: 4
|
152
153
|
summary: Add type casting and full method attributes support to АctiveRecord store
|
File without changes
|