activerecord-typedstore 1.5.0 → 1.6.0

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
  SHA256:
3
- metadata.gz: e4f681cbd23a432d2e40094499e45a693ebacc8784d639dbd35f5d7467d3f024
4
- data.tar.gz: 2adde8a50a784c41a23e188d51ababc7c19a553d6e4599e2265dad9c70ea5ebe
3
+ metadata.gz: 9cbd38a8c2df073a4576501953ef2e9debb1d4c36c673c7e5467c04a247ba9cd
4
+ data.tar.gz: f12be53e1013b2302706e6471bebec9abd0d9edd74efe660d0fcf6d4859368c1
5
5
  SHA512:
6
- metadata.gz: 67570832d5d8f6d7d05617f2f6572ed1e629bb1463f4bc123513d84219306a2aca59439082eed57a00d80e97474bd1b5a63269a94caabd530769bd8c098c3da6
7
- data.tar.gz: a9f3f3d3ab6bbbe4b47625674fde96e3f3c8c0fbb697f2b557b1aa089ed45bd91e2c3bff98c0bbee8c55d52bc3d473bf16fc92759b65ad5a84548d91a5e81b14
6
+ metadata.gz: 8e9ff18057af10d974dc456f3ff66633dd2483aae664f6acab881a895ef341b8006a25641441b216189dd61f972fefad067e41351e3cd04020e51d9a7d974272
7
+ data.tar.gz: 230a6686e806dac2ad28892f2f2f2344f1f7b6b7627f1eb048a1e0e2cce4e4b5f6336e162aa23f37de52197ca41909fa9ac43476a828b4d3fcf5e7ffbdb0dc96
@@ -5,22 +5,31 @@ on: [push, pull_request]
5
5
  jobs:
6
6
  build:
7
7
  runs-on: ubuntu-latest
8
- name: Ruby ${{ matrix.ruby }}
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
- gemfile: [Gemfile.ar-6.1, Gemfile.ar-7.0, Gemfile.ar-master]
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/${{ matrix.gemfile }}
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@v2
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
- [![Gem Version](https://badge.fury.io/rb/activerecord-typedstore.png)](http://badge.fury.io/rb/activerecord-typedstore)
3
+ [![Gem Version](https://badge.fury.io/rb/activerecord-typedstore.svg)](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', '~> 10'
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'
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec path: '..'
4
+
5
+ gem 'activerecord', '~> 7.0.0'
@@ -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('6.1.0.alpha')
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
- if ActiveRecord.version.segments.first >= 5
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActiveRecord
4
4
  module TypedStore
5
- VERSION = '1.5.0'
5
+ VERSION = '1.6.0'
6
6
  end
7
7
  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
@@ -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.integer :grades, array: true
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
- t.string :tags, array: true, null: false, default: [].to_yaml
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["5.0"]
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.5.0
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: 2022-05-24 00:00:00.000000000 Z
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: '10'
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: '10'
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-master
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.3.7
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