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 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