simple_master 1.0.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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/lib/simple_master/active_record/belongs_to_master_polymorphic_reflection.rb +11 -0
  3. data/lib/simple_master/active_record/belongs_to_polymorphic_association.rb +17 -0
  4. data/lib/simple_master/active_record/belongs_to_polymorphic_builder.rb +21 -0
  5. data/lib/simple_master/active_record/extension.rb +183 -0
  6. data/lib/simple_master/active_record/preloader_association_extension.rb +11 -0
  7. data/lib/simple_master/active_record.rb +12 -0
  8. data/lib/simple_master/loader/dataset_loader.rb +20 -0
  9. data/lib/simple_master/loader/marshal_loader.rb +15 -0
  10. data/lib/simple_master/loader/query_loader.rb +63 -0
  11. data/lib/simple_master/loader.rb +55 -0
  12. data/lib/simple_master/master/association/belongs_to_association.rb +79 -0
  13. data/lib/simple_master/master/association/belongs_to_polymorphic_association.rb +79 -0
  14. data/lib/simple_master/master/association/has_many_association.rb +53 -0
  15. data/lib/simple_master/master/association/has_many_through_association.rb +64 -0
  16. data/lib/simple_master/master/association/has_one_association.rb +57 -0
  17. data/lib/simple_master/master/association.rb +50 -0
  18. data/lib/simple_master/master/column/bitmask_column.rb +74 -0
  19. data/lib/simple_master/master/column/boolean_column.rb +51 -0
  20. data/lib/simple_master/master/column/enum_column.rb +96 -0
  21. data/lib/simple_master/master/column/float_column.rb +21 -0
  22. data/lib/simple_master/master/column/id_column.rb +31 -0
  23. data/lib/simple_master/master/column/integer_column.rb +21 -0
  24. data/lib/simple_master/master/column/json_column.rb +27 -0
  25. data/lib/simple_master/master/column/polymorphic_type_column.rb +44 -0
  26. data/lib/simple_master/master/column/sti_type_column.rb +21 -0
  27. data/lib/simple_master/master/column/string_column.rb +17 -0
  28. data/lib/simple_master/master/column/symbol_column.rb +23 -0
  29. data/lib/simple_master/master/column/time_column.rb +38 -0
  30. data/lib/simple_master/master/column.rb +138 -0
  31. data/lib/simple_master/master/dsl.rb +239 -0
  32. data/lib/simple_master/master/editable.rb +155 -0
  33. data/lib/simple_master/master/filterable.rb +47 -0
  34. data/lib/simple_master/master/queryable.rb +75 -0
  35. data/lib/simple_master/master/storable.rb +20 -0
  36. data/lib/simple_master/master/validatable.rb +216 -0
  37. data/lib/simple_master/master.rb +417 -0
  38. data/lib/simple_master/schema.rb +49 -0
  39. data/lib/simple_master/storage/dataset.rb +172 -0
  40. data/lib/simple_master/storage/ondemand_table.rb +68 -0
  41. data/lib/simple_master/storage/table.rb +197 -0
  42. data/lib/simple_master/storage/test_table.rb +69 -0
  43. data/lib/simple_master/storage.rb +11 -0
  44. data/lib/simple_master/version.rb +5 -0
  45. data/lib/simple_master.rb +62 -0
  46. metadata +128 -0
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_master/storage/table"
4
+ require "simple_master/storage/test_table"
5
+
6
+ module SimpleMaster
7
+ module Storage
8
+ class Dataset
9
+ require "objspace"
10
+
11
+ class << self
12
+ def after_load_procs
13
+ @after_load_procs ||= []
14
+ end
15
+
16
+ def after_load(&proc)
17
+ after_load_procs << proc
18
+ end
19
+
20
+ def run_after_load
21
+ after_load_procs.each(&:call)
22
+ end
23
+ end
24
+
25
+ DEFAULT_TABLE = Table
26
+ DEFAULT_LOADER = Loader::QueryLoader
27
+
28
+ attr_reader :table_class
29
+ attr_reader :loader
30
+ attr_reader :diff
31
+ attr_accessor :tables
32
+ attr_accessor :cache
33
+ attr_accessor :load_targets
34
+
35
+ def initialize(table_class: DEFAULT_TABLE, loader: nil)
36
+ @table_class = table_class
37
+ @loader = loader || DEFAULT_LOADER.new
38
+ @diff = {}
39
+
40
+ initialize_cache
41
+ end
42
+
43
+ def initialize_cache
44
+ self.tables = Hash.new { |hash, klass| hash[klass] = table_class.new(klass, self, loader) }.compare_by_identity
45
+ self.cache = {}
46
+ end
47
+
48
+ def table(klass)
49
+ @tables[klass]
50
+ end
51
+
52
+ def reload
53
+ if table_class <= SimpleMaster::Storage::OndemandTable
54
+ unload
55
+ else
56
+ load
57
+ end
58
+ end
59
+
60
+ def load
61
+ memsize do
62
+ cache.clear
63
+ targets = @load_targets || SimpleMaster.targets
64
+
65
+ tables = targets.map(&:base_class).uniq.map { table(_1) }
66
+
67
+ timer("MasterData load") do
68
+ tables.each(&:load_records)
69
+ end
70
+
71
+ timer("MasterData cache update") do
72
+ SimpleMaster.use_dataset(self) do
73
+ tables.each(&:update_class_method_cache)
74
+ tables.each(&:tap_instance_methods)
75
+ tables.each(&:freeze_all)
76
+ end
77
+ end
78
+
79
+ timer("Cache update") do
80
+ SimpleMaster.use_dataset(self) do
81
+ self.class.run_after_load
82
+ end
83
+ end
84
+ end
85
+ self
86
+ end
87
+
88
+ def unload
89
+ cache.clear
90
+ tables.clear
91
+ end
92
+
93
+ # NOTE: Pass a empty hash to duplicate with empty diff.
94
+ def duplicate(diff: nil)
95
+ diff ||= @diff
96
+ new_dataset = self.class.new(table_class: table_class, loader: loader)
97
+ new_dataset.diff = diff.deep_dup
98
+ tables.each do |klass, table|
99
+ new_dataset.tables[klass] = table.duplicate_for(new_dataset)
100
+ end
101
+
102
+ new_dataset
103
+ end
104
+
105
+ def reload_klass(klass)
106
+ fail NotImplementedError unless table_class == SimpleMaster::Storage::OndemandTable
107
+
108
+ tables.delete(klass)
109
+ end
110
+ alias reload_class reload_klass
111
+
112
+ # Cache helper for other data sources.
113
+ def cache_read(key)
114
+ cache[key]
115
+ end
116
+
117
+ def cache_fetch(key)
118
+ cache.fetch(key) do
119
+ cache[key] = yield
120
+ end
121
+ end
122
+
123
+ def cache_write(key, value)
124
+ cache[key] = value
125
+ end
126
+
127
+ def cache_delete(key)
128
+ cache.delete(key)
129
+ end
130
+
131
+ # NOTE: set diff before records loaded.
132
+ def diff=(diff_json)
133
+ @diff = if diff_json.is_a?(String)
134
+ JSON.parse(diff_json)
135
+ else
136
+ diff_json || {}
137
+ end
138
+ end
139
+
140
+ private
141
+
142
+ def memsize
143
+ GC.compact
144
+ before = ObjectSpace.memsize_of_all
145
+
146
+ res = yield
147
+
148
+ GC.compact
149
+ after = ObjectSpace.memsize_of_all
150
+
151
+ SimpleMaster.logger.info { "Consumed memory size: #{after - before}" }
152
+
153
+ res
154
+ end
155
+
156
+ def timer(text)
157
+ t1 = Time.zone.now
158
+ @timer_index ||= 0
159
+ @timer_index += 1
160
+
161
+ res = yield
162
+
163
+ t2 = Time.zone.now
164
+ @timer_index -= 1
165
+
166
+ SimpleMaster.logger.info { "#{' ' * @timer_index}#{text}: #{t2 - t1}s" }
167
+
168
+ res
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_master/storage/table"
4
+
5
+ module SimpleMaster
6
+ module Storage
7
+ class OndemandTable < Table
8
+ def sub_table(sub_klass)
9
+ (@sub_tables ||= klass.descendants.reject(&:abstract_class).index_with { |k| self.class.new(k, dataset, loader) })[sub_klass]
10
+ end
11
+
12
+ def load_records
13
+ @class_method_cache = nil
14
+ super
15
+ end
16
+
17
+ def all
18
+ return @all if @all
19
+
20
+ if klass.sti_sub_class?
21
+ klass.sti_base_class.all
22
+ @all
23
+ else
24
+ load_records
25
+ SimpleMaster.use_dataset(dataset) do
26
+ tap_instance_methods
27
+ freeze_all
28
+ end
29
+ end
30
+ end
31
+
32
+ def id_hash
33
+ return @id_hash if @id_hash
34
+
35
+ update_id_hash
36
+ end
37
+
38
+ def grouped_hash
39
+ return @grouped_hash if @grouped_hash
40
+
41
+ update_grouped_hash
42
+ end
43
+
44
+ def class_method_cache
45
+ return @class_method_cache if @class_method_cache
46
+
47
+ # make sure @all is loaded to prevent errors
48
+ all
49
+
50
+ update_class_method_cache
51
+ end
52
+
53
+ def update_sub_tables
54
+ sub_klasses = klass.descendants.reject(&:abstract_class)
55
+ return if sub_klasses.empty?
56
+
57
+ grouped = all.group_by(&:class)
58
+ sub_klasses.each do |sub_klass|
59
+ sub_sub_klasses = [sub_klass, *sub_klass.descendants].reject(&:abstract_class)
60
+
61
+ sub_table = self.sub_table(sub_klass)
62
+ sub_table.all = sub_sub_klasses.flat_map { grouped[_1] || EMPTY_ARRAY }.freeze
63
+ sub_table.digest = digest
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ module Storage
5
+ class Table
6
+ METADATA_PREFIX = "__"
7
+
8
+ def initialize(klass, dataset, loader)
9
+ @klass = klass
10
+ @dataset = dataset
11
+ @loader = loader
12
+ @method_cache = Hash.new { |k, v| k[v] = {}.compare_by_identity }.compare_by_identity
13
+
14
+ @sub_tables = nil
15
+ end
16
+
17
+ def sub_table(klass)
18
+ @sub_tables[klass]
19
+ end
20
+
21
+ def duplicate_for(dataset, table_klass = self.class)
22
+ table = table_klass.new(klass, dataset, loader)
23
+
24
+ if table.diff == applied_diff
25
+ table.all = all
26
+ table.applied_diff = applied_diff
27
+ table.digest = digest
28
+
29
+ # If already loaded, copy existing id_hash and grouped_hash
30
+ table.id_hash = id_hash if @id_hash
31
+ table.grouped_hash = grouped_hash if @grouped_hash
32
+ table.sub_tables = sub_tables&.transform_values { |sub_table|
33
+ sub_table.duplicate_for(dataset)
34
+ }
35
+ end
36
+
37
+ table
38
+ end
39
+
40
+ def load_records
41
+ @method_cache.clear
42
+
43
+ loader.load_records(self)
44
+ end
45
+
46
+ attr_accessor :klass
47
+ attr_accessor :dataset
48
+ attr_accessor :loader
49
+ attr_accessor :sub_tables
50
+ attr_accessor :all
51
+ attr_accessor :id_hash
52
+ attr_accessor :grouped_hash
53
+ attr_accessor :class_method_cache
54
+ attr_accessor :method_cache
55
+ attr_accessor :applied_diff
56
+ attr_accessor :digest
57
+
58
+ def freeze_all
59
+ all.each(&:freeze).tap { run_on_sub_tables(:freeze_all) }
60
+ end
61
+
62
+ def update_id_hash
63
+ self.id_hash = all.index_by(&:id).freeze
64
+ end
65
+
66
+ def update_grouped_hash
67
+ grouped_hash = {}.compare_by_identity
68
+
69
+ klass.group_keys.each do |group_key|
70
+ grouped_hash[group_key] = all.group_by(&group_key).freeze.each_value(&:freeze)
71
+ end
72
+ grouped_hash.freeze
73
+
74
+ self.grouped_hash = grouped_hash
75
+ end
76
+
77
+ def update_class_method_cache
78
+ self.class_method_cache = class_method_cache = {}.compare_by_identity
79
+ # Use instance_eval so a block runs in the declaring class context
80
+ klass.all_class_method_cache_info.each do |args, initializer|
81
+ result = klass.instance_eval(&initializer)
82
+ if args.length == 1
83
+ result = [result]
84
+ end
85
+
86
+ args.zip(result).each do |arg, value|
87
+ class_method_cache[arg] = value
88
+ end
89
+ end
90
+ class_method_cache.freeze.tap {
91
+ run_on_sub_tables(:update_class_method_cache)
92
+ }
93
+ end
94
+
95
+ def tap_instance_methods
96
+ klass.instance_methods_need_tap&.each do |method_name|
97
+ all.each(&method_name)
98
+ end
99
+ run_on_sub_tables(:tap_instance_methods)
100
+ end
101
+
102
+ def update_sub_tables
103
+ sub_klasses = klass.descendants.reject(&:abstract_class)
104
+ return if sub_klasses.empty?
105
+
106
+ @sub_tables = sub_klasses.index_with { |sub_klass| self.class.new(sub_klass, dataset, loader) }
107
+
108
+ grouped = all.group_by(&:class)
109
+
110
+ sub_klasses.each do |sub_klass|
111
+ sub_sub_klasses = [sub_klass, *sub_klass.descendants].reject(&:abstract_class)
112
+
113
+ sub_table = self.sub_table(sub_klass)
114
+ sub_table.all = sub_sub_klasses.flat_map { grouped[_1] || EMPTY_ARRAY }.freeze
115
+ sub_table.digest = digest
116
+ sub_table.applied_diff = applied_diff
117
+ sub_table.update_id_hash
118
+ sub_table.update_grouped_hash
119
+ end
120
+ end
121
+
122
+ def run_on_sub_tables(method)
123
+ sub_tables&.each_value(&method)
124
+ end
125
+
126
+ def diff
127
+ dataset.diff[klass.table_name]
128
+ end
129
+
130
+ def apply_diff
131
+ update_id_hash
132
+ if diff.present?
133
+ apply_diff_to_id_hash
134
+ self.all = id_hash.values.freeze
135
+ end
136
+ update_grouped_hash
137
+
138
+ self
139
+ end
140
+
141
+ def apply_diff_to_id_hash
142
+ id_hash = self.id_hash.dup
143
+ diff&.each do |key, record_diff|
144
+ next if metadata_key?(key)
145
+ id = key.to_i
146
+
147
+ if record_diff.nil?
148
+ id_hash.delete(id)
149
+ next
150
+ end
151
+
152
+ original_record = id_hash[id]
153
+
154
+ record =
155
+ if original_record.nil?
156
+ new_klass = klass.sti_column && record_diff[klass.sti_column.to_s]&.constantize || klass
157
+ new_klass.new(id: id)
158
+ elsif klass.sti_column && record_diff[klass.sti_column.to_s]
159
+ record_diff[klass.sti_column.to_s].constantize.new(original_record.attributes)
160
+ else
161
+ original_record.create_copy
162
+ end
163
+
164
+ record_diff.each do |k, v|
165
+ next if metadata_key?(k)
166
+ # Handle expanded JSON columns by converting keys back to JSON strings before assignment
167
+ if v.is_a?(Hash) || v.is_a?(Array)
168
+ v = v.to_json
169
+ end
170
+ record.send(:"#{k}=", v)
171
+ rescue NoMethodError => _e
172
+ raise ColumnNotExist, "Column #{k} does not exist on #{klass}."
173
+ rescue => e
174
+ raise AssignmentError, "Failed to assign data: #{klass}.#{k} = #{v.inspect}. Message: #{e.message}."
175
+ end
176
+
177
+ id_hash[id] = record
178
+ end
179
+
180
+ self.applied_diff = diff
181
+ self.id_hash = id_hash.freeze
182
+ end
183
+
184
+ class ColumnNotExist < StandardError
185
+ end
186
+
187
+ class AssignmentError < StandardError
188
+ end
189
+
190
+ private
191
+
192
+ def metadata_key?(key)
193
+ key.to_s.start_with?(METADATA_PREFIX)
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "simple_master/storage/table"
4
+
5
+ module SimpleMaster
6
+ module Storage
7
+ # Ondemand table based on updated id_hash.
8
+ class TestTable < Table
9
+ def initialize(_klass, _dataset, _loader)
10
+ super
11
+
12
+ @all = []
13
+ @id_hash = {}
14
+ @grouped_hash = {}
15
+ @class_method_cache = {}
16
+ @method_cache = Hash.new { |k, v| k[v] = {}.compare_by_identity }.compare_by_identity
17
+
18
+ @all_need_update = true
19
+ @grouped_hash_need_update = true
20
+ @class_method_cache_need_update = true
21
+ end
22
+
23
+ def sub_table(sub_klass)
24
+ (@sub_tables ||= klass.descendants.reject(&:abstract_class).index_with { |k| self.class.new(k, dataset, loader) })[sub_klass]
25
+ end
26
+
27
+ def update(id, record)
28
+ id_hash[id] = record
29
+ @all_need_update = true
30
+ @grouped_hash_need_update = true
31
+ @class_method_cache_need_update = true
32
+ end
33
+
34
+ def record_updated
35
+ @grouped_hash_need_update = true
36
+ @class_method_cache_need_update = true
37
+ end
38
+
39
+ def all=(records)
40
+ @all_need_update = false
41
+ super.tap { update_id_hash }
42
+ end
43
+
44
+ def all
45
+ if @all_need_update
46
+ @all_need_update = false
47
+ @all = id_hash.values
48
+ end
49
+ @all
50
+ end
51
+
52
+ def grouped_hash
53
+ if @grouped_hash_need_update
54
+ @grouped_hash_need_update = false
55
+ update_grouped_hash
56
+ end
57
+ @grouped_hash
58
+ end
59
+
60
+ def class_method_cache
61
+ if @class_method_cache_need_update
62
+ @class_method_cache_need_update = false
63
+ update_class_method_cache
64
+ end
65
+ @class_method_cache
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ module Storage
5
+ end
6
+ end
7
+
8
+ require "simple_master/storage/table"
9
+ require "simple_master/storage/ondemand_table"
10
+ require "simple_master/storage/test_table"
11
+ require "simple_master/storage/dataset"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleMaster
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/dependencies"
4
+ require "active_record"
5
+ require "request_store"
6
+ require "logger"
7
+ require "simple_master/version"
8
+
9
+ module SimpleMaster
10
+ EMPTY_ARRAY = [].freeze
11
+ EMPTY_HASH = {}.freeze
12
+
13
+ def self.logger
14
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
15
+ Rails.logger
16
+ else
17
+ @logger ||= Logger.new($stdout)
18
+ end
19
+ end
20
+
21
+ def self.init(for_test: false)
22
+ is_database_available = database_available?
23
+ unless is_database_available
24
+ SimpleMaster.logger.warn "DB not connected. SimpleMaster will not initialize associations to ActiveRecord."
25
+ end
26
+
27
+ yield if block_given?
28
+
29
+ targets.each { |klass| klass.init(is_database_available, for_test: for_test) }
30
+ end
31
+
32
+ def self.targets
33
+ Master.descendants.reject(&:abstract_class)
34
+ end
35
+
36
+ def self.database_available?
37
+ # Raises an error if the DB is missing
38
+ ::ActiveRecord::Base.connection.verify!
39
+
40
+ true
41
+ rescue
42
+ false
43
+ end
44
+
45
+ def self.use_dataset(dataset)
46
+ former_dataset = $current_dataset
47
+ $current_dataset = dataset
48
+ yield
49
+ ensure
50
+ $current_dataset = former_dataset
51
+ end
52
+ end
53
+
54
+ require "simple_master/active_record"
55
+ require "simple_master/loader"
56
+ require "simple_master/schema"
57
+ require "simple_master/storage"
58
+ require "simple_master/master"
59
+
60
+ class ActiveRecord::Associations::Preloader::Association
61
+ prepend SimpleMaster::ActiveRecord::PreloaderAssociationExtension
62
+ end