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.
- checksums.yaml +7 -0
- data/lib/simple_master/active_record/belongs_to_master_polymorphic_reflection.rb +11 -0
- data/lib/simple_master/active_record/belongs_to_polymorphic_association.rb +17 -0
- data/lib/simple_master/active_record/belongs_to_polymorphic_builder.rb +21 -0
- data/lib/simple_master/active_record/extension.rb +183 -0
- data/lib/simple_master/active_record/preloader_association_extension.rb +11 -0
- data/lib/simple_master/active_record.rb +12 -0
- data/lib/simple_master/loader/dataset_loader.rb +20 -0
- data/lib/simple_master/loader/marshal_loader.rb +15 -0
- data/lib/simple_master/loader/query_loader.rb +63 -0
- data/lib/simple_master/loader.rb +55 -0
- data/lib/simple_master/master/association/belongs_to_association.rb +79 -0
- data/lib/simple_master/master/association/belongs_to_polymorphic_association.rb +79 -0
- data/lib/simple_master/master/association/has_many_association.rb +53 -0
- data/lib/simple_master/master/association/has_many_through_association.rb +64 -0
- data/lib/simple_master/master/association/has_one_association.rb +57 -0
- data/lib/simple_master/master/association.rb +50 -0
- data/lib/simple_master/master/column/bitmask_column.rb +74 -0
- data/lib/simple_master/master/column/boolean_column.rb +51 -0
- data/lib/simple_master/master/column/enum_column.rb +96 -0
- data/lib/simple_master/master/column/float_column.rb +21 -0
- data/lib/simple_master/master/column/id_column.rb +31 -0
- data/lib/simple_master/master/column/integer_column.rb +21 -0
- data/lib/simple_master/master/column/json_column.rb +27 -0
- data/lib/simple_master/master/column/polymorphic_type_column.rb +44 -0
- data/lib/simple_master/master/column/sti_type_column.rb +21 -0
- data/lib/simple_master/master/column/string_column.rb +17 -0
- data/lib/simple_master/master/column/symbol_column.rb +23 -0
- data/lib/simple_master/master/column/time_column.rb +38 -0
- data/lib/simple_master/master/column.rb +138 -0
- data/lib/simple_master/master/dsl.rb +239 -0
- data/lib/simple_master/master/editable.rb +155 -0
- data/lib/simple_master/master/filterable.rb +47 -0
- data/lib/simple_master/master/queryable.rb +75 -0
- data/lib/simple_master/master/storable.rb +20 -0
- data/lib/simple_master/master/validatable.rb +216 -0
- data/lib/simple_master/master.rb +417 -0
- data/lib/simple_master/schema.rb +49 -0
- data/lib/simple_master/storage/dataset.rb +172 -0
- data/lib/simple_master/storage/ondemand_table.rb +68 -0
- data/lib/simple_master/storage/table.rb +197 -0
- data/lib/simple_master/storage/test_table.rb +69 -0
- data/lib/simple_master/storage.rb +11 -0
- data/lib/simple_master/version.rb +5 -0
- data/lib/simple_master.rb +62 -0
- metadata +128 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
class Column
|
|
6
|
+
attr_reader :name
|
|
7
|
+
attr_reader :options
|
|
8
|
+
attr_accessor :group_key
|
|
9
|
+
|
|
10
|
+
def self.column_type
|
|
11
|
+
klass_name = name.split("::").last
|
|
12
|
+
type_name = klass_name.delete_suffix('Column')
|
|
13
|
+
return :column if type_name.empty?
|
|
14
|
+
|
|
15
|
+
type_name.underscore.to_sym
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.inherited(subclass)
|
|
19
|
+
type = subclass.column_type
|
|
20
|
+
Column.register(type, subclass)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.column_types
|
|
24
|
+
@column_types ||= {}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.register(type, klass)
|
|
28
|
+
if column_types.key?(type)
|
|
29
|
+
fail "#{klass}: Column type #{type} is defined at #{column_types[type]}."
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
column_types[type] = klass
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
register(:column, self)
|
|
36
|
+
|
|
37
|
+
def initialize(name, options)
|
|
38
|
+
@name = name
|
|
39
|
+
@group_key = !!options[:group_key]
|
|
40
|
+
@options = options
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def init(master_class, for_test = false)
|
|
44
|
+
master_class.simple_master_module.attr_reader name
|
|
45
|
+
|
|
46
|
+
master_class.simple_master_module.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
47
|
+
def #{name}=(value)
|
|
48
|
+
#{code_for_conversion}
|
|
49
|
+
#{code_for_dirty_check if for_test}
|
|
50
|
+
@#{name} = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def #{name}_value_for_sql
|
|
54
|
+
value = #{code_for_sql_value}
|
|
55
|
+
return "NULL" if value.nil?
|
|
56
|
+
return "'" + value.gsub(/'/, "''").gsub("\\\\", '\\&\\&') + "'" if value.is_a?(String)
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# For inspecting raw DB/CSV values when checking CSV diffs
|
|
61
|
+
def #{name}_value_for_csv
|
|
62
|
+
#{code_for_sql_value}
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
|
|
66
|
+
globalize(master_class) if options[:globalize]
|
|
67
|
+
|
|
68
|
+
if options[:db_column_name]
|
|
69
|
+
master_class.simple_master_module.alias_method :"#{options[:db_column_name]}=", :"#{name}="
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def db_column_name
|
|
74
|
+
options[:db_column_name] || name
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def code_for_conversion
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def globalize(master_class)
|
|
83
|
+
fail "#{master_class}.#{name}: group key can not be globalized." if options[:group_key]
|
|
84
|
+
|
|
85
|
+
mod = Module.new
|
|
86
|
+
mod.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
87
|
+
def #{name}
|
|
88
|
+
return super if @_globalized_#{name}.nil?
|
|
89
|
+
@_globalized_#{name}.fetch(I18n.locale) { super }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
attr_reader :_globalized_#{name}
|
|
93
|
+
|
|
94
|
+
def _globalized_#{name}=(hash_or_json)
|
|
95
|
+
hash = hash_or_json.is_a?(String) ? JSON.parse(hash_or_json, symbolize_names: true) : hash_or_json
|
|
96
|
+
@_globalized_#{name} = hash&.transform_values { |value|
|
|
97
|
+
#{code_for_conversion}
|
|
98
|
+
value
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def _globalized_#{name}_set(locale, value)
|
|
103
|
+
@_globalized_#{name} ||= {}
|
|
104
|
+
#{code_for_conversion}
|
|
105
|
+
@_globalized_#{name}[locale] = value
|
|
106
|
+
end
|
|
107
|
+
RUBY
|
|
108
|
+
|
|
109
|
+
master_class.simple_master_module.prepend(mod)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def code_for_dirty_check
|
|
113
|
+
<<-RUBY
|
|
114
|
+
dirty! unless @#{name} == value
|
|
115
|
+
RUBY
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def code_for_sql_value
|
|
119
|
+
<<-RUBY
|
|
120
|
+
self.#{name}
|
|
121
|
+
RUBY
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
require "simple_master/master/column/id_column"
|
|
128
|
+
require "simple_master/master/column/integer_column"
|
|
129
|
+
require "simple_master/master/column/float_column"
|
|
130
|
+
require "simple_master/master/column/string_column"
|
|
131
|
+
require "simple_master/master/column/symbol_column"
|
|
132
|
+
require "simple_master/master/column/boolean_column"
|
|
133
|
+
require "simple_master/master/column/json_column"
|
|
134
|
+
require "simple_master/master/column/time_column"
|
|
135
|
+
require "simple_master/master/column/enum_column"
|
|
136
|
+
require "simple_master/master/column/bitmask_column"
|
|
137
|
+
require "simple_master/master/column/sti_type_column"
|
|
138
|
+
require "simple_master/master/column/polymorphic_type_column"
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
# Items declared at class-definition time are grouped inside this module
|
|
6
|
+
module Dsl
|
|
7
|
+
TYPES_BY_OPTIONS = {
|
|
8
|
+
polymorphic_type: Column::PolymorphicTypeColumn,
|
|
9
|
+
sti: Column::StiTypeColumn,
|
|
10
|
+
enum: Column::EnumColumn,
|
|
11
|
+
bitmask: Column::BitmaskColumn,
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def def_column(column_name, options = {})
|
|
15
|
+
column = column_type(column_name, options).new(column_name, options)
|
|
16
|
+
columns << column
|
|
17
|
+
|
|
18
|
+
if options[:sti]
|
|
19
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
20
|
+
def self.sti_base_class
|
|
21
|
+
#{name}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.sti_column
|
|
25
|
+
:#{column_name}
|
|
26
|
+
end
|
|
27
|
+
RUBY
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def has_one(name, options = EMPTY_HASH)
|
|
32
|
+
ass = Association::HasOneAssociation.new(self, name, options)
|
|
33
|
+
has_one_associations << ass
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def has_many(name, options = {})
|
|
37
|
+
ass =
|
|
38
|
+
if options[:through]
|
|
39
|
+
Association::HasManyThroughAssociation.new(self, name, options)
|
|
40
|
+
else
|
|
41
|
+
Association::HasManyAssociation.new(self, name, options)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
has_many_associations << ass
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def belongs_to(name, options = EMPTY_HASH)
|
|
48
|
+
ass =
|
|
49
|
+
if options[:polymorphic]
|
|
50
|
+
Association::BelongsToPolymorphicAssociation.new(self, name, options)
|
|
51
|
+
else
|
|
52
|
+
Association::BelongsToAssociation.new(self, name, options)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
belongs_to_associations << ass
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def group_key(column_name)
|
|
59
|
+
proc {
|
|
60
|
+
update_column_info(column_name) do |column|
|
|
61
|
+
column.group_key = true
|
|
62
|
+
|
|
63
|
+
column
|
|
64
|
+
end
|
|
65
|
+
}.tap { dsl_initializers << _1 }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def enum(*args)
|
|
69
|
+
if args.length == 1
|
|
70
|
+
options = args.first
|
|
71
|
+
proc {
|
|
72
|
+
options.each do |name, enums|
|
|
73
|
+
update_column_info(name) do |column|
|
|
74
|
+
Column::EnumColumn.new(name, column.options.merge(enum: enums))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
}.tap { dsl_initializers << _1 }
|
|
78
|
+
elsif args.length >= 2
|
|
79
|
+
name = args.shift
|
|
80
|
+
enums = args.shift
|
|
81
|
+
options = args.shift || {}
|
|
82
|
+
|
|
83
|
+
proc {
|
|
84
|
+
update_column_info(name) do |column|
|
|
85
|
+
Column::EnumColumn.new(name, column.options.merge(options.merge(enum: enums)))
|
|
86
|
+
end
|
|
87
|
+
}.tap { dsl_initializers << _1 }
|
|
88
|
+
else
|
|
89
|
+
fail
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def bitmask(name, options = Association)
|
|
94
|
+
proc {
|
|
95
|
+
update_column_info(name) do |column|
|
|
96
|
+
Column::BitmaskColumn.new(name, column.options.merge(bitmask: options[:as]))
|
|
97
|
+
end
|
|
98
|
+
}.tap { dsl_initializers << _1 }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def globalize(column_name)
|
|
102
|
+
proc {
|
|
103
|
+
update_column_info(column_name) do |column|
|
|
104
|
+
column.options[:globalize] = true
|
|
105
|
+
column
|
|
106
|
+
end
|
|
107
|
+
}.tap { dsl_initializers << _1 }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# * Cache generated at load time and stored in the dataset.
|
|
111
|
+
#
|
|
112
|
+
# cache_class_method method_name_symbol
|
|
113
|
+
#
|
|
114
|
+
# * Can also be declared using the return value of a def:
|
|
115
|
+
#
|
|
116
|
+
# cache_class_method def self.method_name
|
|
117
|
+
# ...
|
|
118
|
+
# end
|
|
119
|
+
#
|
|
120
|
+
# * Pass a block to generate the cache from the block instead of an existing method.
|
|
121
|
+
#
|
|
122
|
+
# cache_class_method method_name_symbol { ... }
|
|
123
|
+
#
|
|
124
|
+
# * Defines class methods; multiple caches can be generated by passing several args.
|
|
125
|
+
#
|
|
126
|
+
# cache_class_method(:cache1, :cache2) {
|
|
127
|
+
# ...
|
|
128
|
+
# [cache1, cache2]
|
|
129
|
+
# }
|
|
130
|
+
def cache_class_method(*args, &block)
|
|
131
|
+
@class_method_cache_info ||= []
|
|
132
|
+
if block_given?
|
|
133
|
+
@class_method_cache_info << [args, block]
|
|
134
|
+
else
|
|
135
|
+
args.each do |arg|
|
|
136
|
+
method_name = :"_class_cache_#{arg}"
|
|
137
|
+
@class_method_cache_info << [[arg], method_name]
|
|
138
|
+
singleton_class.alias_method method_name, arg
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
args.each do |arg|
|
|
143
|
+
define_singleton_method(arg) do
|
|
144
|
+
class_method_cache[arg]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
alias def_custom_cache cache_class_method
|
|
150
|
+
|
|
151
|
+
# Access the method once after the record is loaded
|
|
152
|
+
def tap_instance_method(method_name)
|
|
153
|
+
@instance_methods_need_tap ||= []
|
|
154
|
+
@instance_methods_need_tap << method_name
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# * Cache generated at load time and stored in an instance variable.
|
|
158
|
+
# Lightweight, but must not reference the dataset, so use with care.
|
|
159
|
+
# Intended for caches derived from record attributes.
|
|
160
|
+
#
|
|
161
|
+
# cache_attribute method_name_symbol
|
|
162
|
+
#
|
|
163
|
+
# * Can also be declared using the return value of a def:
|
|
164
|
+
#
|
|
165
|
+
# cache_attribute def method_name
|
|
166
|
+
# ...
|
|
167
|
+
# end
|
|
168
|
+
#
|
|
169
|
+
# * Passing a block generates the cache from the block.
|
|
170
|
+
# cache_attribute method_name { ... }
|
|
171
|
+
def cache_attribute(method_name, &)
|
|
172
|
+
calc_method_name = :"_attribute_cache_#{method_name}"
|
|
173
|
+
|
|
174
|
+
if block_given?
|
|
175
|
+
define_method(calc_method_name, &)
|
|
176
|
+
else
|
|
177
|
+
alias_method calc_method_name, method_name
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
181
|
+
def #{method_name}
|
|
182
|
+
return @#{method_name} if defined? @#{method_name}
|
|
183
|
+
|
|
184
|
+
@#{method_name} = #{calc_method_name}
|
|
185
|
+
end
|
|
186
|
+
RUBY
|
|
187
|
+
|
|
188
|
+
tap_instance_method method_name
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# * Cache generated at load time and stored in the dataset.
|
|
192
|
+
# Heavier than cache_attribute because it uses the dataset.
|
|
193
|
+
#
|
|
194
|
+
# cache_method method_name_symbol
|
|
195
|
+
#
|
|
196
|
+
# * Can also be declared using the return value of a def:
|
|
197
|
+
#
|
|
198
|
+
# cache_method def method_name
|
|
199
|
+
# ...
|
|
200
|
+
# end
|
|
201
|
+
#
|
|
202
|
+
# * Passing a block generates the cache from the block.
|
|
203
|
+
#
|
|
204
|
+
# cache_method method_name_symbol { ... }
|
|
205
|
+
def cache_method(method_name, &)
|
|
206
|
+
calc_method_name = :"_method_cache_#{method_name}"
|
|
207
|
+
if block_given?
|
|
208
|
+
define_method(calc_method_name, &)
|
|
209
|
+
else
|
|
210
|
+
alias_method calc_method_name, method_name
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
214
|
+
def #{method_name}
|
|
215
|
+
self.class.method_cache[:#{method_name}].fetch(self) {
|
|
216
|
+
self.class.method_cache[:#{method_name}][self] = #{calc_method_name}
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
RUBY
|
|
220
|
+
|
|
221
|
+
tap_instance_method method_name
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
private
|
|
225
|
+
|
|
226
|
+
def column_type(column_name, options)
|
|
227
|
+
return Column::IdColumn if column_name == :id
|
|
228
|
+
|
|
229
|
+
TYPES_BY_OPTIONS.each do |option_key, type|
|
|
230
|
+
return type if options[option_key]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
return Column unless options[:type]
|
|
234
|
+
|
|
235
|
+
Column.column_types.fetch(options[:type]) { fail "Undefined column type: #{options[:type]}" }
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
# Provide save! helpers for test data
|
|
6
|
+
module Editable
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def create(...)
|
|
11
|
+
new(...).save
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def create!(...)
|
|
15
|
+
new(...).save!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
def dirty!
|
|
19
|
+
@dirty = true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new_record?
|
|
23
|
+
id.nil? || self.class.id_hash[id] != self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def has_changes_to_save?
|
|
27
|
+
!@dirty.nil?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def record_to_save
|
|
31
|
+
# Assumes type_not_match? has already been checked
|
|
32
|
+
return nil if type.nil?
|
|
33
|
+
return @record_to_save if @record_to_save && @record_to_save.type == type
|
|
34
|
+
|
|
35
|
+
@record_to_save = type.constantize.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def type_not_match?
|
|
39
|
+
self.class.sti_class? && type != self.class.to_s
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Use a generator that avoids ID collisions across classes
|
|
43
|
+
@@generated_id = 0 # rubocop:disable Style/ClassVars
|
|
44
|
+
def generate_id
|
|
45
|
+
loop do
|
|
46
|
+
@@generated_id += 1 # rubocop:disable Style/ClassVars
|
|
47
|
+
break unless self.class.base_class.id_hash.key?(@@generated_id)
|
|
48
|
+
end
|
|
49
|
+
@@generated_id
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# FOR TEST
|
|
53
|
+
def save(**_options)
|
|
54
|
+
save!
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def save!(**_options)
|
|
58
|
+
return if @saving
|
|
59
|
+
@saving = true
|
|
60
|
+
if id.nil?
|
|
61
|
+
self.id = generate_id
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Save belongs_to.
|
|
65
|
+
association_records = belongs_to_store.dup
|
|
66
|
+
association_records.each do |association_name, record|
|
|
67
|
+
next unless record
|
|
68
|
+
|
|
69
|
+
unless send(:"_#{association_name}_target_save?")
|
|
70
|
+
belongs_to_store.delete(association_name)
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
record.save
|
|
75
|
+
send(:"#{association_name}=", record)
|
|
76
|
+
belongs_to_store.delete(association_name) if record.is_a?(SimpleMaster::Master)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if @dirty
|
|
80
|
+
# Update this table
|
|
81
|
+
if type_not_match?
|
|
82
|
+
# If data would be created on the parent class, discard that instance and save separately
|
|
83
|
+
record_to_save&.update!(attributes)
|
|
84
|
+
else
|
|
85
|
+
if new_record?
|
|
86
|
+
id_updated = true
|
|
87
|
+
current_and_super_classes_each { |klass| klass.master_storage.update(id, self) }
|
|
88
|
+
SimpleMaster.logger.debug { "[SimpleMaster] Created: #{self.class}##{id}" }
|
|
89
|
+
else
|
|
90
|
+
current_and_super_classes_each { |klass| klass.master_storage.record_updated }
|
|
91
|
+
SimpleMaster.logger.debug { "[SimpleMaster] Updated: #{self.class}##{id}" }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
@dirty = false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# save has_many
|
|
98
|
+
association_records = has_many_store.dup
|
|
99
|
+
|
|
100
|
+
association_records.each do |association_name, records|
|
|
101
|
+
association = (self.class.all_has_many_associations | self.class.all_has_one_associations).find { |ass| ass.name == association_name }
|
|
102
|
+
records.each do |record|
|
|
103
|
+
if id_updated
|
|
104
|
+
record.send(:"#{association.foreign_key}=", send(association.primary_key))
|
|
105
|
+
end
|
|
106
|
+
record.save
|
|
107
|
+
end
|
|
108
|
+
has_many_store.delete(association_name) if association.target_class < Master
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
@saving = false
|
|
112
|
+
|
|
113
|
+
self
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def current_and_super_classes_each
|
|
117
|
+
klass = self.class
|
|
118
|
+
yield klass
|
|
119
|
+
|
|
120
|
+
loop do
|
|
121
|
+
klass = klass.superclass
|
|
122
|
+
break unless klass < Master
|
|
123
|
+
next if klass.abstract_class?
|
|
124
|
+
|
|
125
|
+
yield klass
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def update(attributes)
|
|
130
|
+
update!(attributes)
|
|
131
|
+
true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def update!(attributes)
|
|
135
|
+
attributes.each do |key, value|
|
|
136
|
+
send :"#{key}=", value
|
|
137
|
+
end
|
|
138
|
+
save!
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def destroy
|
|
142
|
+
destroy!
|
|
143
|
+
true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def destroy!
|
|
147
|
+
fail "Destroy is not allowed"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# NOTE: no freezing
|
|
151
|
+
def freeze
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
# Lookup helper methods
|
|
6
|
+
module Filterable
|
|
7
|
+
def find(id)
|
|
8
|
+
id_hash.fetch(id)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def find_by_id(id)
|
|
12
|
+
id_hash[id]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find_by_ids(ids)
|
|
16
|
+
id_hash.values_at(*ids).compact
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def find_by_ids!(ids)
|
|
20
|
+
id_hash.fetch_values(*ids)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_by(key, value)
|
|
24
|
+
all_by(key, value).first
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def all_by(key, value)
|
|
28
|
+
grouped_hash.fetch(key).fetch(value) { EMPTY_ARRAY }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def all_by!(key, value)
|
|
32
|
+
grouped_hash.fetch(key).fetch(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def all_in(key, values)
|
|
36
|
+
# NOTE: Avoid Array#flatten because it is slow here
|
|
37
|
+
grouped_hash.fetch(key).fetch_values(*values) { EMPTY_ARRAY }.flat_map(&:itself).freeze
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def exists?(key)
|
|
41
|
+
id_hash.key?(key)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
delegate :pluck, :map, :first, :last, :each, :select, to: :all
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
# Query in database
|
|
6
|
+
module Queryable
|
|
7
|
+
def query_select_all
|
|
8
|
+
connection.select_all("SELECT * from #{table_name}")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def query_upsert_records(records, batch_size: 10000)
|
|
12
|
+
insert_queries(records, true, batch_size: batch_size).each do |sql|
|
|
13
|
+
connection.execute(sql)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def insert_queries(records, on_duplicate_key_update = false, batch_size: 10000)
|
|
18
|
+
return [] if records.empty?
|
|
19
|
+
|
|
20
|
+
column_names = all_columns.map(&:name)
|
|
21
|
+
db_column_names = all_columns.map(&:db_column_name)
|
|
22
|
+
sql_columns = db_column_names.map { "`#{_1}`" }.join(", ").then { "(#{_1})" }
|
|
23
|
+
|
|
24
|
+
sql_column_methods = column_names.map { |column_name| :"#{column_name}_value_for_sql" }
|
|
25
|
+
current_time = "'#{Time.now.utc.strftime('%Y-%m-%d %H:%M:%S')}'"
|
|
26
|
+
|
|
27
|
+
if on_duplicate_key_update
|
|
28
|
+
sql_update = db_column_names.filter_map { |column_name| "`#{column_name}` = new.#{column_name}" if column_name != :created_at }.join(", ")
|
|
29
|
+
on_duplicate_key_update_sql = " AS new ON DUPLICATE KEY UPDATE #{sql_update}"
|
|
30
|
+
else
|
|
31
|
+
on_duplicate_key_update_sql = ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
records.each_slice(batch_size).map { |sliced_records|
|
|
35
|
+
values_sql =
|
|
36
|
+
sliced_records.map { |record|
|
|
37
|
+
sql_column_methods
|
|
38
|
+
.zip(column_names)
|
|
39
|
+
.map { |method_name, column_name|
|
|
40
|
+
if [:updated_at, :created_at].include?(column_name)
|
|
41
|
+
current_time
|
|
42
|
+
else
|
|
43
|
+
record.send(method_name)
|
|
44
|
+
end
|
|
45
|
+
}.join(", ").then { "(#{_1})" }
|
|
46
|
+
}.join(", \n")
|
|
47
|
+
|
|
48
|
+
"INSERT INTO `#{table_name}` \n#{sql_columns} VALUES \n#{values_sql}#{on_duplicate_key_update_sql};\n"
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def sqlite_insert_query(records)
|
|
53
|
+
insert_queries(records, false).join("\n").gsub("\\\\", "\\")
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def query_delete_all
|
|
57
|
+
connection.execute(delete_all_query)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def delete_all_query
|
|
61
|
+
"DELETE FROM #{table_name};"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def table_available?
|
|
65
|
+
connection.table_exists? table_name
|
|
66
|
+
rescue
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def connection
|
|
71
|
+
::ActiveRecord::Base.connection
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleMaster
|
|
4
|
+
class Master
|
|
5
|
+
module Storable
|
|
6
|
+
def master_storage(dataset = $current_dataset)
|
|
7
|
+
dataset.table(self)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
delegate :all, :all=, :id_hash, :id_hash=, :grouped_hash, :grouped_hash=, :class_method_cache, :class_method_cache=, :method_cache, to: :master_storage
|
|
11
|
+
delegate :update_id_hash, :update_grouped_hash, :update_class_method_cache, :tap_instance_methods, :freeze_all, to: :master_storage
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module SubClassStorable
|
|
15
|
+
def master_storage(dataset = $current_dataset)
|
|
16
|
+
dataset.table(sti_base_class).sub_table(self)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|