safrano 0.0.1 → 0.0.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 +4 -4
- data/lib/odata/batch.rb +190 -0
- data/lib/odata/collection.rb +408 -0
- data/lib/odata/collection_filter.rb +447 -0
- data/lib/odata/collection_order.rb +91 -0
- data/lib/odata/entity.rb +267 -0
- data/lib/odata/error.rb +86 -0
- data/lib/odata/relations.rb +79 -0
- data/lib/odata/walker.rb +100 -0
- metadata +24 -2
@@ -0,0 +1,447 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'odata/error.rb'
|
4
|
+
|
5
|
+
# a few helper method
|
6
|
+
class String
|
7
|
+
# MASK_RGX = /'([^']*)'/.freeze
|
8
|
+
MASK_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
9
|
+
# QUOTED_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
10
|
+
UNMASK_RGX = /'(%?)(\$\d+)(%?)'/.freeze
|
11
|
+
def with_mask_quoted_substrings!
|
12
|
+
cnt = 0
|
13
|
+
repl = {}
|
14
|
+
gsub!(MASK_RGX) do |_m|
|
15
|
+
cnt += 1
|
16
|
+
repl["$#{cnt}"] = Regexp.last_match(1)
|
17
|
+
"'$#{cnt}'"
|
18
|
+
end
|
19
|
+
yield self
|
20
|
+
|
21
|
+
gsub!(UNMASK_RGX) do |_m|
|
22
|
+
k = Regexp.last_match(2).to_s
|
23
|
+
"'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def with_mask_quoted_substrings
|
28
|
+
cnt = 0
|
29
|
+
repl = {}
|
30
|
+
tmpstr = gsub(MASK_RGX) do |_m|
|
31
|
+
cnt += 1
|
32
|
+
repl["$#{cnt}"] = Regexp.last_match(1)
|
33
|
+
"'$#{cnt}'"
|
34
|
+
end
|
35
|
+
yield tmpstr
|
36
|
+
# tmpstr2.gsub(UNMASK_RGX) do |_m|
|
37
|
+
# k = Regexp.last_match(2).to_s
|
38
|
+
# "'#{Regexp.last_match(1)}#{repl[k]}#{Regexp.last_match(3)}'"
|
39
|
+
# end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Handles filtering with ruby expressions
|
44
|
+
# (eval and DB full scan used --> better avoid using this)
|
45
|
+
module FilterWithRuby
|
46
|
+
# this module requires the @fn attribute to exist where it is used
|
47
|
+
def fn=(farg)
|
48
|
+
@fn = farg
|
49
|
+
@fn_tab = farg.split('/').map(&:to_sym)
|
50
|
+
end
|
51
|
+
|
52
|
+
# returns the attribute named "@fn" of object inp. This version assumes that
|
53
|
+
# @fn can be a path like "address/city"
|
54
|
+
def get_value(inp, colescev = '')
|
55
|
+
obj = inp
|
56
|
+
@fn_tab.each do |csymb|
|
57
|
+
tmp = obj.send(csymb)
|
58
|
+
if tmp.nil?
|
59
|
+
obj = colescev
|
60
|
+
break
|
61
|
+
else
|
62
|
+
obj = tmp
|
63
|
+
end
|
64
|
+
end
|
65
|
+
obj
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# filter base class and subclass in our OData namespace
|
70
|
+
module OData
|
71
|
+
# handles associations in filter arguments
|
72
|
+
module FilterWithAssoc
|
73
|
+
def get_assoc
|
74
|
+
@assoc, @fn = @fn.split('/') if @fn.include?('/')
|
75
|
+
end
|
76
|
+
|
77
|
+
def get_qualified_fn(dtcx)
|
78
|
+
seqtn = if dtcx.respond_to? :table_name
|
79
|
+
@assoc ? @assoc.to_sym : dtcx.table_name
|
80
|
+
else
|
81
|
+
@assoc ? @assoc.to_sym : dtcx.model.table_name
|
82
|
+
end
|
83
|
+
Sequel[seqtn][@fn.to_sym]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# base class for filtering
|
88
|
+
class Filter
|
89
|
+
attr_reader :assoc
|
90
|
+
attr_reader :assocs
|
91
|
+
QUOTED_RGX = /'((?:[^']|(?:\'{2}))*)'/.freeze
|
92
|
+
NOTQ_RGX = /((?:[^'\s\)])*)/.freeze
|
93
|
+
|
94
|
+
def initialize(matx)
|
95
|
+
@matx = matx
|
96
|
+
@assocs = Set.new
|
97
|
+
end
|
98
|
+
|
99
|
+
class << self
|
100
|
+
attr_reader :regexp
|
101
|
+
end
|
102
|
+
|
103
|
+
# input : the filter string
|
104
|
+
# returns a filter object that should have a apply_to(cx) method
|
105
|
+
def self.new_by_parse(filterstr, dtset = nil)
|
106
|
+
# handle complex flat expressions (flat = without relationships)
|
107
|
+
# with Sequel
|
108
|
+
ComplexFilter.new(filterstr, dtset)
|
109
|
+
end
|
110
|
+
|
111
|
+
# try to handle a simple expression like < fieldname EQ value >
|
112
|
+
# or < substringof(a,b) >
|
113
|
+
# NO parenthesis and no AND/OR handled here
|
114
|
+
def self.new_full_match(filterstr)
|
115
|
+
full_reg = Regexp.new(/\A#{regexp}\z/)
|
116
|
+
# this needs to return nil, not false, or some testcase will fail
|
117
|
+
return nil unless (matx = filterstr.match(full_reg))
|
118
|
+
|
119
|
+
# actually it's self.new(matx)
|
120
|
+
new(matx)
|
121
|
+
end
|
122
|
+
|
123
|
+
# handle complex deep (relations) expressions with Ruby
|
124
|
+
def self.new_full_match_complexpr_by_ruby(filterstr)
|
125
|
+
ComplexFilterByRuby.new(filterstr)
|
126
|
+
end
|
127
|
+
|
128
|
+
def apply_to_dataset(dtcx)
|
129
|
+
dtcx
|
130
|
+
end
|
131
|
+
|
132
|
+
def apply_associations(dtcx)
|
133
|
+
@assocs.each { |aj| dtcx = dtcx.association_join(aj) }
|
134
|
+
dtcx
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# well...
|
139
|
+
class EqualFilter < Filter
|
140
|
+
include FilterWithRuby
|
141
|
+
include FilterWithAssoc
|
142
|
+
EQUAL_KW_RGX = '[eE][qQ]|[LlgGNn][eETt]'.freeze
|
143
|
+
RIGHT_RGX = %q{'[^']+'|[^'\)\s]+}.freeze
|
144
|
+
|
145
|
+
def self.qualified_regexp(paths_rx)
|
146
|
+
/\s*(#{paths_rx})\s+(#{EQUAL_KW_RGX})\s+(?:#{QUOTED_RGX}|#{NOTQ_RGX})\s*/
|
147
|
+
end
|
148
|
+
|
149
|
+
def initialize(matx)
|
150
|
+
super(matx)
|
151
|
+
self.fn = matx[1]
|
152
|
+
get_assoc if @fn
|
153
|
+
|
154
|
+
@val = matx[3] ? matx[3].gsub("''", "'") : matx[4]
|
155
|
+
|
156
|
+
@op = matx[2].downcase
|
157
|
+
end
|
158
|
+
|
159
|
+
# ugly hack... but working
|
160
|
+
def gen_sql(dtcx)
|
161
|
+
# handle ambiguous column names (joins) by qualifiying the names
|
162
|
+
# seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
|
163
|
+
# handle ambiguous column names (joins) by qualifiying the names
|
164
|
+
# for the main table as well
|
165
|
+
# TODO: find a better way to differentiate between the two types
|
166
|
+
# of dtcx : Model or Dataset
|
167
|
+
|
168
|
+
# DONE: same handling for order and substring and everywhere where
|
169
|
+
# qualified fieldnames could be needed (uses attrib path regexp)
|
170
|
+
x = apply_to_dataset(dtcx)
|
171
|
+
# ugly ugly hack :-(
|
172
|
+
y = x.unordered.sql.sub(x.unfiltered.unordered.sql + ' WHERE', '')
|
173
|
+
y
|
174
|
+
end
|
175
|
+
|
176
|
+
def apply_to_dataset(dtcx)
|
177
|
+
seqfn = get_qualified_fn(dtcx)
|
178
|
+
# using @val in Procs below does not work reliably. Using a local var
|
179
|
+
# seems to work better
|
180
|
+
argval = @val
|
181
|
+
case @op
|
182
|
+
when 'eq'
|
183
|
+
dtcx.where(seqfn => @val)
|
184
|
+
when 'ne'
|
185
|
+
dtcx.exclude(seqfn => @val)
|
186
|
+
when 'le'
|
187
|
+
dtcx.where { seqfn <= argval }
|
188
|
+
when 'ge'
|
189
|
+
dtcx.where { seqfn >= argval }
|
190
|
+
when 'lt'
|
191
|
+
dtcx.where { seqfn < argval }
|
192
|
+
when 'gt'
|
193
|
+
dtcx.where { seqfn > argval }
|
194
|
+
else
|
195
|
+
raise OData::FilterParseError
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def apply_to_object(inp)
|
200
|
+
case @op
|
201
|
+
when 'eq'
|
202
|
+
get_value(inp) == @val
|
203
|
+
when 'ne'
|
204
|
+
get_value(inp) != @val
|
205
|
+
else
|
206
|
+
raise OData::FilterParseError
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# keyword based
|
212
|
+
class FilterWithKeyword < Filter
|
213
|
+
attr_reader :keyword
|
214
|
+
end
|
215
|
+
|
216
|
+
# for substringof('value', field)
|
217
|
+
class SubstringOfFilterSig1 < FilterWithKeyword
|
218
|
+
include FilterWithRuby
|
219
|
+
include FilterWithAssoc
|
220
|
+
|
221
|
+
def self.qualified_regexp(paths_rx)
|
222
|
+
# Note: QUOTED_RGX captures the string content without
|
223
|
+
# start and end quote (no need to un-quote)
|
224
|
+
# NOTQ_RGX captures an not-quoted value argument (like for numbers)
|
225
|
+
/(substringof)\((?:#{QUOTED_RGX}|#{NOTQ_RGX}),\s*(#{paths_rx})\)/
|
226
|
+
end
|
227
|
+
|
228
|
+
def initialize(matx)
|
229
|
+
super(matx)
|
230
|
+
# binding.pry
|
231
|
+
self.fn = matx[4]
|
232
|
+
get_assoc if @fn
|
233
|
+
# @val = matx[2].strip_single_quote
|
234
|
+
@val = matx[2] ? matx[2].gsub("''", "'") : matx[3]
|
235
|
+
@keyword = matx[1]
|
236
|
+
end
|
237
|
+
|
238
|
+
def whcl(dtcx)
|
239
|
+
# handle ambiguous column names (joins) by qualifiying the names
|
240
|
+
# seqfn = @assoc ? Sequel[@assoc.to_sym][@fn.to_sym] : @fn.to_sym
|
241
|
+
seqfn = get_qualified_fn(dtcx)
|
242
|
+
Sequel.like(seqfn, "%#{@val}%")
|
243
|
+
end
|
244
|
+
|
245
|
+
def gen_sql(dtcx)
|
246
|
+
sql_ = ''
|
247
|
+
dt = dtcx.respond_to?(:dataset) ? dtcx.dataset : dtcx
|
248
|
+
dt.literal_append(sql_, whcl(dt))
|
249
|
+
sql_
|
250
|
+
end
|
251
|
+
|
252
|
+
def apply_to_dataset(dtcx)
|
253
|
+
dtcx.where(whcl(dtcx))
|
254
|
+
end
|
255
|
+
|
256
|
+
def apply_to_object(inp)
|
257
|
+
get_value(inp).include?(@val)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
# for substringof(field,'value')
|
261
|
+
class SubstringOfFilterSig2 < FilterWithKeyword
|
262
|
+
include FilterWithRuby
|
263
|
+
include FilterWithAssoc
|
264
|
+
|
265
|
+
def initialize(matx)
|
266
|
+
super(matx)
|
267
|
+
self.fn = matx[2]
|
268
|
+
get_assoc if @fn
|
269
|
+
@val = matx[3]
|
270
|
+
@keyword = matx[1]
|
271
|
+
|
272
|
+
raise FilterParseError
|
273
|
+
end
|
274
|
+
|
275
|
+
def apply_to_dataset(dtcx)
|
276
|
+
dtcx
|
277
|
+
end
|
278
|
+
|
279
|
+
def apply_to_object(inp)
|
280
|
+
get_value(inp).include?(@val)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Filter by start/end with
|
285
|
+
class StartOrEndsWithFilter < FilterWithKeyword
|
286
|
+
include FilterWithRuby
|
287
|
+
include FilterWithAssoc
|
288
|
+
# DONE: better handle quotes vs not-quotes. Unblanced quotes are now handled
|
289
|
+
# example : startswith(year, 190')
|
290
|
+
|
291
|
+
def self.qualified_regexp(paths)
|
292
|
+
/(endswith|startswith)\((#{paths}),\s*(?:#{QUOTED_RGX}|#{NOTQ_RGX})\s*\)/
|
293
|
+
end
|
294
|
+
|
295
|
+
def initialize(matx)
|
296
|
+
super(matx)
|
297
|
+
# DONE: the regexp should only match on known field names...
|
298
|
+
# --> use qualified_regexp(attr_paths_rgx)
|
299
|
+
self.fn = matx[2]
|
300
|
+
get_assoc if @fn
|
301
|
+
# double quotes need to be unescaped
|
302
|
+
@val = matx[3] ? matx[3].gsub("''", "'") : matx[4]
|
303
|
+
@keyword = matx[1]
|
304
|
+
ve = Regexp.escape(@val)
|
305
|
+
# used for "filter with ruby"
|
306
|
+
case @keyword
|
307
|
+
when 'startswith'
|
308
|
+
@startend_rgx = Regexp.new("\A#{ve}")
|
309
|
+
when 'endswith'
|
310
|
+
@startend_rgx = Regexp.new("#{ve}\z")
|
311
|
+
else
|
312
|
+
raise FilterParseError
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def whcl(dtcx)
|
317
|
+
seqfn = get_qualified_fn(dtcx)
|
318
|
+
case @keyword
|
319
|
+
when 'startswith'
|
320
|
+
Sequel.like(seqfn, "#{@val}%")
|
321
|
+
when 'endswith'
|
322
|
+
Sequel.like(seqfn, "%#{@val}")
|
323
|
+
else
|
324
|
+
raise FilterParseError
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
def gen_sql(dtcx)
|
329
|
+
sql_ = ''
|
330
|
+
dt = dtcx.respond_to?(:dataset) ? dtcx.dataset : dtcx
|
331
|
+
dt.literal_append(sql_, whcl(dt))
|
332
|
+
# puts "in gen_sql : sql_ == " , sql_
|
333
|
+
sql_
|
334
|
+
end
|
335
|
+
|
336
|
+
def apply_to_dataset(dtcx)
|
337
|
+
dtcx.where(whcl(dtcx))
|
338
|
+
end
|
339
|
+
|
340
|
+
def apply_to_object(inp)
|
341
|
+
get_value(inp) =~ @startend_rgx
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
FILTER_CLASSES = [EqualFilter,
|
346
|
+
SubstringOfFilterSig1,
|
347
|
+
SubstringOfFilterSig2,
|
348
|
+
StartOrEndsWithFilter].freeze
|
349
|
+
def Filter.new_by_simple_full_match(filterstr)
|
350
|
+
new_filt_obj = nil
|
351
|
+
OData::FILTER_CLASSES.find do |fklas|
|
352
|
+
new_filt_obj = fklas.new_full_match(filterstr)
|
353
|
+
end
|
354
|
+
new_filt_obj
|
355
|
+
end
|
356
|
+
|
357
|
+
# handles some combinations
|
358
|
+
class ComplexFilter < Filter
|
359
|
+
# list of active Subfilter classes. The ordering is important
|
360
|
+
SUBFILTERS = [EqualFilter,
|
361
|
+
SubstringOfFilterSig1,
|
362
|
+
StartOrEndsWithFilter].freeze
|
363
|
+
# for counting number of AND OR's
|
364
|
+
ANDORRGX = /(AND|OR\s+)/i.freeze
|
365
|
+
|
366
|
+
# for detecting consecutive AND OR
|
367
|
+
ANDORERRRGX = /(AND|OR)\s+[\(\)]*(AND|OR)/i.freeze
|
368
|
+
|
369
|
+
def initialize(filterstr, dtset)
|
370
|
+
super(nil)
|
371
|
+
@filterstr = filterstr.dup
|
372
|
+
|
373
|
+
@attrib_paths_url_regexp = if dtset.is_a? Sequel::Model::ClassMethods
|
374
|
+
dtset.attrib_paths_url_regexp.dup
|
375
|
+
else
|
376
|
+
dtset.model.attrib_paths_url_regexp.dup
|
377
|
+
end
|
378
|
+
@active_subfs = []
|
379
|
+
@osql = filterstr.dup
|
380
|
+
@dt = dtset
|
381
|
+
|
382
|
+
md = nil
|
383
|
+
@osql.with_mask_quoted_substrings! do |s|
|
384
|
+
unless (@and_or_err = ANDORERRRGX.match(s))
|
385
|
+
md = ANDORRGX.match(s)
|
386
|
+
|
387
|
+
@expected_count = md.nil? ? 1 : md.size
|
388
|
+
SUBFILTERS.each { |klass| init_subfilter(klass) }
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def parse_error?
|
394
|
+
@and_or_err || (@active_subfs.size != @expected_count)
|
395
|
+
end
|
396
|
+
|
397
|
+
def init_subfilter(subfiltclass)
|
398
|
+
rgx = subfiltclass.qualified_regexp(@attrib_paths_url_regexp)
|
399
|
+
@osql.gsub!(rgx) do |_mx|
|
400
|
+
subf = subfiltclass.new(Regexp.last_match)
|
401
|
+
@active_subfs << subf
|
402
|
+
@assocs.add(subf.assoc.to_sym) if subf.assoc
|
403
|
+
subf.gen_sql(@dt)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
def apply_to_dataset(dtcx)
|
408
|
+
# new Sequel 5.6 requires -- lit --
|
409
|
+
dtcx.where(Sequel.lit(@osql))
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
# Handle complex filter expression with Ruby
|
414
|
+
class ComplexFilterByRuby < Filter
|
415
|
+
def initialize(filterstr)
|
416
|
+
@subf = []
|
417
|
+
@f = nil
|
418
|
+
@rbcode = filterstr.dup
|
419
|
+
@rbcode.with_mask_quoted_substrings! do |_s|
|
420
|
+
SUBFILTERS.each { |klass| init_subfilter(klass) }
|
421
|
+
downcase_and_or
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
# downcase AND OR in @rbcode otherwise it is seen as a Ruby constant
|
426
|
+
# instead as a ruby langu keyword
|
427
|
+
def downcase_and_or
|
428
|
+
@rbcode.gsub!(/(OR|Or|AND|And)/) do |_mx|
|
429
|
+
Regexp.last_match[1].downcase
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
def init_subfilter(subfiltclass)
|
434
|
+
@rbcode.gsub!(subfiltclass.regexp) do |_mx|
|
435
|
+
@f = subfiltclass.new(Regexp.last_match)
|
436
|
+
@subf << @f
|
437
|
+
"@subf[#{@subf.size - 1}].apply_to_object(@o)"
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
def apply_to_object(inp)
|
442
|
+
@o = inp
|
443
|
+
b = binding
|
444
|
+
eval(@rbcode, b)
|
445
|
+
end
|
446
|
+
end
|
447
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
require 'odata/error.rb'
|
5
|
+
|
6
|
+
# Ordering with ruby expression
|
7
|
+
module OrderWithRuby
|
8
|
+
# this module requires the @fn attribute to exist where it is used
|
9
|
+
def fn=(fnam)
|
10
|
+
@fn = fnam
|
11
|
+
@fn_tab = fnam.split('/').map(&:to_sym)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# all ordering related classes in our OData module
|
16
|
+
module OData
|
17
|
+
# base class for ordering
|
18
|
+
class Order
|
19
|
+
attr_reader :assoc
|
20
|
+
attr_reader :assocs
|
21
|
+
attr_reader :oarg
|
22
|
+
def initialize(ostr, _dt)
|
23
|
+
ostr.strip!
|
24
|
+
@orderp = ostr
|
25
|
+
@assocs = Set.new
|
26
|
+
build_oarg if @orderp
|
27
|
+
end
|
28
|
+
|
29
|
+
class << self
|
30
|
+
attr_reader :regexp
|
31
|
+
end
|
32
|
+
|
33
|
+
# input : the filter string
|
34
|
+
# returns a filter object that should have a apply_to(cx) method
|
35
|
+
def self.new_by_parse(orderstr, dtset = nil)
|
36
|
+
Order.new_full_match_complexpr(orderstr, dtset)
|
37
|
+
end
|
38
|
+
|
39
|
+
# handle with Sequel
|
40
|
+
def self.new_full_match_complexpr(orderstr, dtset)
|
41
|
+
ComplexOrder.new(orderstr, dtset)
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply_to_dataset(dtcx)
|
45
|
+
dtcx
|
46
|
+
end
|
47
|
+
|
48
|
+
def apply_associations(dtcx)
|
49
|
+
@assocs.each { |aj| dtcx = dtcx.association_join(aj) }
|
50
|
+
dtcx
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_oarg
|
54
|
+
field, ord = @orderp.split(' ')
|
55
|
+
oargu = if field.include?('/')
|
56
|
+
@assoc, field = field.split('/')
|
57
|
+
@assoc = @assoc.to_sym
|
58
|
+
Sequel[@assoc][field.strip.to_sym]
|
59
|
+
else
|
60
|
+
Sequel[field.strip.to_sym]
|
61
|
+
end
|
62
|
+
|
63
|
+
@oarg = if ord == 'desc'
|
64
|
+
Sequel.desc(oargu)
|
65
|
+
else
|
66
|
+
Sequel.asc(oargu)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# complex ordering logic
|
72
|
+
class ComplexOrder < Order
|
73
|
+
def initialize(orderstr, dtset)
|
74
|
+
super
|
75
|
+
@dt = dtset
|
76
|
+
@olist = []
|
77
|
+
return unless orderstr
|
78
|
+
|
79
|
+
@olist = orderstr.split(',').map do |ostr|
|
80
|
+
oo = Order.new(ostr, dtset)
|
81
|
+
@assocs.add oo.assoc if oo.assoc
|
82
|
+
oo.oarg
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def apply_to_dataset(dtcx)
|
87
|
+
@olist.each { |oarg| dtcx = dtcx.order(oarg) }
|
88
|
+
dtcx
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|