mr 0.35.2
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/.gitignore +19 -0
- data/Gemfile +13 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/bench/all.rb +4 -0
- data/bench/factory.rb +68 -0
- data/bench/fake_record.rb +174 -0
- data/bench/model.rb +201 -0
- data/bench/read_model.rb +191 -0
- data/bench/results/factory.txt +21 -0
- data/bench/results/fake_record.txt +37 -0
- data/bench/results/model.txt +44 -0
- data/bench/results/read_model.txt +46 -0
- data/bench/setup.rb +132 -0
- data/lib/mr.rb +11 -0
- data/lib/mr/after_commit.rb +49 -0
- data/lib/mr/after_commit/fake_record.rb +39 -0
- data/lib/mr/after_commit/record.rb +48 -0
- data/lib/mr/after_commit/record_procs_methods.rb +82 -0
- data/lib/mr/factory.rb +82 -0
- data/lib/mr/factory/config.rb +240 -0
- data/lib/mr/factory/model_factory.rb +103 -0
- data/lib/mr/factory/model_stack.rb +28 -0
- data/lib/mr/factory/read_model_factory.rb +104 -0
- data/lib/mr/factory/record_factory.rb +130 -0
- data/lib/mr/factory/record_stack.rb +219 -0
- data/lib/mr/fake_query.rb +53 -0
- data/lib/mr/fake_record.rb +58 -0
- data/lib/mr/fake_record/associations.rb +257 -0
- data/lib/mr/fake_record/attributes.rb +168 -0
- data/lib/mr/fake_record/persistence.rb +116 -0
- data/lib/mr/json_field.rb +180 -0
- data/lib/mr/json_field/fake_record.rb +31 -0
- data/lib/mr/json_field/record.rb +38 -0
- data/lib/mr/model.rb +67 -0
- data/lib/mr/model/associations.rb +161 -0
- data/lib/mr/model/configuration.rb +67 -0
- data/lib/mr/model/fields.rb +177 -0
- data/lib/mr/model/persistence.rb +79 -0
- data/lib/mr/query.rb +126 -0
- data/lib/mr/read_model.rb +83 -0
- data/lib/mr/read_model/data.rb +38 -0
- data/lib/mr/read_model/fields.rb +218 -0
- data/lib/mr/read_model/query_expression.rb +188 -0
- data/lib/mr/read_model/querying.rb +214 -0
- data/lib/mr/read_model/set_querying.rb +82 -0
- data/lib/mr/read_model/subquery.rb +98 -0
- data/lib/mr/record.rb +35 -0
- data/lib/mr/test_helpers.rb +229 -0
- data/lib/mr/type_converter.rb +85 -0
- data/lib/mr/version.rb +3 -0
- data/log/.gitkeep +0 -0
- data/mr.gemspec +29 -0
- data/test/helper.rb +21 -0
- data/test/support/db.rb +10 -0
- data/test/support/factory.rb +13 -0
- data/test/support/factory/area.rb +6 -0
- data/test/support/factory/comment.rb +14 -0
- data/test/support/factory/image.rb +6 -0
- data/test/support/factory/user.rb +6 -0
- data/test/support/models/area.rb +58 -0
- data/test/support/models/comment.rb +60 -0
- data/test/support/models/image.rb +53 -0
- data/test/support/models/user.rb +96 -0
- data/test/support/read_model/querying.rb +150 -0
- data/test/support/read_models/comment_with_user_data.rb +27 -0
- data/test/support/read_models/set_data.rb +49 -0
- data/test/support/read_models/subquery_data.rb +41 -0
- data/test/support/read_models/user_with_area_data.rb +15 -0
- data/test/support/schema.rb +39 -0
- data/test/support/setup_test_db.rb +10 -0
- data/test/system/factory/model_factory_tests.rb +87 -0
- data/test/system/factory/model_stack_tests.rb +30 -0
- data/test/system/factory/record_factory_tests.rb +84 -0
- data/test/system/factory/record_stack_tests.rb +51 -0
- data/test/system/factory_tests.rb +32 -0
- data/test/system/read_model_tests.rb +199 -0
- data/test/system/with_model_tests.rb +275 -0
- data/test/unit/after_commit/fake_record_tests.rb +110 -0
- data/test/unit/after_commit/record_procs_methods_tests.rb +177 -0
- data/test/unit/after_commit/record_tests.rb +134 -0
- data/test/unit/after_commit_tests.rb +113 -0
- data/test/unit/factory/config_tests.rb +651 -0
- data/test/unit/factory/model_factory_tests.rb +473 -0
- data/test/unit/factory/model_stack_tests.rb +97 -0
- data/test/unit/factory/read_model_factory_tests.rb +195 -0
- data/test/unit/factory/record_factory_tests.rb +446 -0
- data/test/unit/factory/record_stack_tests.rb +549 -0
- data/test/unit/factory_tests.rb +213 -0
- data/test/unit/fake_query_tests.rb +137 -0
- data/test/unit/fake_record/associations_tests.rb +585 -0
- data/test/unit/fake_record/attributes_tests.rb +265 -0
- data/test/unit/fake_record/persistence_tests.rb +239 -0
- data/test/unit/fake_record_tests.rb +106 -0
- data/test/unit/json_field/fake_record_tests.rb +75 -0
- data/test/unit/json_field/record_tests.rb +80 -0
- data/test/unit/json_field_tests.rb +302 -0
- data/test/unit/model/associations_tests.rb +346 -0
- data/test/unit/model/configuration_tests.rb +92 -0
- data/test/unit/model/fields_tests.rb +278 -0
- data/test/unit/model/persistence_tests.rb +114 -0
- data/test/unit/model_tests.rb +137 -0
- data/test/unit/query_tests.rb +300 -0
- data/test/unit/read_model/data_tests.rb +56 -0
- data/test/unit/read_model/fields_tests.rb +416 -0
- data/test/unit/read_model/query_expression_tests.rb +381 -0
- data/test/unit/read_model/querying_tests.rb +613 -0
- data/test/unit/read_model/set_querying_tests.rb +149 -0
- data/test/unit/read_model/subquery_tests.rb +242 -0
- data/test/unit/read_model_tests.rb +187 -0
- data/test/unit/record_tests.rb +45 -0
- data/test/unit/test_helpers_tests.rb +431 -0
- data/test/unit/type_converter_tests.rb +207 -0
- metadata +285 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/read_model/data'
|
3
|
+
require 'mr/read_model/fields'
|
4
|
+
require 'mr/read_model/querying'
|
5
|
+
|
6
|
+
module MR
|
7
|
+
|
8
|
+
module ReadModel
|
9
|
+
include MuchPlugin
|
10
|
+
|
11
|
+
plugin_included do
|
12
|
+
include MR::ReadModelStruct
|
13
|
+
include MR::ReadModel::Querying
|
14
|
+
extend ClassMethods
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.add_select_for_field(read_model_class, name, column_sql = nil, &column_sql_block)
|
18
|
+
if column_sql
|
19
|
+
read_model_class.select("#{column_sql} AS #{name}")
|
20
|
+
elsif column_sql_block
|
21
|
+
read_model_class.select do |params|
|
22
|
+
"#{column_sql_block.call(params)} AS #{name}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
|
29
|
+
def field(name, type, column_sql = nil, &column_sql_block)
|
30
|
+
super(name, type)
|
31
|
+
MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
|
32
|
+
end
|
33
|
+
|
34
|
+
def json_struct_list(name, struct_class, column_sql = nil, &column_sql_block)
|
35
|
+
super(name, struct_class)
|
36
|
+
MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def json_struct_obj(name, struct_class, column_sql = nil, &column_sql_block)
|
40
|
+
super(name, struct_class)
|
41
|
+
MR::ReadModel.add_select_for_field(self, name, column_sql, &column_sql_block)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
module ReadModelStruct
|
49
|
+
include MuchPlugin
|
50
|
+
|
51
|
+
plugin_included do
|
52
|
+
include MR::ReadModel::Data
|
53
|
+
include MR::ReadModel::Fields
|
54
|
+
include InstanceMethods
|
55
|
+
end
|
56
|
+
|
57
|
+
module InstanceMethods
|
58
|
+
|
59
|
+
def initialize(data = nil)
|
60
|
+
set_read_model_data(data || {})
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
if other.kind_of?(self.class)
|
65
|
+
self.fields == other.fields
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def inspect
|
72
|
+
object_hex = (self.object_id << 1).to_s(16)
|
73
|
+
fields_inspect = self.class.fields.map do |field|
|
74
|
+
"#{field.ivar_name}=#{field.read(self.read_model_data).inspect}"
|
75
|
+
end.sort.join(" ")
|
76
|
+
"#<#{self.class}:0x#{object_hex} #{fields_inspect}>"
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module MR; end
|
2
|
+
module MR::ReadModel
|
3
|
+
|
4
|
+
module Data
|
5
|
+
|
6
|
+
# `MR::ReadModel::Data` is a mixin that provides helpers for setting and
|
7
|
+
# accessing the "data" for a read model. These methods provide a strict
|
8
|
+
# interface to avoid confusing errors and ensure that the data for a
|
9
|
+
# read model should, as much as possible, work.
|
10
|
+
#
|
11
|
+
# * Use the `read_model_data` protected method to access the data object.
|
12
|
+
# * Use the `set_read_model_data` private method to write a data object.
|
13
|
+
|
14
|
+
protected
|
15
|
+
|
16
|
+
def read_model_data
|
17
|
+
@read_model_data || raise(NoDataError.new(caller))
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def set_read_model_data(data)
|
23
|
+
raise InvalidDataError unless data.respond_to?(:[])
|
24
|
+
@read_model_data = data
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
InvalidDataError = Class.new(ArgumentError)
|
30
|
+
|
31
|
+
class NoDataError < RuntimeError
|
32
|
+
def initialize(called_from = nil)
|
33
|
+
super "the read model's data hasn't been set"
|
34
|
+
set_backtrace(called_from) if called_from
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'much-plugin'
|
2
|
+
require 'mr/json_field'
|
3
|
+
require 'mr/read_model/data'
|
4
|
+
require 'mr/type_converter'
|
5
|
+
|
6
|
+
module MR; end
|
7
|
+
module MR::ReadModel
|
8
|
+
|
9
|
+
module Fields
|
10
|
+
include MuchPlugin
|
11
|
+
|
12
|
+
plugin_included do
|
13
|
+
include MR::ReadModel::Data
|
14
|
+
extend ClassMethods
|
15
|
+
include InstanceMethods
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
def fields
|
21
|
+
@fields ||= MR::ReadModel::FieldSet.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def field(name, type)
|
25
|
+
fields.add(name, type, self)
|
26
|
+
rescue InvalidFieldTypeError => exception
|
27
|
+
raise ArgumentError, exception.message, caller
|
28
|
+
end
|
29
|
+
|
30
|
+
def json_struct_list(name, struct_class)
|
31
|
+
field = JsonStructListField.new(self, name.to_s, struct_class)
|
32
|
+
ivar_name = "@#{name}"
|
33
|
+
|
34
|
+
define_method(name) do
|
35
|
+
begin
|
36
|
+
instance_variable_get(ivar_name) ||
|
37
|
+
instance_variable_set(ivar_name, field.reader(self.read_model_data))
|
38
|
+
rescue StandardError => exception
|
39
|
+
exception.set_backtrace(caller)
|
40
|
+
raise exception
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
(self.json_struct_lists << field).uniq!
|
45
|
+
end
|
46
|
+
|
47
|
+
def json_struct_lists
|
48
|
+
@json_struct_lists ||= []
|
49
|
+
end
|
50
|
+
|
51
|
+
def json_struct_obj(name, struct_class)
|
52
|
+
field = JsonStructObjField.new(self, name.to_s, struct_class)
|
53
|
+
ivar_name = "@#{name}"
|
54
|
+
|
55
|
+
define_method(name) do
|
56
|
+
begin
|
57
|
+
instance_variable_get(ivar_name) ||
|
58
|
+
instance_variable_set(ivar_name, field.reader(self.read_model_data))
|
59
|
+
rescue StandardError => exception
|
60
|
+
exception.set_backtrace(caller)
|
61
|
+
raise exception
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
(self.json_struct_objs << field).uniq!
|
66
|
+
end
|
67
|
+
|
68
|
+
def json_struct_objs
|
69
|
+
@json_struct_objs ||= []
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
module InstanceMethods
|
75
|
+
|
76
|
+
def fields
|
77
|
+
@fields ||= begin
|
78
|
+
fields = self.class.fields.read_all(self.read_model_data)
|
79
|
+
( self.class.json_struct_lists +
|
80
|
+
self.class.json_struct_objs
|
81
|
+
).inject(fields) do |h, field|
|
82
|
+
h.merge!(field.name => self.send(field.name))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
class FieldSet
|
92
|
+
include Enumerable
|
93
|
+
|
94
|
+
def initialize
|
95
|
+
@fields = []
|
96
|
+
end
|
97
|
+
|
98
|
+
def find(name)
|
99
|
+
@fields.detect{ |f| f.name == name.to_s }
|
100
|
+
end
|
101
|
+
|
102
|
+
def read_all(data)
|
103
|
+
inject({}) do |h, field|
|
104
|
+
h.merge(field.name => field.read(data))
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def add(name, type, model_class = nil)
|
109
|
+
@fields << Field.new(name, type).tap do |field|
|
110
|
+
field.define_on(model_class) if model_class
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def each(&block)
|
115
|
+
@fields.each(&block)
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
class Field
|
121
|
+
attr_reader :name, :type, :value
|
122
|
+
attr_reader :method_name, :ivar_name
|
123
|
+
|
124
|
+
def initialize(name, type)
|
125
|
+
@name = name.to_s
|
126
|
+
@type = type.to_sym
|
127
|
+
unless MR::TypeConverter.valid?(@type)
|
128
|
+
raise InvalidFieldTypeError, "`#{@type}` is not a valid field type"
|
129
|
+
end
|
130
|
+
|
131
|
+
@method_name = @name
|
132
|
+
@ivar_name = "@#{@name}"
|
133
|
+
@attribute_name = @name
|
134
|
+
@converter = nil
|
135
|
+
end
|
136
|
+
|
137
|
+
def read(data)
|
138
|
+
@converter ||= MR::TypeConverter.new(determine_ar_column_class(data))
|
139
|
+
@converter.send(@type, data[@attribute_name])
|
140
|
+
end
|
141
|
+
|
142
|
+
def define_on(model_class)
|
143
|
+
field = self
|
144
|
+
model_class.class_eval do
|
145
|
+
|
146
|
+
define_method(field.method_name) do
|
147
|
+
instance_variable_get(field.ivar_name) ||
|
148
|
+
instance_variable_set(field.ivar_name, field.read(self.read_model_data))
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def determine_ar_column_class(data)
|
157
|
+
data.class.columns.first.class if data.class.respond_to?(:columns)
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
class JsonStructField < Struct.new(:read_model_class, :name, :struct_class_name)
|
163
|
+
|
164
|
+
def struct_class
|
165
|
+
@struct_class ||= if self.struct_class_name.kind_of?(String)
|
166
|
+
names = self.struct_class_name.to_s.split('::').reject{ |name| name.empty? }
|
167
|
+
namespace = self.struct_class_name =~ /^::/ ? Object : self.read_model_class
|
168
|
+
names.inject(namespace){ |o, name| o.const_get(name) }
|
169
|
+
else
|
170
|
+
self.struct_class_name
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def reader(read_model_data)
|
175
|
+
json_value = read_model_data[self.name]
|
176
|
+
return json_value if json_value.nil?
|
177
|
+
|
178
|
+
if json_value.kind_of?(String)
|
179
|
+
begin
|
180
|
+
return MR::JsonField.decode(json_value)
|
181
|
+
rescue MR::JsonField::InvalidJSONError => exception
|
182
|
+
message = "can't decode `#{self.name}` JSON: #{exception.message}"
|
183
|
+
raise exception.class, message, exception.backtrace
|
184
|
+
end
|
185
|
+
else
|
186
|
+
return json_value
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
class JsonStructListField < JsonStructField
|
193
|
+
|
194
|
+
def reader(read_model_data)
|
195
|
+
if !(json_datas = super(read_model_data)).nil?
|
196
|
+
json_datas.map{ |data| self.struct_class.new(data) }
|
197
|
+
else
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
end
|
203
|
+
|
204
|
+
class JsonStructObjField < JsonStructField
|
205
|
+
|
206
|
+
def reader(read_model_data)
|
207
|
+
if !(json_data = super(read_model_data)).nil?
|
208
|
+
self.struct_class.new(json_data)
|
209
|
+
else
|
210
|
+
nil
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
InvalidFieldTypeError = Class.new(RuntimeError)
|
217
|
+
|
218
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'mr/read_model/subquery'
|
2
|
+
|
3
|
+
module MR::ReadModel
|
4
|
+
|
5
|
+
module QueryExpression
|
6
|
+
def self.new(type, *args, &block)
|
7
|
+
if !args.empty?
|
8
|
+
StaticQueryExpression.new(type, *args)
|
9
|
+
elsif block
|
10
|
+
DynamicQueryExpression.new(type, &block)
|
11
|
+
else
|
12
|
+
raise InvalidQueryExpressionError, "must be passed args or a block"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class StaticQueryExpression
|
18
|
+
attr_reader :type, :args
|
19
|
+
|
20
|
+
def initialize(type, *args)
|
21
|
+
@type = type
|
22
|
+
@args = args
|
23
|
+
end
|
24
|
+
|
25
|
+
# `apply_to` has to take params that it ignores, so it has the same
|
26
|
+
# interface as `DynamicQueryExpression` (which actually uses the params)
|
27
|
+
def apply_to(relation, params = nil)
|
28
|
+
relation.send(@type, *@args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class DynamicQueryExpression
|
33
|
+
attr_reader :type, :block
|
34
|
+
|
35
|
+
def initialize(type, &block)
|
36
|
+
@type = type
|
37
|
+
@block = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def apply_to(relation, params)
|
41
|
+
relation.send(@type, relation.instance_exec(params, &@block))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class MergeQueryExpression
|
46
|
+
attr_accessor :type, :query_expression
|
47
|
+
|
48
|
+
def initialize(type, *args, &block)
|
49
|
+
@type = type
|
50
|
+
@query_expression = QueryExpression.new(:merge, *args, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_to(relation, params = nil)
|
54
|
+
@query_expression.apply_to(relation, params)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class SubqueryExpression
|
59
|
+
TYPES = {
|
60
|
+
:joins => JoinSubquery
|
61
|
+
}.freeze
|
62
|
+
|
63
|
+
attr_reader :subquery_type, :subquery_args, :subquery_block
|
64
|
+
alias :type :subquery_type
|
65
|
+
|
66
|
+
def initialize(type, *args, &block)
|
67
|
+
raise ArgumentError, "a block must be provided" unless block
|
68
|
+
@subquery_type = type
|
69
|
+
@subquery_args = args
|
70
|
+
@subquery_block = block
|
71
|
+
end
|
72
|
+
|
73
|
+
def subquery
|
74
|
+
@subquery ||= TYPES[self.subquery_type].new(
|
75
|
+
*self.subquery_args,
|
76
|
+
&self.subquery_block
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def apply_to(relation, params = nil)
|
81
|
+
relation.send(self.type, self.subquery.build_sql(params))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class FromExpression
|
86
|
+
attr_reader :record_class
|
87
|
+
|
88
|
+
def initialize(record_class)
|
89
|
+
raise ArgumentError, "must be passed a MR::Record" unless record_class < MR::Record
|
90
|
+
@record_class = record_class
|
91
|
+
end
|
92
|
+
|
93
|
+
def default_find_attr
|
94
|
+
@default_find_attr ||= [
|
95
|
+
self.record_class.table_name,
|
96
|
+
self.record_class.primary_key
|
97
|
+
].join('.')
|
98
|
+
end
|
99
|
+
|
100
|
+
def ar_relation(params = nil)
|
101
|
+
self.record_class.scoped
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class FromSubqueryExpression
|
106
|
+
attr_reader :subquery_block
|
107
|
+
|
108
|
+
def initialize(&block)
|
109
|
+
raise ArgumentError, "a block must be provided" unless block
|
110
|
+
@subquery_block = block
|
111
|
+
end
|
112
|
+
|
113
|
+
# lazy-build `FromSubquery` so its block is not evaluated until the read
|
114
|
+
# model `query` method is called, cache it so it doesn't have to be rebuilt
|
115
|
+
# for every `query` call
|
116
|
+
def from_subquery
|
117
|
+
@from_subquery ||= FromSubquery.new(&self.subquery_block)
|
118
|
+
end
|
119
|
+
|
120
|
+
def record_class
|
121
|
+
self.from_subquery.record_class
|
122
|
+
end
|
123
|
+
|
124
|
+
def default_find_attr
|
125
|
+
raise NoFindAttrError, "a `find_attr` has to be specified " \
|
126
|
+
"when using a from subquery"
|
127
|
+
end
|
128
|
+
|
129
|
+
def ar_relation(params = nil)
|
130
|
+
self.record_class.scoped.from(self.from_subquery.build_sql(params))
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class NullFromExpression
|
135
|
+
def record_class; raise NoFromExpressionError; end
|
136
|
+
def default_find_attr; raise NoFromExpressionError; end
|
137
|
+
def ar_relation(params = nil); raise NoFromExpressionError; end
|
138
|
+
end
|
139
|
+
|
140
|
+
class SetExpression
|
141
|
+
OPERATOR_SQL = {
|
142
|
+
:union => 'UNION'.freeze,
|
143
|
+
:union_all => 'UNION ALL'.freeze,
|
144
|
+
:intersect => 'INTERSECT'.freeze,
|
145
|
+
:intersect_all => 'INTERSECT ALL'.freeze,
|
146
|
+
:except => 'EXCEPT'.freeze,
|
147
|
+
:except_all => 'EXCEPT ALL'.freeze
|
148
|
+
}.freeze
|
149
|
+
|
150
|
+
attr_reader :operator_sql, :read_model_block
|
151
|
+
|
152
|
+
def initialize(operator, &block)
|
153
|
+
raise ArgumentError, "a block must be provided" unless block
|
154
|
+
@operator_sql = OPERATOR_SQL[operator]
|
155
|
+
@read_model_block = block
|
156
|
+
end
|
157
|
+
|
158
|
+
def read_model_class
|
159
|
+
@read_model_class ||= begin
|
160
|
+
c = Class.new do
|
161
|
+
# TODO - fix circular require
|
162
|
+
require 'mr/read_model'
|
163
|
+
require 'mr/read_model/set_querying'
|
164
|
+
include MR::ReadModel
|
165
|
+
include MR::ReadModel::SetQuerying
|
166
|
+
end
|
167
|
+
c.class_eval(&self.read_model_block)
|
168
|
+
c
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def combine_sql(sql, params = nil)
|
173
|
+
"#{sql} #{self.operator_sql} #{self.read_model_class.build_sql(params)}"
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
InvalidQueryExpressionError = Class.new(RuntimeError)
|
178
|
+
|
179
|
+
NoFindAttrError = Class.new(RuntimeError)
|
180
|
+
|
181
|
+
class NoFromExpressionError < RuntimeError
|
182
|
+
def initialize
|
183
|
+
super "a from expression hasn't been set - " \
|
184
|
+
"set one using `from` or `from_subquery`"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
end
|