smql 0.0.4.2 → 0.0.4.3
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.
- data/VERSION +1 -1
- data/lib/smql_to_ar/condition_types.rb +138 -45
- data/lib/smql_to_ar/query_builder.rb +126 -39
- data/lib/smql_to_ar.rb +69 -17
- metadata +2 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.4.3
|
@@ -25,28 +25,44 @@ class SmqlToAR
|
|
25
25
|
# Nimmt eine Klasse ein Objekt an, so soll diese Klasse instanziert werden.
|
26
26
|
# Alles weitere siehe Condition.
|
27
27
|
module ConditionTypes
|
28
|
+
extend SmqlToAR::Assertion
|
29
|
+
|
28
30
|
class <<self
|
29
31
|
# Ex: 'givenname|surname|nick' => [:givenname, :surname, :nick]
|
30
32
|
def split_keys k
|
31
33
|
k.split( '|').collect &:to_sym
|
32
34
|
end
|
33
35
|
|
36
|
+
def conditions &e
|
37
|
+
unless block_given?
|
38
|
+
r = Enumerator.new( self, :conditions)
|
39
|
+
s = self
|
40
|
+
r.define_singleton_method :[] do |k|
|
41
|
+
s.conditions.select {|c| c::Operator === k }
|
42
|
+
end
|
43
|
+
return r
|
44
|
+
end
|
45
|
+
constants.each do |c|
|
46
|
+
next if :Condition == c
|
47
|
+
c = const_get c
|
48
|
+
next if Condition === c
|
49
|
+
yield c
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
34
53
|
# Eine Regel parsen.
|
35
54
|
# Ex: Person, "givenname=", "Peter"
|
36
55
|
def try_parse_it model, colop, val
|
37
56
|
r = nil
|
38
57
|
#p :try_parse => { :model => model, :colop => colop, :value => val }
|
39
|
-
|
40
|
-
|
41
|
-
c = const_get c
|
42
|
-
next if Condition === c
|
43
|
-
raise UnexpectedColOpError.new( model, colop, val) unless colop =~ /^(?:\d*:)?(.*?)(\W*)$/
|
58
|
+
conditions.each do |c|
|
59
|
+
raise_unless colop =~ /^(?:\d*:)?(.*?)(\W*)$/, UnexpectedColOpError.new( model, colop, val)
|
44
60
|
col, op = $1, $2
|
45
61
|
col = split_keys( col).collect {|c| Column.new model, c }
|
46
62
|
r = c.try_parse model, col, op, val
|
47
63
|
break if r
|
48
64
|
end
|
49
|
-
|
65
|
+
raise_unless r, UnexpectedError.new( model, colop, val)
|
50
66
|
r
|
51
67
|
end
|
52
68
|
|
@@ -72,16 +88,24 @@ class SmqlToAR
|
|
72
88
|
end
|
73
89
|
|
74
90
|
class Condition
|
91
|
+
include SmqlToAR::Assertion
|
92
|
+
extend SmqlToAR::Assertion
|
75
93
|
attr_reader :value, :cols
|
76
94
|
Operator = nil
|
77
95
|
Expected = []
|
78
96
|
Where = nil
|
79
97
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
98
|
+
class <<self
|
99
|
+
# Versuche das Objekt zu erkennen. Operator und Expected muessen passen.
|
100
|
+
# Passt das Object, die Klasse instanzieren.
|
101
|
+
def try_parse model, cols, op, val
|
102
|
+
#p :self => name, :try_parse => op, :cols => cols, :with => self::Operator, :value => val, :expected => self::Expected, :model => model.name
|
103
|
+
new model, cols, val if self::Operator === op and self::Expected.any?( &it === val)
|
104
|
+
end
|
105
|
+
|
106
|
+
def inspect
|
107
|
+
"#{self.name}(:operator=>#{self::Operator.inspect}, :expected=>#{self::Expected.inspect}, :where=>#{self::Where.inspect})"
|
108
|
+
end
|
85
109
|
end
|
86
110
|
|
87
111
|
def initialize model, cols, val
|
@@ -93,6 +117,10 @@ class SmqlToAR
|
|
93
117
|
verify
|
94
118
|
end
|
95
119
|
|
120
|
+
def inspect
|
121
|
+
"#<#{self.class.name}:0x#{(self.object_id<<1).to_s 16} model: #{self.class.name}, cols: #{@cols.inspect}, value: #{@value.inspect}>"
|
122
|
+
end
|
123
|
+
|
96
124
|
def verify
|
97
125
|
@cols.each do |col|
|
98
126
|
verify_column col
|
@@ -103,19 +131,19 @@ class SmqlToAR
|
|
103
131
|
# Gibt es eine Spalte diesen Namens?
|
104
132
|
# Oder: Gibt es eine Relation diesen Namens? (Hier nicht der Fall)
|
105
133
|
def verify_column col
|
106
|
-
|
134
|
+
raise_unless col.exist_in?, NonExistingColumnError.new( %w[Column], col)
|
107
135
|
end
|
108
136
|
|
109
137
|
# Modelle koennen Spalten/Relationen verbieten mit Model#smql_protected.
|
110
138
|
# Dieses muss ein Object mit #include?( name_als_string) zurueckliefern,
|
111
139
|
# welches true fuer verboten und false fuer, erlaubt steht.
|
112
140
|
def verify_allowed col
|
113
|
-
|
141
|
+
raise_if col.protected?, ProtectedColumnError.new( col)
|
114
142
|
end
|
115
143
|
|
116
144
|
# Erstelle alle noetigen Klauseln. builder nimmt diese entgegen,
|
117
|
-
# wobei builder.
|
118
|
-
# mehrere Schluessel bedeuten, dass die Values _alle_ zutreffen muessen, wobei die Schluessel
|
145
|
+
# wobei builder.joins, builder.select, builder.where und builder.wobs von interesse sind.
|
146
|
+
# mehrere Schluessel bedeuten, dass die Values _alle_ zutreffen muessen, wobei die Schluessel geODERt werden.
|
119
147
|
# Ex:
|
120
148
|
# 1) {"givenname=", "Peter"} #=> givenname = 'Peter'
|
121
149
|
# 2) {"givenname=", ["Peter", "Hans"]} #=> ( givenname = 'Peter' OR givenname = 'Hans' )
|
@@ -128,15 +156,16 @@ class SmqlToAR
|
|
128
156
|
@cols.each do |col|
|
129
157
|
col.joins builder, table
|
130
158
|
col = builder.column table+col.path, col.col
|
131
|
-
builder.where
|
159
|
+
builder.where values.keys.collect {|vid| self.class::Where % [ col, vid.to_s ] }
|
132
160
|
end
|
133
161
|
else
|
162
|
+
b2 = SmqlToAR::And.new builder
|
134
163
|
values.keys.each do |vid|
|
135
|
-
|
164
|
+
b2.where SmqlToAR::Or[ *@cols.collect {|col|
|
136
165
|
col.joins builder, table
|
137
166
|
col = builder.column table+col.path, col.col
|
138
167
|
self.class::Where % [ col, vid.to_s ]
|
139
|
-
}
|
168
|
+
}]
|
140
169
|
end
|
141
170
|
end
|
142
171
|
self
|
@@ -185,50 +214,94 @@ class SmqlToAR
|
|
185
214
|
|
186
215
|
In = simple_condition NotIn, '|=', '%s IN (%s)', [Array]
|
187
216
|
In2 = simple_condition In, '', nil, [Array]
|
188
|
-
NotEqual = simple_condition Condition,
|
217
|
+
NotEqual = simple_condition Condition, '!=', "%s <> %s", [Array, String, Numeric]
|
218
|
+
NotEqual2 = simple_condition Condition, '<>', "%s <> %s", [Array, String, Numeric]
|
189
219
|
GreaterThanOrEqual = simple_condition Condition, '>=', "%s >= %s", [Array, Numeric]
|
190
220
|
LesserThanOrEqual = simple_condition Condition, '<=', "%s <= %s", [Array, Numeric]
|
221
|
+
|
222
|
+
# Examples:
|
223
|
+
# { 'articles=>' => { id: 1 } }
|
224
|
+
# { 'articles=>' => [ { id: 1 }, { id: 2 } ] }
|
191
225
|
class EqualJoin <Condition
|
192
|
-
Operator = '
|
193
|
-
Expected = [Hash]
|
226
|
+
Operator = '=>'
|
227
|
+
Expected = [Hash, lambda {|x| x.kind_of?( Array) and x.all?( &it.kind_of?( Hash))}]
|
194
228
|
|
195
229
|
def initialize *pars
|
196
230
|
super( *pars)
|
231
|
+
@value = Array.wrap @value
|
197
232
|
cols = {}
|
198
233
|
@cols.each do |col|
|
199
|
-
col_model =
|
200
|
-
|
201
|
-
cols[col] = [col_model] + ConditionTypes.try_parse( col_model, @value)
|
234
|
+
col_model = col.relation
|
235
|
+
cols[col] = [col_model] + @value.collect {|val| ConditionTypes.try_parse( col_model, val) }
|
202
236
|
end
|
203
237
|
@cols = cols
|
204
238
|
end
|
205
239
|
|
206
240
|
def verify_column col
|
207
|
-
|
208
|
-
#p refl: refl, model: @model.name, col: col, :reflections => @model.reflections.keys
|
209
|
-
raise NonExistingRelationError.new( %w[Relation], col) unless refl
|
241
|
+
raise_unless col.relation, NonExistingRelationError.new( %w[Relation], col)
|
210
242
|
end
|
211
243
|
|
212
244
|
def build builder, table
|
213
245
|
@cols.each do |col, sub|
|
246
|
+
model, *sub = sub
|
214
247
|
t = table + col.path + [col.col]
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
248
|
+
col.joins.each {|j, m| builder.joins table+j, m }
|
249
|
+
builder.joins t, model
|
250
|
+
b2 = 1 == sub.length ? builder : Or.new( builder)
|
251
|
+
sub.each {|i| i.collect( &it.build( And.new( b2), t)); p 'or' => b2 }
|
252
|
+
end
|
253
|
+
self
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Takes to Queries.
|
258
|
+
# First Query will be a Subquery, second a regular query.
|
259
|
+
# Example:
|
260
|
+
# Person.smql 'sub.articles:' => [{'limit:' => 1, 'order:': 'updated_at desc'}, {'content~' => 'some text'}]
|
261
|
+
# Person must have as last Article (compared by updated_at) owned by Person a Artive which has 'some text' in content.
|
262
|
+
# The last Article needn't to have 'some text' has content, the subquery takes it anyway.
|
263
|
+
# But the second query compares to it and never to any other Article, because these are filtered by first query.
|
264
|
+
# The difference to
|
265
|
+
# Person.smql :articles => {'content~' => 'some text', 'limit:' => 1, 'order:': 'updated_at desc'}
|
266
|
+
# is, second is not allowed (limit and order must be in root) and this means something like
|
267
|
+
# "Person must have the Article owned by Person which has 'some text' in content.
|
268
|
+
# limit and order has no function in this query and this article needn't to be the last."
|
269
|
+
=begin
|
270
|
+
class SubEqualJoin < EqualJoin
|
271
|
+
Operator = '()'
|
272
|
+
Expected = [lambda {|x| x.kind_of?( Array) and (1..2).include?( x.length) and x.all?( &it.kind_of?( Hash))}]
|
273
|
+
|
274
|
+
def initialize model, cols, val
|
275
|
+
super model, cols, val[1]
|
276
|
+
# sub: model, subquery, sub(condition)
|
277
|
+
@cols.each {|col, sub| sub[ 1..-1] = SmqlToAR.new( col.relation, val[0]).parse, *sub[-1] }
|
278
|
+
end
|
279
|
+
|
280
|
+
def verify_column col
|
281
|
+
raise_unless col.child?, ConColumnError.new( [:Column], col)
|
282
|
+
end
|
283
|
+
|
284
|
+
def build builder, table
|
285
|
+
@cols.each do |col, sub|
|
286
|
+
t = table+col.to_a
|
287
|
+
builder.sub_joins t, col, *sub[0..1]
|
288
|
+
#ap sub: sub[2..-1]
|
289
|
+
sub[2..-1].each &it.build( builder, t)
|
220
290
|
end
|
221
291
|
self
|
222
292
|
end
|
223
293
|
end
|
294
|
+
=end
|
295
|
+
|
224
296
|
Equal = simple_condition Condition, '=', "%s = %s", [Array, String, Numeric]
|
225
297
|
Equal2 = simple_condition Equal, '', "%s = %s", [String, Numeric]
|
226
298
|
GreaterThan = simple_condition Condition, '>', "%s > %s", [Array, Numeric]
|
227
299
|
LesserThan = simple_condition Condition, '<', "%s < %s", [Array, Numeric]
|
228
300
|
NotIlike = simple_condition Condition, '!~', "%s NOT ILIKE %s", [Array, String]
|
229
301
|
Ilike = simple_condition Condition, '~', "%s ILIKE %s", [Array, String]
|
302
|
+
Exists = simple_condition Condition, '', '%s IS NOT NULL', [TrueClass]
|
303
|
+
NotExists = simple_condition Condition, '', '%s IS NULL', [FalseClass]
|
230
304
|
|
231
|
-
####### No Operator #######
|
232
305
|
Join = simple_condition EqualJoin, '', nil, [Hash]
|
233
306
|
InRange2 = simple_condition InRange, '', nil, [Range]
|
234
307
|
class Select < Condition
|
@@ -236,7 +309,7 @@ class SmqlToAR
|
|
236
309
|
Expected = [nil]
|
237
310
|
|
238
311
|
def verify_column col
|
239
|
-
|
312
|
+
raise_unless col.exist_in? || SmqlToAR.model_of( col.last_model, col.col), NonExistingSelectableError.new( col)
|
240
313
|
end
|
241
314
|
|
242
315
|
def build builder, table
|
@@ -258,13 +331,19 @@ class SmqlToAR
|
|
258
331
|
Expected = [String, Array, Hash, Numeric, nil]
|
259
332
|
|
260
333
|
class Function
|
334
|
+
include SmqlToAR::Assertion
|
261
335
|
Name = nil
|
262
336
|
Expected = []
|
263
337
|
attr_reader :model, :func, :args
|
264
338
|
|
265
|
-
|
266
|
-
|
267
|
-
|
339
|
+
class <<self
|
340
|
+
def try_parse model, func, args
|
341
|
+
self.new model, func, args if self::Name === func and self::Expected.any?( &it === args)
|
342
|
+
end
|
343
|
+
|
344
|
+
def inspect
|
345
|
+
"#{self.name}(:name=>#{self::Name}, :expected=>#{self::Expected})"
|
346
|
+
end
|
268
347
|
end
|
269
348
|
|
270
349
|
def initialize model, func, args
|
@@ -277,22 +356,19 @@ class SmqlToAR
|
|
277
356
|
Expected = [String, Array, Hash, nil]
|
278
357
|
|
279
358
|
def initialize model, func, args
|
280
|
-
SmqlToAR.logger.info( {args: args}.inspect)
|
281
359
|
args = case args
|
282
360
|
when String then [args]
|
283
361
|
when Array, Hash then args.to_a
|
284
362
|
when nil then nil
|
285
363
|
else raise 'Oops'
|
286
364
|
end
|
287
|
-
SmqlToAR.logger.info( {args: args}.inspect)
|
288
365
|
args.andand.collect! do |o|
|
289
366
|
o = Array.wrap o
|
290
367
|
col = Column.new model, o.first
|
291
368
|
o = 'desc' == o.last.to_s.downcase ? :DESC : :ASC
|
292
|
-
|
369
|
+
raise_unless col.exist_in?, NonExistingColumnError.new( [:Column], col)
|
293
370
|
[col, o]
|
294
371
|
end
|
295
|
-
SmqlToAR.logger.info( {args: args}.inspect)
|
296
372
|
super model, func, args
|
297
373
|
end
|
298
374
|
|
@@ -302,22 +378,39 @@ class SmqlToAR
|
|
302
378
|
col, o = o
|
303
379
|
col.joins builder, table
|
304
380
|
t = table + col.path
|
305
|
-
#
|
381
|
+
#raise_unless 1 == t.length, RootOnlyFunctionError.new( t)
|
306
382
|
builder.order t, col.col, o
|
307
383
|
end
|
308
384
|
end
|
309
385
|
end
|
310
386
|
|
387
|
+
class Limit < Function
|
388
|
+
Name = :limit
|
389
|
+
Expected = [Fixnum]
|
390
|
+
|
391
|
+
def build builder, table
|
392
|
+
raise_unless 1 == table.length, RootOnlyFunctionError.new( table)
|
393
|
+
builder.limit = Array.wrap(@args).first.to_i
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
class Offset < Function
|
398
|
+
Name = :offset
|
399
|
+
Expected = [Fixnum]
|
400
|
+
|
401
|
+
def build builder, table
|
402
|
+
raise_unless 1 == table.length, RootOnlyFunctionError.new( table)
|
403
|
+
builder.offset = Array.wrap(@args).first.to_i
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
311
407
|
def self.new model, col, val
|
312
|
-
SmqlToAR.logger.info( { function: col.first.to_sym }.inspect)
|
313
408
|
r = nil
|
314
409
|
constants.each do |c|
|
315
410
|
next if [:Function, :Where, :Expected, :Operator].include? c
|
316
411
|
c = const_get c
|
317
412
|
next if Function === c or not c.respond_to?( :try_parse)
|
318
|
-
SmqlToAR.logger.info( {f: c}.inspect)
|
319
413
|
r = c.try_parse model, col.first.to_sym, val
|
320
|
-
SmqlToAR.logger.info( {r: r}.inspect)
|
321
414
|
break if r
|
322
415
|
end
|
323
416
|
r
|
@@ -22,36 +22,41 @@ class SmqlToAR
|
|
22
22
|
class Vid
|
23
23
|
attr_reader :vid
|
24
24
|
def initialize( vid) @vid = vid end
|
25
|
-
def to_s() ":
|
26
|
-
def to_sym() "
|
25
|
+
def to_s() ":smql_c#{@vid}" end
|
26
|
+
def to_sym() "smql_c#{@vid}".to_sym end
|
27
27
|
alias sym to_sym
|
28
28
|
def to_i() @vid end
|
29
29
|
end
|
30
30
|
|
31
|
-
attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins
|
32
|
-
attr_accessor :logger
|
31
|
+
attr_reader :table_alias, :model, :table_model, :base_table, :_where, :_select, :_wobs, :_joins, :prefix, :_vid
|
32
|
+
attr_accessor :logger, :limit, :offset
|
33
33
|
|
34
|
-
def initialize model
|
34
|
+
def initialize model, prefix = nil
|
35
|
+
@prefix = "smql"
|
35
36
|
@logger = SmqlToAR.logger
|
36
37
|
@table_alias = Hash.new do |h, k|
|
37
|
-
|
38
|
-
h[k] = "
|
38
|
+
j = Array.wrap( k).compact
|
39
|
+
h[k] = h.key?(j) ? h[j] : "#{@prefix},#{j.join(',')}"
|
39
40
|
end
|
40
|
-
@_vid, @_where, @_wobs, @model, @quoter = 0, [], {}, model, model.connection
|
41
|
+
@_vid, @_where, @_wobs, @model, @quoter = 0, SmqlToAR::And[], {}, model, model.connection
|
41
42
|
@base_table = [model.table_name.to_sym]
|
42
43
|
@table_alias[ @base_table] = @base_table.first
|
43
44
|
t = quote_table_name @table_alias[ @base_table]
|
44
|
-
@_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [], [], []
|
45
|
+
@_select, @_joins, @_joined, @_includes, @_order = ["DISTINCT #{t}.*"], "", [@base_table], [], []
|
45
46
|
@table_model = {@base_table => @model}
|
46
47
|
end
|
47
48
|
|
48
49
|
def vid() Vid.new( @_vid+=1) end
|
49
50
|
|
51
|
+
def inspect
|
52
|
+
"#<#{self.class.name}:#{"0x%x"% (self.object_id<<1)}|#{@prefix}:#{@base_table}:#{@model} vid=#{@_vid} where=#{@_where} wobs=#{@_wobs} select=#{@_select} aliases=#{@_table_alias}>"
|
53
|
+
end
|
54
|
+
|
50
55
|
# Jede via where uebergebene Condition wird geodert und alle zusammen werden geundet.
|
51
56
|
# "Konjunktive Normalform". Allerdings duerfen Conditions auch Komplexe Abfragen enthalten.
|
52
|
-
# Ex: builder.where( 'a = a', 'b = c').where( 'c = d', 'e = e').where( 'x = y').where( '( m = n AND o = p )', 'f = g')
|
57
|
+
# Ex: builder.where( ['a = a', 'b = c']).where( ['c = d', 'e = e']).where( 'x = y').where( ['( m = n AND o = p )', 'f = g'])
|
53
58
|
# #=> WHERE ( a = a OR b = c ) AND ( c = d OR e = e ) AND x = y ( ( m = n AND o = p ) OR f = g )
|
54
|
-
def where
|
59
|
+
def where cond
|
55
60
|
@_where.push cond
|
56
61
|
self
|
57
62
|
end
|
@@ -74,32 +79,54 @@ class SmqlToAR
|
|
74
79
|
end
|
75
80
|
|
76
81
|
def build_join orig, pretable, table, prekey, key
|
77
|
-
" JOIN #{
|
82
|
+
" LEFT JOIN #{orig} AS #{quote_table_name table} ON #{column pretable, prekey} = #{column table, key} "
|
78
83
|
end
|
79
84
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
85
|
+
def sub_joins table, col, model, query
|
86
|
+
prefix, base_table = "#{@prefix}_sub", col.relation.table_name
|
87
|
+
join_ table, model, "(#{query.build( prefix).ar.to_sql})"
|
88
|
+
end
|
89
|
+
|
90
|
+
def join_ table, model, query, pretable = nil
|
91
|
+
pretable ||= table[0...-1]
|
83
92
|
@table_model[ table] = model
|
84
93
|
premodel = @table_model[ pretable]
|
85
94
|
t = @table_alias[ table]
|
86
95
|
pt = quote_table_name @table_alias[ table[ 0...-1]]
|
87
96
|
refl = premodel.reflections[table.last]
|
88
|
-
case refl
|
89
|
-
when
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
97
|
+
case refl
|
98
|
+
when ActiveRecord::Reflection::ThroughReflection
|
99
|
+
through = refl.through_reflection
|
100
|
+
throughtable = table[0...-1]+[through.name.to_sym]
|
101
|
+
srctable = throughtable+[refl.source_reflection.name]
|
102
|
+
@table_model[ srctable] = model
|
103
|
+
@table_alias[ table] = @table_alias[ srctable]
|
104
|
+
join_ throughtable, through.klass, quote_table_name( through.table_name)
|
105
|
+
join_ srctable, refl.klass, query, throughtable
|
106
|
+
when ActiveRecord::Reflection::AssociationReflection
|
107
|
+
case refl.macro
|
108
|
+
when :has_many, :has_one
|
109
|
+
@_joins += build_join query, pretable, t, premodel.primary_key, refl.primary_key_name
|
110
|
+
when :belongs_to
|
111
|
+
@_joins += build_join query, pretable, t, refl.primary_key_name, premodel.primary_key
|
112
|
+
when :has_and_belongs_to_many
|
113
|
+
jointable = [','] + table
|
114
|
+
@_joins += build_join refl.options[:join_table], pretable, @table_alias[jointable], premodel.primary_key, refl.primary_key_name
|
115
|
+
@_joins += build_join query, jointable, t, refl.association_foreign_key, refl.association_primary_key
|
116
|
+
else raise BuilderError, "Unkown reflection macro: #{refl.macro.inspect}"
|
117
|
+
end
|
118
|
+
else raise BuilderError, "Unkown reflection type: #{refl.class.name}"
|
98
119
|
end
|
99
|
-
@_joined.push table
|
100
120
|
self
|
101
121
|
end
|
102
122
|
|
123
|
+
def joins table, model
|
124
|
+
table = table.flatten.compact
|
125
|
+
return self if @_joined.include? table # Already joined
|
126
|
+
join_ table, model, quote_table_name( model.table_name)
|
127
|
+
@_joined.push table
|
128
|
+
end
|
129
|
+
|
103
130
|
def includes table
|
104
131
|
@_includes.push table
|
105
132
|
self
|
@@ -111,36 +138,28 @@ class SmqlToAR
|
|
111
138
|
end
|
112
139
|
|
113
140
|
def order table, col, o
|
114
|
-
|
141
|
+
ct = column table, col
|
115
142
|
@_select.push ct
|
116
143
|
@_order.push "#{ct} #{:DESC == o ? :DESC : :ASC}"
|
117
144
|
self
|
118
145
|
end
|
119
146
|
|
120
|
-
class Dummy
|
121
|
-
def method_missing m, *a, &e
|
122
|
-
#p :dummy => m, :pars => a, :block => e
|
123
|
-
self
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
147
|
def build_ar
|
128
|
-
where_str = @_where.
|
129
|
-
w = Array.wrap w
|
130
|
-
1 == w.length ? w.first : "( #{w.join( ' OR ')} )"
|
131
|
-
end.join ' AND '
|
148
|
+
where_str = @_where.type_correction!.optimize!.build_where
|
132
149
|
incls = {}
|
133
150
|
@_includes.each do |inc|
|
134
151
|
b = incls
|
135
152
|
inc[1..-1].collect {|rel| b = b[rel] ||= {} }
|
136
153
|
end
|
137
|
-
@logger.debug incls: incls, joins: @_joins
|
138
154
|
@model = @model.
|
139
155
|
select( @_select.join( ', ')).
|
140
156
|
joins( @_joins).
|
141
157
|
where( where_str, @_wobs).
|
142
158
|
order( @_order.join( ', ')).
|
143
159
|
includes( incls)
|
160
|
+
@model = @model.limit @limit if @limit
|
161
|
+
@model = @model.offset @offset if @offset
|
162
|
+
@model
|
144
163
|
end
|
145
164
|
|
146
165
|
def fix_calculate
|
@@ -159,4 +178,72 @@ class SmqlToAR
|
|
159
178
|
@model
|
160
179
|
end
|
161
180
|
end
|
181
|
+
|
182
|
+
class SubBuilder < Array
|
183
|
+
attr_reader :parent, :_where
|
184
|
+
delegate :wobs, :joins, :includes, :sub_joins, :vid, :quote_column_name, :quoter, :quote_table_name, :column, :to => :parent
|
185
|
+
|
186
|
+
def initialize parent, tmp = false
|
187
|
+
@parent = parent
|
188
|
+
@parent.where self unless @parend.nil? && tmp
|
189
|
+
end
|
190
|
+
|
191
|
+
def new parent, tmp = false
|
192
|
+
super parent, tmp
|
193
|
+
#return parent if self.class == parent.class
|
194
|
+
#super parent
|
195
|
+
end
|
196
|
+
|
197
|
+
alias where push
|
198
|
+
|
199
|
+
def type_correction!
|
200
|
+
collect! do |sub|
|
201
|
+
if sub.kind_of? Array
|
202
|
+
sub = default[ *sub] unless sub.respond_to?( :type_correction!)
|
203
|
+
sub.type_correction!
|
204
|
+
end
|
205
|
+
sub
|
206
|
+
end
|
207
|
+
self
|
208
|
+
end
|
209
|
+
|
210
|
+
def optimize!
|
211
|
+
ext = []
|
212
|
+
collect! do |sub|
|
213
|
+
sub = sub.optimize! if sub.kind_of? Array
|
214
|
+
if self.class == sub.class
|
215
|
+
ext.push *sub
|
216
|
+
nil
|
217
|
+
elsif sub.blank?
|
218
|
+
nil
|
219
|
+
else
|
220
|
+
sub
|
221
|
+
end
|
222
|
+
end.compact!
|
223
|
+
push *ext
|
224
|
+
self
|
225
|
+
end
|
226
|
+
|
227
|
+
def inspect
|
228
|
+
"#{self.class.name.sub( /.*::/, '')}[ #{collect(&:inspect).join ', '}]"
|
229
|
+
end
|
230
|
+
def default() SmqlToAR::And end
|
231
|
+
def default_new( parent) default.new self, parent, false end
|
232
|
+
def collect_build_where
|
233
|
+
collect {|x| x.respond_to?( :build_where) ? x.build_where : x.to_s }
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class And < SubBuilder
|
238
|
+
def default; SmqlToAR::Or; end
|
239
|
+
def build_where
|
240
|
+
collect_build_where.join ' AND '
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class Or < SubBuilder
|
245
|
+
def build_where
|
246
|
+
collect_build_where.join ' OR '
|
247
|
+
end
|
248
|
+
end
|
162
249
|
end
|
data/lib/smql_to_ar.rb
CHANGED
@@ -15,6 +15,18 @@
|
|
15
15
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
16
16
|
|
17
17
|
class SmqlToAR
|
18
|
+
module Assertion
|
19
|
+
def raise_unless cond, exception = nil, *args
|
20
|
+
cond, exception, *args = yield. cond, exception, *args if block_given?
|
21
|
+
raise exception || Exception, *args unless cond
|
22
|
+
end
|
23
|
+
|
24
|
+
def raise_if cond, exception = nil, *args
|
25
|
+
cond, exception, *args = yield. cond, exception, *args if block_given?
|
26
|
+
raise exception || Exception, *args if cond
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
18
30
|
include ActiveSupport::Benchmarkable
|
19
31
|
############################################################################r
|
20
32
|
# Exceptions
|
@@ -77,19 +89,27 @@ class SmqlToAR
|
|
77
89
|
end
|
78
90
|
end
|
79
91
|
|
80
|
-
class
|
92
|
+
class RootOnlyFunctionError < SMQLError
|
81
93
|
def initialize path
|
82
94
|
super :path => path
|
83
95
|
end
|
84
96
|
end
|
85
97
|
|
98
|
+
class ConColumnError < SMQLError
|
99
|
+
def initialize expected, got
|
100
|
+
super :expected => expected, :got => got
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
86
104
|
class BuilderError < Exception; end
|
87
105
|
|
88
106
|
#############################################################################
|
89
107
|
|
90
108
|
# Model der Relation `rel` von `model`
|
91
109
|
def self.model_of model, rel
|
92
|
-
|
110
|
+
rel = rel.to_sym
|
111
|
+
r = model.reflections[ rel].andand.klass
|
112
|
+
r.nil? && :self == rel ? model : r
|
93
113
|
end
|
94
114
|
|
95
115
|
# Eine Spalte in einer Tabelle, relativ zu `Column#model`.
|
@@ -103,7 +123,7 @@ class SmqlToAR
|
|
103
123
|
def initialize model, *col
|
104
124
|
@model = model
|
105
125
|
@last_model = nil
|
106
|
-
*@path, @col = Array.wrap( col).collect
|
126
|
+
*@path, @col = *Array.wrap( col).collect( &it.to_s.split( /[.\/]/)).flatten.collect( &:to_sym).reject( &it==:self)
|
107
127
|
end
|
108
128
|
|
109
129
|
def last_model
|
@@ -113,9 +133,12 @@ class SmqlToAR
|
|
113
133
|
def each
|
114
134
|
model = @model
|
115
135
|
@path.each do |rel|
|
116
|
-
|
117
|
-
|
118
|
-
|
136
|
+
rel = rel.to_sym
|
137
|
+
unless :self == rel
|
138
|
+
model = SmqlToAR.model_of model, rel
|
139
|
+
return false unless model
|
140
|
+
yield rel, model
|
141
|
+
end
|
119
142
|
end
|
120
143
|
model
|
121
144
|
end
|
@@ -140,19 +163,23 @@ class SmqlToAR
|
|
140
163
|
def joins builder = nil, table = nil, &exe
|
141
164
|
pp = []
|
142
165
|
table = Array.wrap table
|
143
|
-
exe ||= builder ? lambda {|j, m| builder.
|
166
|
+
exe ||= builder ? lambda {|j, m| builder.joins table+j, m} : Array.method( :[])
|
144
167
|
collect do |rel, model|
|
145
168
|
pp.push rel
|
146
169
|
exe.call pp, model
|
147
170
|
end
|
148
171
|
end
|
149
|
-
def
|
172
|
+
def self?() !@col end
|
173
|
+
def length() @path.length+(self.self? ? 0 : 1) end
|
174
|
+
def size() @path.size+(self.self? ? 0 : 1) end
|
175
|
+
def to_a() @path+(self.self? ? [] : [@col]) end
|
150
176
|
def to_s() to_a.join '.' end
|
151
177
|
def to_sym() to_s.to_sym end
|
152
178
|
def to_json() to_s end
|
153
179
|
def inspect() "#<Column: #{model} #{to_s}>" end
|
154
|
-
def relation() SmqlToAR.model_of last_model, @col end
|
180
|
+
def relation() self.self? ? model : SmqlToAR.model_of( last_model, @col) end
|
155
181
|
def allowed?() ! self.protected? end
|
182
|
+
def child?() @path.empty? and !!relation end
|
156
183
|
end
|
157
184
|
|
158
185
|
attr_reader :model, :query, :conditions, :builder, :order
|
@@ -179,39 +206,64 @@ class SmqlToAR
|
|
179
206
|
#p model: @model, query: @query
|
180
207
|
end
|
181
208
|
|
209
|
+
def self.models models
|
210
|
+
models = Array.wrap models
|
211
|
+
r = Hash.new {|h,k| h[k] = {} }
|
212
|
+
while model = models.tap( &:uniq!).pop
|
213
|
+
refls = model.respond_to?( :reflections) && model.reflections
|
214
|
+
refls && refls.each do |name, refl|
|
215
|
+
r[model.name][name] = case refl
|
216
|
+
when ActiveRecord::Reflection::ThroughReflection then {:macro => refl.macro, :model => refl.klass.name, :through => refl.through_reflection.name}
|
217
|
+
when ActiveRecord::Reflection::AssociationReflection then {:macro => refl.macro, :model => refl.klass.name}
|
218
|
+
else raise "Ups: #{refl.class}"
|
219
|
+
end
|
220
|
+
models.push refl.klass unless r.keys.include? refl.klass.name
|
221
|
+
end
|
222
|
+
end
|
223
|
+
r
|
224
|
+
end
|
225
|
+
|
182
226
|
def parse
|
183
|
-
benchmark 'SMQL parse' do
|
227
|
+
#benchmark 'SMQL parse' do
|
184
228
|
@conditions = ConditionTypes.try_parse @model, @query
|
185
|
-
end
|
229
|
+
#end
|
186
230
|
#p conditions: @conditions
|
187
231
|
self
|
188
232
|
end
|
189
233
|
|
190
234
|
def build
|
191
|
-
benchmark 'SMQL build query' do
|
235
|
+
#benchmark 'SMQL build query' do
|
192
236
|
@builder = QueryBuilder.new @model
|
193
237
|
table = @builder.base_table
|
194
238
|
@conditions.each {|condition| condition.build builder, table }
|
195
|
-
end
|
239
|
+
#end
|
196
240
|
#p builder: @builder
|
197
241
|
self
|
198
242
|
end
|
199
243
|
|
200
244
|
def ar
|
201
|
-
@ar ||= benchmark 'SMQL ar' do
|
245
|
+
@ar ||= #benchmark 'SMQL ar' do
|
202
246
|
@builder.to_ar
|
203
|
-
end
|
247
|
+
#end
|
204
248
|
end
|
205
249
|
|
206
250
|
def to_ar
|
207
|
-
benchmark 'SMQL' do
|
251
|
+
#benchmark 'SMQL' do
|
208
252
|
parse
|
209
253
|
build
|
210
254
|
ar
|
211
|
-
end
|
255
|
+
#end
|
212
256
|
end
|
213
257
|
|
214
258
|
def self.to_ar *params
|
215
259
|
new( *params).to_ar
|
216
260
|
end
|
261
|
+
|
262
|
+
def self.reload_library
|
263
|
+
lib_dir = File.dirname __FILE__
|
264
|
+
fj = lambda {|*a| File.join lib_dir, *a }
|
265
|
+
load fj.call( 'smql_to_ar.rb')
|
266
|
+
load fj.call( 'smql_to_ar', 'condition_types.rb')
|
267
|
+
load fj.call( 'smql_to_ar', 'query_builder.rb')
|
268
|
+
end
|
217
269
|
end
|