intermine 0.98.01
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/Gemfile +4 -0
- data/LICENCE +165 -0
- data/MANIFEST +0 -0
- data/README.rdoc +79 -0
- data/Rakefile +67 -0
- data/lib/intermine/lists.rb +716 -0
- data/lib/intermine/model.rb +867 -0
- data/lib/intermine/query.rb +1569 -0
- data/lib/intermine/results.rb +196 -0
- data/lib/intermine/service.rb +253 -0
- data/lib/intermine/version.rb +3 -0
- data/test/data/lists.json +29 -0
- data/test/data/model.json +3 -0
- data/test/data/resultobjs.json +3 -0
- data/test/data/resultrow.json +1 -0
- data/test/data/resultset.json +3 -0
- data/test/data/testmodel_model.xml +94 -0
- data/test/live_test.rb +35 -0
- data/test/test.rb +84 -0
- data/test/test_helper.rb +67 -0
- data/test/test_lists.rb +68 -0
- data/test/test_model.rb +417 -0
- data/test/test_query.rb +1202 -0
- data/test/test_result_row.rb +114 -0
- data/test/test_results.rb +22 -0
- data/test/test_service.rb +86 -0
- data/test/test_sugar.rb +219 -0
- data/test/unit_tests.rb +6 -0
- metadata +192 -0
@@ -0,0 +1,1569 @@
|
|
1
|
+
require "rexml/document"
|
2
|
+
require "rexml/streamlistener"
|
3
|
+
require "stringio"
|
4
|
+
require "intermine/model"
|
5
|
+
require "intermine/results"
|
6
|
+
require "intermine/service"
|
7
|
+
require "intermine/lists"
|
8
|
+
|
9
|
+
include InterMine
|
10
|
+
|
11
|
+
unless String.instance_methods.include?(:start_with?)
|
12
|
+
|
13
|
+
class String
|
14
|
+
|
15
|
+
def start_with?(prefix)
|
16
|
+
prefix = Regexp.escape(prefix.to_s)
|
17
|
+
return self.match("^#{prefix}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def end_with?(suffix)
|
21
|
+
suffix = Regexp.escape(suffix.to_s)
|
22
|
+
return self.match("#{suffix}$")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Array
|
29
|
+
def every(count)
|
30
|
+
chunks = []
|
31
|
+
each_with_index do |item, index|
|
32
|
+
chunks << [] if index % count == 0
|
33
|
+
chunks.last << item
|
34
|
+
end
|
35
|
+
chunks
|
36
|
+
end
|
37
|
+
alias / every
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
module InterMine::PathQuery
|
42
|
+
|
43
|
+
include REXML
|
44
|
+
|
45
|
+
class QueryLoader
|
46
|
+
|
47
|
+
attr_reader :model
|
48
|
+
|
49
|
+
def initialize(model)
|
50
|
+
@model = model
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_handler
|
54
|
+
return QueryBuilder.new(@model)
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse(xml)
|
58
|
+
xml = StringIO.new(xml.to_s)
|
59
|
+
handler = get_handler
|
60
|
+
REXML::Document.parse_stream(xml, handler)
|
61
|
+
return handler.query
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
class TemplateLoader < QueryLoader
|
67
|
+
|
68
|
+
def get_handler
|
69
|
+
return TemplateBuilder.new(@model)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class QueryBuilder
|
74
|
+
include REXML::StreamListener
|
75
|
+
|
76
|
+
def initialize(model)
|
77
|
+
@model = model
|
78
|
+
@query_attributes = {}
|
79
|
+
@subclass_constraints = []
|
80
|
+
@coded_constraints = []
|
81
|
+
@joins = []
|
82
|
+
end
|
83
|
+
|
84
|
+
def query
|
85
|
+
q = create_query
|
86
|
+
# Add first, in case other bits depend on them
|
87
|
+
@subclass_constraints.each do |sc|
|
88
|
+
q.add_constraint(sc)
|
89
|
+
end
|
90
|
+
@joins.each do |j|
|
91
|
+
q.add_join(*j)
|
92
|
+
end
|
93
|
+
@coded_constraints.each do |con|
|
94
|
+
q.add_constraint(con)
|
95
|
+
end
|
96
|
+
@query_attributes.sort_by {|k, v| k}.reverse.each do |k,v|
|
97
|
+
begin
|
98
|
+
q.send(k + "=", v)
|
99
|
+
rescue
|
100
|
+
end
|
101
|
+
end
|
102
|
+
return q
|
103
|
+
end
|
104
|
+
|
105
|
+
def tag_start(name, attrs)
|
106
|
+
@in_value = false
|
107
|
+
if name == "query"
|
108
|
+
attrs.each do |a|
|
109
|
+
@query_attributes[a.first] = a.last if a.first != "model"
|
110
|
+
end
|
111
|
+
elsif name=="constraint"
|
112
|
+
process_constraint(attrs)
|
113
|
+
elsif name=="value"
|
114
|
+
@in_value = true
|
115
|
+
elsif name=="join"
|
116
|
+
@joins.push([attrs["path"], attrs["style"]])
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def process_constraint(attrs)
|
121
|
+
if attrs.has_key?("type")
|
122
|
+
@subclass_constraints.push({:path => attrs["path"], :sub_class => attrs["type"]})
|
123
|
+
else
|
124
|
+
args = {}
|
125
|
+
args[:path] = attrs["path"]
|
126
|
+
args[:op] = attrs["op"]
|
127
|
+
args[:value] = attrs["value"] if attrs.has_key?("value")
|
128
|
+
args[:loopPath] = attrs["loopPath"] if attrs.has_key?("loopPath")
|
129
|
+
args[:extra_value] = attrs["extraValue"] if attrs.has_key?("extraValue")
|
130
|
+
args[:code] = attrs["code"]
|
131
|
+
if MultiValueConstraint.valid_ops.include?(attrs["op"])
|
132
|
+
args[:values] = [] # actual values will be pushed on later
|
133
|
+
end
|
134
|
+
if attrs.has_key?("loopPath")
|
135
|
+
LoopConstraint.xml_ops.each do |k,v|
|
136
|
+
args[:op] = k if v == args[:op]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@coded_constraints.push(args)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def text(t)
|
144
|
+
@coded_constraints.last[:values].push(t)
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def create_query
|
150
|
+
return Query.new(@model)
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
class TemplateBuilder < QueryBuilder
|
156
|
+
|
157
|
+
def initialize(model)
|
158
|
+
super
|
159
|
+
@template_attrs = {}
|
160
|
+
end
|
161
|
+
|
162
|
+
def tag_start(name, attrs)
|
163
|
+
super
|
164
|
+
if name == "template"
|
165
|
+
attrs.each do |a|
|
166
|
+
@template_attrs[a.first] = a.last
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def query
|
172
|
+
template = super
|
173
|
+
@template_attrs.each do |k,v|
|
174
|
+
template.send(k + '=', v)
|
175
|
+
end
|
176
|
+
return template
|
177
|
+
end
|
178
|
+
|
179
|
+
def process_constraint(attrs)
|
180
|
+
super
|
181
|
+
unless attrs.has_key? "type"
|
182
|
+
if attrs.has_key?("editable") and attrs["editable"].downcase == "false"
|
183
|
+
@coded_constraints.last[:editable] = false
|
184
|
+
else
|
185
|
+
@coded_constraints.last[:editable] = true
|
186
|
+
end
|
187
|
+
@coded_constraints.last[:switchable] = attrs["switchable"] || "locked"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
def create_query
|
194
|
+
return Template.new(@model, nil, @model.service)
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
198
|
+
|
199
|
+
# == A class representing a structured query against an InterMine Data-Warehouse
|
200
|
+
#
|
201
|
+
# Queries represent structured requests for data from an InterMine data-warehouse. They
|
202
|
+
# consist basically of output columns you select, and a set of constraints on the results
|
203
|
+
# to return. These are known as the "view" and the "constraints". In a nod to the SQL-origins
|
204
|
+
# of the queries, and to the syntax of ActiveRecord, there is both a method-chaining SQL-ish
|
205
|
+
# DSL, and a more isolating common InterMine DSL.
|
206
|
+
#
|
207
|
+
# query = service.query("Gene").select("*").where("proteins.molecularWeight" => {">" => 10000})
|
208
|
+
# query.each_result do |gene|
|
209
|
+
# puts gene.symbol
|
210
|
+
# end
|
211
|
+
#
|
212
|
+
# OR:
|
213
|
+
#
|
214
|
+
# query = service.query("Gene")
|
215
|
+
# query.add_views("*")
|
216
|
+
# query.add_constraint("proteins.molecularWeight", ">", 10000)
|
217
|
+
# ...
|
218
|
+
#
|
219
|
+
# The main differences from SQL are that the joining between tables is implicit
|
220
|
+
# and automatic. Simply by naming the column "Gene.proteins.molecularWeight" we have access
|
221
|
+
# to the protein table joined onto the gene table. (A consequence of this is that all queries must have a
|
222
|
+
# unique root that all paths descend from, and we do not permit right outer joins.)
|
223
|
+
#
|
224
|
+
# You can define the following features of a query:
|
225
|
+
# * The output column
|
226
|
+
# * The filtering constraints (what values certain columns must or must not have)
|
227
|
+
# * The sort order of the results
|
228
|
+
# * The way constraints are combined (AND or OR)
|
229
|
+
#
|
230
|
+
# In processing results, there are two powerful result formats available, depending on
|
231
|
+
# whether you want to process results row by row, or whether you would like the information grouped into
|
232
|
+
# logically coherent records. The latter is more similar to the ORM model, and can be seen above.
|
233
|
+
# The mechanisms we offer for row access allow accessing cell values of the result table
|
234
|
+
# transparently by index or column-name.
|
235
|
+
#
|
236
|
+
#:include:contact_header.rdoc
|
237
|
+
#
|
238
|
+
class Query
|
239
|
+
|
240
|
+
# The first possible constraint code
|
241
|
+
LOWEST_CODE = "A"
|
242
|
+
|
243
|
+
# The last possible constraint code
|
244
|
+
HIGHEST_CODE = "Z"
|
245
|
+
|
246
|
+
# The (optional) name of the query. Used in automatic access (eg: "query1")
|
247
|
+
attr_accessor :name
|
248
|
+
|
249
|
+
# A human readable title of the query (eg: "Gene --> Protein Domain")
|
250
|
+
attr_accessor :title
|
251
|
+
|
252
|
+
# The root class of the query.
|
253
|
+
attr_accessor :root
|
254
|
+
|
255
|
+
# The data model associated with the query
|
256
|
+
attr_reader :model
|
257
|
+
|
258
|
+
# All the current Join objects on the query
|
259
|
+
attr_reader :joins
|
260
|
+
|
261
|
+
# All the current constraints on the query
|
262
|
+
attr_reader :constraints
|
263
|
+
|
264
|
+
# All the columns currently selected for output.
|
265
|
+
attr_reader :views
|
266
|
+
|
267
|
+
# The current sort-order.
|
268
|
+
attr_reader :sort_order
|
269
|
+
|
270
|
+
# The current logic (as a LogicGroup)
|
271
|
+
attr_reader :logic
|
272
|
+
|
273
|
+
# The service this query is associated with
|
274
|
+
attr_reader :service
|
275
|
+
|
276
|
+
# URLs for internal consumption.
|
277
|
+
attr_reader :list_upload_uri, :list_append_uri
|
278
|
+
|
279
|
+
# Construct a new query object. You should not use this directly.
|
280
|
+
# Instead use the factory methods in Service.
|
281
|
+
#
|
282
|
+
# query = service.query("Gene")
|
283
|
+
#
|
284
|
+
def initialize(model, root=nil, service=nil)
|
285
|
+
@model = model
|
286
|
+
@service = service
|
287
|
+
@url = (@service.nil?) ? nil : @service.root + Service::QUERY_RESULTS_PATH
|
288
|
+
@list_upload_uri = (@service.nil?) ? nil : @service.root + Service::QUERY_TO_LIST_PATH
|
289
|
+
@list_append_uri = (@service.nil?) ? nil : @service.root + Service::QUERY_APPEND_PATH
|
290
|
+
if root
|
291
|
+
@root = InterMine::Metadata::Path.new(root, model).rootClass
|
292
|
+
end
|
293
|
+
@constraints = []
|
294
|
+
@joins = []
|
295
|
+
@views = []
|
296
|
+
@sort_order = []
|
297
|
+
@used_codes = []
|
298
|
+
@logic_parser = LogicParser.new(self)
|
299
|
+
@constraint_factory = ConstraintFactory.new(self)
|
300
|
+
end
|
301
|
+
|
302
|
+
# Return a parser for deserialising queries.
|
303
|
+
#
|
304
|
+
# parser = Query.parser(service.model)
|
305
|
+
# query = parser.parse(string)
|
306
|
+
# query.each_row |r|
|
307
|
+
# puts r.to_h
|
308
|
+
# end
|
309
|
+
#
|
310
|
+
def self.parser(model)
|
311
|
+
return QueryLoader.new(model)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Return all the constraints that have codes and can thus
|
315
|
+
# participate in logic.
|
316
|
+
def coded_constraints
|
317
|
+
return @constraints.select {|x| !x.is_a?(SubClassConstraint)}
|
318
|
+
end
|
319
|
+
|
320
|
+
# Return all the constraints that restrict the class of
|
321
|
+
# paths in the query.
|
322
|
+
def subclass_constraints
|
323
|
+
return @constraints.select {|x| x.is_a?(SubClassConstraint)}
|
324
|
+
end
|
325
|
+
|
326
|
+
# Return an XML document node representing the XML form of the query.
|
327
|
+
#
|
328
|
+
# This is the canonical serialisable form of the query.
|
329
|
+
#
|
330
|
+
def to_xml
|
331
|
+
doc = REXML::Document.new
|
332
|
+
|
333
|
+
if @sort_order.empty?
|
334
|
+
so = SortOrder.new(@views.first, "ASC")
|
335
|
+
else
|
336
|
+
so = @sort_order.join(" ")
|
337
|
+
end
|
338
|
+
|
339
|
+
query = doc.add_element("query", {
|
340
|
+
"name" => @name,
|
341
|
+
"model" => @model.name,
|
342
|
+
"title" => @title,
|
343
|
+
"sortOrder" => so,
|
344
|
+
"view" => @views.join(" "),
|
345
|
+
"constraintLogic" => @logic
|
346
|
+
}.delete_if { |k, v | !v })
|
347
|
+
@joins.each { |join|
|
348
|
+
query.add_element("join", join.attrs)
|
349
|
+
}
|
350
|
+
subclass_constraints.each { |con|
|
351
|
+
query.add_element(con.to_elem)
|
352
|
+
}
|
353
|
+
coded_constraints.each { |con|
|
354
|
+
query.add_element(con.to_elem)
|
355
|
+
}
|
356
|
+
return doc
|
357
|
+
end
|
358
|
+
|
359
|
+
# Get your own result reader for handling the results at a low level.
|
360
|
+
def results_reader(start=0, size=nil)
|
361
|
+
return Results::ResultsReader.new(@url, self, start, size)
|
362
|
+
end
|
363
|
+
|
364
|
+
# Iterate over the results of this query one row at a time.
|
365
|
+
#
|
366
|
+
# Rows support both array-like index based access as well as
|
367
|
+
# hash-like key based access. For key based acces you can use
|
368
|
+
# either the full path or the headless short version:
|
369
|
+
#
|
370
|
+
# query.each_row do |row|
|
371
|
+
# puts r["Gene.symbol"], r["proteins.primaryIdentifier"]
|
372
|
+
# puts r[0]
|
373
|
+
# puts r.to_a # Materialize the row an an Array
|
374
|
+
# puts r.to_h # Materialize the row an a Hash
|
375
|
+
# end
|
376
|
+
#
|
377
|
+
def each_row(start=0, size=nil)
|
378
|
+
results_reader(start, size).each_row {|row|
|
379
|
+
yield row
|
380
|
+
}
|
381
|
+
end
|
382
|
+
|
383
|
+
# Iterate over the results, one record at a time.
|
384
|
+
#
|
385
|
+
# query.each_result do |gene|
|
386
|
+
# puts gene.symbol
|
387
|
+
# gene.proteins.each do |prot|
|
388
|
+
# puts prot.primaryIdentifier
|
389
|
+
# end
|
390
|
+
# end
|
391
|
+
#
|
392
|
+
def each_result(start=0, size=nil)
|
393
|
+
results_reader(start, size).each_result {|row|
|
394
|
+
yield row
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
# Return the number of result rows this query will return in its current state.
|
399
|
+
# This makes a very small request to the webservice, and is the most efficient
|
400
|
+
# method of getting the size of the result set.
|
401
|
+
def count
|
402
|
+
return results_reader.get_size
|
403
|
+
end
|
404
|
+
|
405
|
+
# Returns an Array of ResultRow objects containing the
|
406
|
+
# data returned by running this query, starting at the given offset and
|
407
|
+
# containing up to the given maximum size.
|
408
|
+
#
|
409
|
+
# The webservice enforces a maximum page-size of 10,000,000 rows,
|
410
|
+
# independent of any size you specify - this can be obviated with paging
|
411
|
+
# for large result sets.
|
412
|
+
#
|
413
|
+
# rows = query.rows
|
414
|
+
# rows.last["symbol"]
|
415
|
+
# => "eve"
|
416
|
+
#
|
417
|
+
def rows(start=0, size=nil)
|
418
|
+
res = []
|
419
|
+
results_reader(start, size).each_row {|row|
|
420
|
+
res << row
|
421
|
+
}
|
422
|
+
res
|
423
|
+
end
|
424
|
+
|
425
|
+
# Return objects corresponding to the type of data requested, starting
|
426
|
+
# at the given row offset.
|
427
|
+
#
|
428
|
+
# genes = query.results
|
429
|
+
# genes.last.symbol
|
430
|
+
# => "eve"
|
431
|
+
#
|
432
|
+
def results(start=0, size=nil)
|
433
|
+
res = []
|
434
|
+
results_reader(start, size).each_result {|row|
|
435
|
+
res << row
|
436
|
+
}
|
437
|
+
res
|
438
|
+
end
|
439
|
+
|
440
|
+
# Return all result record objects returned by running this query.
|
441
|
+
def all
|
442
|
+
return self.results
|
443
|
+
end
|
444
|
+
|
445
|
+
# Return all the rows returned by running the query
|
446
|
+
def all_rows
|
447
|
+
return self.rows
|
448
|
+
end
|
449
|
+
|
450
|
+
# Get the first result record from the query, starting at the
|
451
|
+
# given offset. If the offset is large, then this is not an efficient
|
452
|
+
# way to retrieve this data, and you may with to consider a looping approach
|
453
|
+
# or row based access instead.
|
454
|
+
def first(start=0)
|
455
|
+
current_row = 0
|
456
|
+
# Have to iterate as start refers to row count
|
457
|
+
results_reader.each_result { |r|
|
458
|
+
if current_row == start
|
459
|
+
return r
|
460
|
+
end
|
461
|
+
current_row += 1
|
462
|
+
}
|
463
|
+
return nil
|
464
|
+
end
|
465
|
+
|
466
|
+
# Get the first row of results from the query, starting at the given offset.
|
467
|
+
def first_row(start = 0)
|
468
|
+
return self.results(start, 1).first
|
469
|
+
end
|
470
|
+
|
471
|
+
# Get the constraint on the query with the given code.
|
472
|
+
# Raises an error if there is no such constraint.
|
473
|
+
def get_constraint(code)
|
474
|
+
@constraints.each do |x|
|
475
|
+
if x.respond_to?(:code) and x.code == code
|
476
|
+
return x
|
477
|
+
end
|
478
|
+
end
|
479
|
+
raise ArgumentError, "#{code} not in query"
|
480
|
+
end
|
481
|
+
|
482
|
+
|
483
|
+
# Remove the constraint with the given code from the query.
|
484
|
+
# If no such constraint exists, no error will be raised.
|
485
|
+
def remove_constraint(code)
|
486
|
+
@constraints.reject! do |x|
|
487
|
+
x.respond_to?(:code) and x.code == code
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
# Add the given views (output columns) to the query.
|
492
|
+
#
|
493
|
+
# Any columns ending in "*" will be interpreted as a request to add
|
494
|
+
# all attribute columns from that table to the query
|
495
|
+
#
|
496
|
+
# query = service.query("Gene")
|
497
|
+
# query.add_views("*")
|
498
|
+
# query.add_to_select("*")
|
499
|
+
# query.add_views("proteins.*")
|
500
|
+
# query.add_views("pathways.*", "organism.shortName")
|
501
|
+
#
|
502
|
+
def add_views(*views)
|
503
|
+
views.flatten.map do |x|
|
504
|
+
y = add_prefix(x)
|
505
|
+
if y.end_with?("*")
|
506
|
+
prefix = y.chomp(".*")
|
507
|
+
path = InterMine::Metadata::Path.new(prefix, @model, subclasses)
|
508
|
+
attrs = path.end_cd.attributes.map {|x| prefix + "." + x.name}
|
509
|
+
add_views(attrs)
|
510
|
+
else
|
511
|
+
path = InterMine::Metadata::Path.new(y, @model, subclasses)
|
512
|
+
if @root.nil?
|
513
|
+
@root = path.rootClass
|
514
|
+
end
|
515
|
+
@views << path
|
516
|
+
end
|
517
|
+
end
|
518
|
+
return self
|
519
|
+
end
|
520
|
+
|
521
|
+
alias add_to_select add_views
|
522
|
+
|
523
|
+
# Replace any currently existing views with the given view list.
|
524
|
+
# If the view is not already an Array, it will be split by commas and whitespace.
|
525
|
+
#
|
526
|
+
def view=(*view)
|
527
|
+
@views = []
|
528
|
+
view.each do |v|
|
529
|
+
if v.is_a?(Array)
|
530
|
+
views = v
|
531
|
+
else
|
532
|
+
views = v.to_s.split(/(?:,\s*|\s+)/)
|
533
|
+
end
|
534
|
+
add_views(*views)
|
535
|
+
end
|
536
|
+
return self
|
537
|
+
end
|
538
|
+
|
539
|
+
alias select view=
|
540
|
+
|
541
|
+
# Get the current sub-class map for this query.
|
542
|
+
#
|
543
|
+
# This contains information about which fields of this query have
|
544
|
+
# been declared to be restricted to contain only a subclass of their
|
545
|
+
# normal type.
|
546
|
+
#
|
547
|
+
# > query = service.query("Gene")
|
548
|
+
# > query.where(:microArrayResults => service.model.table("FlyAtlasResult"))
|
549
|
+
# > query.subclasses
|
550
|
+
# => {"Gene.microArrayResults" => "FlyAtlasResult"}
|
551
|
+
#
|
552
|
+
def subclasses
|
553
|
+
subclasses = {}
|
554
|
+
@constraints.each do |con|
|
555
|
+
if con.is_a?(SubClassConstraint)
|
556
|
+
subclasses[con.path.to_s] = con.sub_class.to_s
|
557
|
+
end
|
558
|
+
end
|
559
|
+
return subclasses
|
560
|
+
end
|
561
|
+
|
562
|
+
# Declare how a particular join should be treated.
|
563
|
+
#
|
564
|
+
# The default join style is for an INNER join, but joins can
|
565
|
+
# optionally be declared to be LEFT OUTER joins. The difference is
|
566
|
+
# that with an inner join, each join in the query implicitly constrains
|
567
|
+
# the values of that path to be non-null, whereas an outer-join allows
|
568
|
+
# null values in the joined path. If the path passed to the constructor
|
569
|
+
# has a chain of joins, the last section is the one the join is applied to.
|
570
|
+
#
|
571
|
+
# query = service.query("Gene")
|
572
|
+
# # Allow genes without proteins
|
573
|
+
# query.add_join("proteins")
|
574
|
+
# # Demand the results contain only those genes that have interactions that have interactingGenes,
|
575
|
+
# # but allow those interactingGenes to not have any proteins.
|
576
|
+
# query.add_join("interactions.interactingGenes.proteins")
|
577
|
+
#
|
578
|
+
# The valid join styles are OUTER and INNER (case-insensitive). There is never
|
579
|
+
# any need to declare a join to be INNER, as it is inner by default. Consider
|
580
|
+
# using Query#outerjoin which is more explicitly declarative.
|
581
|
+
#
|
582
|
+
def add_join(path, style="OUTER")
|
583
|
+
p = InterMine::Metadata::Path.new(add_prefix(path), @model, subclasses)
|
584
|
+
if @root.nil?
|
585
|
+
@root = p.rootClass
|
586
|
+
end
|
587
|
+
@joins << Join.new(p, style)
|
588
|
+
return self
|
589
|
+
end
|
590
|
+
|
591
|
+
alias join add_join
|
592
|
+
|
593
|
+
# Explicitly declare a join to be an outer join.
|
594
|
+
def outerjoin(path)
|
595
|
+
return add_join(path)
|
596
|
+
end
|
597
|
+
|
598
|
+
# Add a sort order element to sort order information.
|
599
|
+
# A sort order consists of the name of an output column and
|
600
|
+
# (optionally) the direction to sort in. The default direction is "ASC".
|
601
|
+
# The valid directions are "ASC" and "DESC" (case-insensitive).
|
602
|
+
#
|
603
|
+
# query.add_sort_order("length")
|
604
|
+
# query.add_sort_order("proteins.primaryIdentifier", "desc")
|
605
|
+
#
|
606
|
+
def add_sort_order(path, direction="ASC")
|
607
|
+
p = self.path(path)
|
608
|
+
if !@views.include? p
|
609
|
+
raise ArgumentError, "Sort order (#{p}) not in view (#{@views.map {|v| v.to_s}.inspect} in #{self.name || 'unnamed query'})"
|
610
|
+
end
|
611
|
+
@sort_order << SortOrder.new(p, direction)
|
612
|
+
return self
|
613
|
+
end
|
614
|
+
|
615
|
+
# Set the sort order completely, replacing the current sort order.
|
616
|
+
#
|
617
|
+
# query.sortOrder = "Gene.length asc Gene.proteins.length desc"
|
618
|
+
#
|
619
|
+
# The sort order expression will be parsed and checked for conformity with the
|
620
|
+
# current state of the query.
|
621
|
+
def sortOrder=(so)
|
622
|
+
if so.is_a?(Array)
|
623
|
+
sos = so
|
624
|
+
else
|
625
|
+
sos = so.split(/(ASC|DESC|asc|desc)/).map {|x| x.strip}.every(2)
|
626
|
+
end
|
627
|
+
sos.each do |args|
|
628
|
+
add_sort_order(*args)
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
alias order_by add_sort_order
|
633
|
+
alias order add_sort_order
|
634
|
+
|
635
|
+
# Add a constraint to the query matching the given parameters, and
|
636
|
+
# return the created constraint.
|
637
|
+
#
|
638
|
+
# con = query.add_constraint("length", ">", 500)
|
639
|
+
#
|
640
|
+
# Note that (at least for now) the style of argument used by where and
|
641
|
+
# add_constraint is not compatible. This is on the TODO list.
|
642
|
+
def add_constraint(*parameters)
|
643
|
+
con = @constraint_factory.make_constraint(parameters)
|
644
|
+
@constraints << con
|
645
|
+
return con
|
646
|
+
end
|
647
|
+
|
648
|
+
# Returns a Path object constructed from the given path-string,
|
649
|
+
# taking the current state of the query into account (its data-model
|
650
|
+
# and subclass constraints).
|
651
|
+
def path(pathstr)
|
652
|
+
return InterMine::Metadata::Path.new(add_prefix(pathstr), @model, subclasses)
|
653
|
+
end
|
654
|
+
|
655
|
+
# Add a constraint clause to the query.
|
656
|
+
#
|
657
|
+
# query.where(:symbol => "eve")
|
658
|
+
# query.where(:symbol => %{eve h bib zen})
|
659
|
+
# query.where(:length => {:le => 100}, :symbol => "eve*")
|
660
|
+
#
|
661
|
+
# Interprets the arguments in a style similar to that of
|
662
|
+
# ActiveRecord constraints, and adds them to the query.
|
663
|
+
# If multiple constraints are supplied in a single hash (as
|
664
|
+
# in the third example), then the order in which they are
|
665
|
+
# applied to the query (and thus the codes they will receive) is
|
666
|
+
# not predictable. To determine the order use chained where clauses
|
667
|
+
# or use multiple hashes:
|
668
|
+
#
|
669
|
+
# query.where({:length => {:le => 100}}, {:symbol => "eve*"})
|
670
|
+
#
|
671
|
+
# Returns self to support method chaining
|
672
|
+
#
|
673
|
+
def where(*wheres)
|
674
|
+
if @views.empty?
|
675
|
+
self.select('*')
|
676
|
+
end
|
677
|
+
wheres.each do |w|
|
678
|
+
w.each do |k,v|
|
679
|
+
if v.is_a?(Hash)
|
680
|
+
parameters = {:path => k}
|
681
|
+
v.each do |subk, subv|
|
682
|
+
normalised_k = subk.to_s.upcase.gsub(/_/, " ")
|
683
|
+
if subk == :with
|
684
|
+
parameters[:extra_value] = subv
|
685
|
+
elsif subk == :sub_class
|
686
|
+
parameters[subk] = subv
|
687
|
+
elsif subk == :code
|
688
|
+
parameters[:code] = subv
|
689
|
+
elsif LoopConstraint.valid_ops.include?(normalised_k)
|
690
|
+
parameters[:op] = normalised_k
|
691
|
+
parameters[:loopPath] = subv
|
692
|
+
else
|
693
|
+
if subv.nil?
|
694
|
+
if subk == "="
|
695
|
+
parameters[:op] = "IS NULL"
|
696
|
+
elsif subk == "!="
|
697
|
+
parameters[:op] = "IS NOT NULL"
|
698
|
+
else
|
699
|
+
parameters[:op] = normalised_k
|
700
|
+
end
|
701
|
+
elsif subv.is_a?(Range) or subv.is_a?(Array)
|
702
|
+
if subk == "="
|
703
|
+
parameters[:op] = "ONE OF"
|
704
|
+
elsif subk == "!="
|
705
|
+
parameters[:op] = "NONE OF"
|
706
|
+
else
|
707
|
+
parameters[:op] = normalised_k
|
708
|
+
end
|
709
|
+
parameters[:values] = subv.to_a
|
710
|
+
elsif subv.is_a?(Lists::List)
|
711
|
+
if subk == "="
|
712
|
+
parameters[:op] = "IN"
|
713
|
+
elsif subk == "!="
|
714
|
+
parameters[:op] = "NOT IN"
|
715
|
+
else
|
716
|
+
parameters[:op] = normalised_k
|
717
|
+
end
|
718
|
+
parameters[:value] = subv.name
|
719
|
+
else
|
720
|
+
parameters[:op] = normalised_k
|
721
|
+
parameters[:value] = subv
|
722
|
+
end
|
723
|
+
end
|
724
|
+
end
|
725
|
+
add_constraint(parameters)
|
726
|
+
elsif v.is_a?(Range) or v.is_a?(Array)
|
727
|
+
add_constraint(k.to_s, 'ONE OF', v.to_a)
|
728
|
+
elsif v.is_a?(InterMine::Metadata::ClassDescriptor)
|
729
|
+
add_constraint(:path => k.to_s, :sub_class => v.name)
|
730
|
+
elsif v.is_a?(InterMine::Lists::List)
|
731
|
+
add_constraint(k.to_s, 'IN', v.name)
|
732
|
+
elsif v.nil?
|
733
|
+
add_constraint(k.to_s, "IS NULL")
|
734
|
+
else
|
735
|
+
if path(k.to_s).is_attribute?
|
736
|
+
add_constraint(k.to_s, '=', v)
|
737
|
+
else
|
738
|
+
add_constraint(k.to_s, 'LOOKUP', v)
|
739
|
+
end
|
740
|
+
end
|
741
|
+
end
|
742
|
+
end
|
743
|
+
return self
|
744
|
+
end
|
745
|
+
|
746
|
+
# Set the logic to the given value.
|
747
|
+
#
|
748
|
+
# The value will be parsed for consistency is it is a logic
|
749
|
+
# string.
|
750
|
+
#
|
751
|
+
# Returns self to support chaining.
|
752
|
+
def set_logic(value)
|
753
|
+
if value.is_a?(LogicGroup)
|
754
|
+
@logic = value
|
755
|
+
else
|
756
|
+
@logic = @logic_parser.parse_logic(value)
|
757
|
+
end
|
758
|
+
return self
|
759
|
+
end
|
760
|
+
|
761
|
+
alias constraintLogic= set_logic
|
762
|
+
|
763
|
+
# Get the next available code for the query.
|
764
|
+
def next_code
|
765
|
+
c = LOWEST_CODE
|
766
|
+
while Query.is_valid_code(c)
|
767
|
+
return c unless used_codes.include?(c)
|
768
|
+
c = c.next
|
769
|
+
end
|
770
|
+
raise RuntimeError, "Maximum number of codes reached - all 26 have been allocated"
|
771
|
+
end
|
772
|
+
|
773
|
+
# Return the list of currently used codes by the query.
|
774
|
+
def used_codes
|
775
|
+
if @constraints.empty?
|
776
|
+
return []
|
777
|
+
else
|
778
|
+
return @constraints.select {|x| !x.is_a?(SubClassConstraint)}.map {|x| x.code}
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
# Whether or not the argument is a valid constraint code.
|
783
|
+
#
|
784
|
+
# to be valid, it must be a one character string between A and Z inclusive.
|
785
|
+
def self.is_valid_code(str)
|
786
|
+
return (str.length == 1) && (str >= LOWEST_CODE) && (str <= HIGHEST_CODE)
|
787
|
+
end
|
788
|
+
|
789
|
+
# Adds the root prefix to the given string.
|
790
|
+
#
|
791
|
+
# Arguments:
|
792
|
+
# [+x+] An object with a #to_s method
|
793
|
+
#
|
794
|
+
# Returns the prefixed string.
|
795
|
+
def add_prefix(x)
|
796
|
+
x = x.to_s
|
797
|
+
if @root && !x.start_with?(@root.name)
|
798
|
+
return @root.name + "." + x
|
799
|
+
else
|
800
|
+
return x
|
801
|
+
end
|
802
|
+
end
|
803
|
+
|
804
|
+
# Return the parameter hash for running this query in its current state.
|
805
|
+
def params
|
806
|
+
hash = {"query" => self.to_xml}
|
807
|
+
if @service and @service.token
|
808
|
+
hash["token"] = @service.token
|
809
|
+
end
|
810
|
+
return hash
|
811
|
+
end
|
812
|
+
|
813
|
+
# Return the textual representation of the query. Here it returns the Query XML
|
814
|
+
def to_s
|
815
|
+
return to_xml.to_s
|
816
|
+
end
|
817
|
+
|
818
|
+
# Return an informative textual representation of the query.
|
819
|
+
def inspect
|
820
|
+
return "<#{self.class.name} query=#{self.to_s.inspect}>"
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
|
825
|
+
class ConstraintFactory
|
826
|
+
|
827
|
+
def initialize(query)
|
828
|
+
@classes = [
|
829
|
+
SingleValueConstraint,
|
830
|
+
SubClassConstraint,
|
831
|
+
LookupConstraint, MultiValueConstraint,
|
832
|
+
UnaryConstraint, LoopConstraint, ListConstraint]
|
833
|
+
|
834
|
+
@query = query
|
835
|
+
end
|
836
|
+
|
837
|
+
def make_constraint(args)
|
838
|
+
case args.length
|
839
|
+
when 2
|
840
|
+
parameters = {:path => args[0], :op => args[1]}
|
841
|
+
when 3
|
842
|
+
if args[2].is_a?(Array)
|
843
|
+
parameters = {:path => args[0], :op => args[1], :values => args[2]}
|
844
|
+
elsif LoopConstraint.valid_ops.include?(args[1])
|
845
|
+
parameters = {:path => args[0], :op => args[1], :loopPath => args[2]}
|
846
|
+
else
|
847
|
+
parameters = {:path => args[0], :op => args[1], :value => args[2]}
|
848
|
+
end
|
849
|
+
when 4
|
850
|
+
parameters = {:path => args[0], :op => args[1], :value => args[2], :extra_value => args[3]}
|
851
|
+
else
|
852
|
+
parameters = args.first
|
853
|
+
end
|
854
|
+
|
855
|
+
attr_keys = parameters.keys
|
856
|
+
suitable_classes = @classes.select { |cls|
|
857
|
+
is_suitable = true
|
858
|
+
attr_keys.each { |key|
|
859
|
+
is_suitable = is_suitable && (cls.method_defined?(key))
|
860
|
+
if key.to_s == "op"
|
861
|
+
is_suitable = is_suitable && cls.valid_ops.include?(parameters[key])
|
862
|
+
end
|
863
|
+
}
|
864
|
+
is_suitable
|
865
|
+
}
|
866
|
+
if suitable_classes.size > 1
|
867
|
+
raise ArgumentError, "More than one class found for #{parameters.inspect}"
|
868
|
+
elsif suitable_classes.size < 1
|
869
|
+
raise ArgumentError, "No suitable classes found for #{parameters.inspect}"
|
870
|
+
end
|
871
|
+
|
872
|
+
cls = suitable_classes.first
|
873
|
+
con = cls.new
|
874
|
+
parameters.each_pair { |key, value|
|
875
|
+
if key == :path || key == :loopPath
|
876
|
+
value = @query.path(value)
|
877
|
+
end
|
878
|
+
if key == :sub_class
|
879
|
+
value = InterMine::Metadata::Path.new(value, @query.model)
|
880
|
+
end
|
881
|
+
con.send(key.to_s + '=', value)
|
882
|
+
}
|
883
|
+
con.validate
|
884
|
+
if con.respond_to?(:code)
|
885
|
+
code = con.code
|
886
|
+
if code.nil?
|
887
|
+
con.code = @query.next_code
|
888
|
+
else
|
889
|
+
code = code.to_s
|
890
|
+
unless Query.is_valid_code(code)
|
891
|
+
raise ArgumentError, "Coded must be between A and Z, got: #{code}"
|
892
|
+
end
|
893
|
+
if @query.used_codes.include?(code)
|
894
|
+
con.code = @query.next_code
|
895
|
+
end
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
return con
|
900
|
+
end
|
901
|
+
|
902
|
+
|
903
|
+
end
|
904
|
+
|
905
|
+
class TemplateConstraintFactory < ConstraintFactory
|
906
|
+
|
907
|
+
def initialize(query)
|
908
|
+
super
|
909
|
+
@classes = [
|
910
|
+
TemplateSingleValueConstraint,
|
911
|
+
SubClassConstraint,
|
912
|
+
TemplateLookupConstraint, TemplateMultiValueConstraint,
|
913
|
+
TemplateUnaryConstraint, TemplateLoopConstraint, TemplateListConstraint]
|
914
|
+
end
|
915
|
+
|
916
|
+
end
|
917
|
+
|
918
|
+
module PathFeature
|
919
|
+
attr_accessor :path
|
920
|
+
|
921
|
+
def validate
|
922
|
+
end
|
923
|
+
end
|
924
|
+
|
925
|
+
module TemplateConstraint
|
926
|
+
|
927
|
+
attr_accessor :editable, :switchable
|
928
|
+
|
929
|
+
def to_elem
|
930
|
+
attributes = {"editable" => @editable, "switchable" => @switchable}
|
931
|
+
elem = super
|
932
|
+
elem.add_attributes(attributes)
|
933
|
+
return elem
|
934
|
+
end
|
935
|
+
|
936
|
+
def template_param_op
|
937
|
+
return @op
|
938
|
+
end
|
939
|
+
|
940
|
+
end
|
941
|
+
|
942
|
+
module Coded
|
943
|
+
attr_accessor :code, :op
|
944
|
+
|
945
|
+
def self.valid_ops
|
946
|
+
return []
|
947
|
+
end
|
948
|
+
|
949
|
+
def to_elem
|
950
|
+
attributes = {
|
951
|
+
"path" => @path,
|
952
|
+
"op" => @op,
|
953
|
+
"code" => @code
|
954
|
+
}.delete_if {|k,v| !v}
|
955
|
+
elem = REXML::Element.new("constraint")
|
956
|
+
elem.add_attributes(attributes)
|
957
|
+
return elem
|
958
|
+
end
|
959
|
+
end
|
960
|
+
|
961
|
+
class SubClassConstraint
|
962
|
+
include PathFeature
|
963
|
+
attr_accessor :sub_class
|
964
|
+
|
965
|
+
def to_elem
|
966
|
+
attributes = {
|
967
|
+
"path" => @path,
|
968
|
+
"type" => @sub_class
|
969
|
+
}
|
970
|
+
elem = REXML::Element.new("constraint")
|
971
|
+
elem.add_attributes(attributes)
|
972
|
+
return elem
|
973
|
+
end
|
974
|
+
|
975
|
+
def validate
|
976
|
+
if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
|
977
|
+
raise ArgumentError, "#{self.class.name}s must be on objects or references to objects"
|
978
|
+
end
|
979
|
+
if @sub_class.length > 1
|
980
|
+
raise ArgumentError, "#{self.class.name} expects sub-classes to be named as bare class names"
|
981
|
+
end
|
982
|
+
model = @path.model
|
983
|
+
cdA = model.get_cd(@path.end_type)
|
984
|
+
cdB = model.get_cd(@sub_class.end_type)
|
985
|
+
unless ((cdB == cdA) or cdB.subclass_of?(cdA))
|
986
|
+
raise ArgumentError, "The subclass in a #{self.class.name} must be a subclass of its path, but #{cdB} is not a subclass of #{cdA}"
|
987
|
+
end
|
988
|
+
|
989
|
+
end
|
990
|
+
|
991
|
+
end
|
992
|
+
|
993
|
+
module ObjectConstraint
|
994
|
+
def validate
|
995
|
+
if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
|
996
|
+
raise ArgumentError, "#{self.class.name}s must be on objects or references to objects, got #{@path}"
|
997
|
+
end
|
998
|
+
end
|
999
|
+
end
|
1000
|
+
|
1001
|
+
module AttributeConstraint
|
1002
|
+
def validate
|
1003
|
+
if !@path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
|
1004
|
+
raise ArgumentError, "Attribute constraints must be on attributes, got #{@path}"
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def coerce_value(val)
|
1009
|
+
nums = ["Float", "Double", "float", "double"]
|
1010
|
+
ints = ["Integer", "int"]
|
1011
|
+
bools = ["Boolean", "boolean"]
|
1012
|
+
dataType = @path.elements.last.dataType.split(".").last
|
1013
|
+
coerced = val
|
1014
|
+
if nums.include?(dataType)
|
1015
|
+
if !val.is_a?(Numeric)
|
1016
|
+
coerced = val.to_f
|
1017
|
+
end
|
1018
|
+
end
|
1019
|
+
if ints.include?(dataType)
|
1020
|
+
coerced = val.to_i
|
1021
|
+
end
|
1022
|
+
if bools.include?(dataType)
|
1023
|
+
if !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
|
1024
|
+
if val == 0 or val == "0" or val.downcase == "yes" or val.downcase == "true" or val.downcase == "t"
|
1025
|
+
coerced = true
|
1026
|
+
elsif val == 1 or val == "1" or val.downcase == "no" or val.downcase == "false" or val.downcase == "f"
|
1027
|
+
coerced = false
|
1028
|
+
end
|
1029
|
+
end
|
1030
|
+
end
|
1031
|
+
if coerced == 0 and not val.to_s.start_with?("0")
|
1032
|
+
raise ArgumentError, "cannot coerce #{val} to a #{dataType}"
|
1033
|
+
end
|
1034
|
+
return coerced
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
def validate_value(val)
|
1038
|
+
nums = ["Float", "Double", "float", "double"]
|
1039
|
+
ints = ["Integer", "int"]
|
1040
|
+
bools = ["Boolean", "boolean"]
|
1041
|
+
dataType = @path.elements.last.dataType.split(".").last
|
1042
|
+
if nums.include?(dataType)
|
1043
|
+
if !val.is_a?(Numeric)
|
1044
|
+
raise ArgumentError, "value #{val} is not numeric for #{@path}"
|
1045
|
+
end
|
1046
|
+
end
|
1047
|
+
if ints.include?(dataType)
|
1048
|
+
val = val.to_i
|
1049
|
+
if !val.is_a?(Integer)
|
1050
|
+
raise ArgumentError, "value #{val} is not an integer for #{@path}"
|
1051
|
+
end
|
1052
|
+
end
|
1053
|
+
if bools.include?(dataType)
|
1054
|
+
if !val.is_a?(TrueClass) && !val.is_a?(FalseClass)
|
1055
|
+
raise ArgumentError, "value #{val} is not a boolean value for #{@path}"
|
1056
|
+
end
|
1057
|
+
end
|
1058
|
+
end
|
1059
|
+
end
|
1060
|
+
|
1061
|
+
class SingleValueConstraint
|
1062
|
+
include PathFeature
|
1063
|
+
include Coded
|
1064
|
+
include AttributeConstraint
|
1065
|
+
attr_accessor :value
|
1066
|
+
|
1067
|
+
CANONICAL_OPS = {
|
1068
|
+
"EQ" => "=",
|
1069
|
+
"==" => "=",
|
1070
|
+
"NE" => "!=",
|
1071
|
+
"LT" => "<",
|
1072
|
+
"GT" => ">",
|
1073
|
+
"LE" => "<=",
|
1074
|
+
"GE" => ">="
|
1075
|
+
}
|
1076
|
+
|
1077
|
+
def self.valid_ops
|
1078
|
+
return %w{= == > < >= <= != CONTAINS LIKE EQ NE GT LT LE GE}
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
def to_elem
|
1082
|
+
elem = super
|
1083
|
+
attributes = {"value" => @value}
|
1084
|
+
elem.add_attributes(attributes)
|
1085
|
+
return elem
|
1086
|
+
end
|
1087
|
+
|
1088
|
+
def validate
|
1089
|
+
super
|
1090
|
+
@op = SingleValueConstraint::CANONICAL_OPS[@op] || @op
|
1091
|
+
@value = coerce_value(@value)
|
1092
|
+
validate_value(@value)
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
class TemplateSingleValueConstraint < SingleValueConstraint
|
1098
|
+
include TemplateConstraint
|
1099
|
+
|
1100
|
+
def template_param_op
|
1101
|
+
case @op
|
1102
|
+
when '='
|
1103
|
+
return 'eq'
|
1104
|
+
when '!='
|
1105
|
+
return 'ne'
|
1106
|
+
when '<'
|
1107
|
+
return 'lt'
|
1108
|
+
when '<='
|
1109
|
+
return 'le'
|
1110
|
+
when '>'
|
1111
|
+
return 'gt'
|
1112
|
+
when '>='
|
1113
|
+
return 'ge'
|
1114
|
+
else
|
1115
|
+
return @op
|
1116
|
+
end
|
1117
|
+
end
|
1118
|
+
end
|
1119
|
+
|
1120
|
+
class ListConstraint < SingleValueConstraint
|
1121
|
+
include ObjectConstraint
|
1122
|
+
|
1123
|
+
def self.valid_ops
|
1124
|
+
return ["IN", "NOT IN"]
|
1125
|
+
end
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
class TemplateListConstraint < ListConstraint
|
1129
|
+
include TemplateConstraint
|
1130
|
+
end
|
1131
|
+
|
1132
|
+
class LoopConstraint
|
1133
|
+
include PathFeature
|
1134
|
+
include Coded
|
1135
|
+
attr_accessor :loopPath
|
1136
|
+
|
1137
|
+
def self.valid_ops
|
1138
|
+
return ["IS", "IS NOT"]
|
1139
|
+
end
|
1140
|
+
|
1141
|
+
def self.xml_ops
|
1142
|
+
return { "IS" => "=", "IS NOT" => "!=" }
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def to_elem
|
1146
|
+
elem = super
|
1147
|
+
elem.add_attribute("op", LoopConstraint.xml_ops[@op])
|
1148
|
+
elem.add_attribute("loopPath", @loopPath)
|
1149
|
+
return elem
|
1150
|
+
end
|
1151
|
+
|
1152
|
+
def validate
|
1153
|
+
if @path.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
|
1154
|
+
raise ArgumentError, "#{self.class.name}s must be on objects or references to objects"
|
1155
|
+
end
|
1156
|
+
if @loopPath.elements.last.is_a?(InterMine::Metadata::AttributeDescriptor)
|
1157
|
+
raise ArgumentError, "loopPaths on #{self.class.name}s must be on objects or references to objects"
|
1158
|
+
end
|
1159
|
+
model = @path.model
|
1160
|
+
cdA = model.get_cd(@path.end_type)
|
1161
|
+
cdB = model.get_cd(@loopPath.end_type)
|
1162
|
+
if !(cdA == cdB) && !cdA.subclass_of?(cdB) && !cdB.subclass_of?(cdA)
|
1163
|
+
raise ArgumentError, "Incompatible types in #{self.class.name}: #{@path} -> #{cdA} and #{@loopPath} -> #{cdB}"
|
1164
|
+
end
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
class TemplateLoopConstraint < LoopConstraint
|
1170
|
+
include TemplateConstraint
|
1171
|
+
def template_param_op
|
1172
|
+
case @op
|
1173
|
+
when 'IS'
|
1174
|
+
return 'eq'
|
1175
|
+
when 'IS NOT'
|
1176
|
+
return 'ne'
|
1177
|
+
end
|
1178
|
+
end
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
class UnaryConstraint
|
1182
|
+
include PathFeature
|
1183
|
+
include Coded
|
1184
|
+
|
1185
|
+
def self.valid_ops
|
1186
|
+
return ["IS NULL", "IS NOT NULL"]
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
end
|
1190
|
+
|
1191
|
+
class TemplateUnaryConstraint < UnaryConstraint
|
1192
|
+
include TemplateConstraint
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
class LookupConstraint < ListConstraint
|
1196
|
+
attr_accessor :extra_value
|
1197
|
+
|
1198
|
+
def self.valid_ops
|
1199
|
+
return ["LOOKUP"]
|
1200
|
+
end
|
1201
|
+
|
1202
|
+
def to_elem
|
1203
|
+
elem = super
|
1204
|
+
if @extra_value
|
1205
|
+
elem.add_attribute("extraValue", @extra_value)
|
1206
|
+
end
|
1207
|
+
return elem
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
class TemplateLookupConstraint < LookupConstraint
|
1213
|
+
include TemplateConstraint
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
class MultiValueConstraint
|
1217
|
+
include PathFeature
|
1218
|
+
include Coded
|
1219
|
+
include AttributeConstraint
|
1220
|
+
|
1221
|
+
def self.valid_ops
|
1222
|
+
return ["ONE OF", "NONE OF"]
|
1223
|
+
end
|
1224
|
+
|
1225
|
+
attr_accessor :values
|
1226
|
+
def to_elem
|
1227
|
+
elem = super
|
1228
|
+
@values.each { |x|
|
1229
|
+
value = REXML::Element.new("value")
|
1230
|
+
value.add_text(x.to_s)
|
1231
|
+
elem.add_element(value)
|
1232
|
+
}
|
1233
|
+
return elem
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
def validate
|
1237
|
+
super
|
1238
|
+
@values.map! {|val| coerce_value(val)}
|
1239
|
+
@values.each do |val|
|
1240
|
+
validate_value(val)
|
1241
|
+
end
|
1242
|
+
end
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
class TemplateMultiValueConstraint < MultiValueConstraint
|
1246
|
+
include TemplateConstraint
|
1247
|
+
end
|
1248
|
+
|
1249
|
+
class SortOrder
|
1250
|
+
include PathFeature
|
1251
|
+
attr_accessor :direction
|
1252
|
+
class << self; attr_accessor :valid_directions end
|
1253
|
+
@valid_directions = %w{ASC DESC}
|
1254
|
+
|
1255
|
+
def initialize(path, direction)
|
1256
|
+
direction = direction.to_s.upcase
|
1257
|
+
unless SortOrder.valid_directions.include? direction
|
1258
|
+
raise ArgumentError, "Illegal sort direction: #{direction}"
|
1259
|
+
end
|
1260
|
+
self.path = path
|
1261
|
+
self.direction = direction
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
def to_s
|
1265
|
+
return @path.to_s + " " + @direction
|
1266
|
+
end
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
class Join
|
1270
|
+
include PathFeature
|
1271
|
+
attr_accessor :style
|
1272
|
+
class << self; attr_accessor :valid_styles end
|
1273
|
+
@valid_styles = %{INNER OUTER}
|
1274
|
+
|
1275
|
+
def initialize(path, style)
|
1276
|
+
unless Join.valid_styles.include?(style)
|
1277
|
+
raise ArgumentError, "Invalid style: #{style}"
|
1278
|
+
end
|
1279
|
+
self.path = path
|
1280
|
+
self.style = style
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
def attrs
|
1284
|
+
attributes = {
|
1285
|
+
"path" => @path,
|
1286
|
+
"style" => @style
|
1287
|
+
}
|
1288
|
+
return attributes
|
1289
|
+
end
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
class LogicNode
|
1293
|
+
end
|
1294
|
+
|
1295
|
+
class LogicGroup < LogicNode
|
1296
|
+
|
1297
|
+
attr_reader :left, :right, :op
|
1298
|
+
attr_accessor :parent
|
1299
|
+
|
1300
|
+
def initialize(left, op, right, parent=nil)
|
1301
|
+
if !["AND", "OR"].include?(op)
|
1302
|
+
raise ArgumentError, "#{op} is not a legal logical operator"
|
1303
|
+
end
|
1304
|
+
@parent = parent
|
1305
|
+
@left = left
|
1306
|
+
@op = op
|
1307
|
+
@right = right
|
1308
|
+
[left, right].each do |node|
|
1309
|
+
if node.is_a?(LogicGroup)
|
1310
|
+
node.parent = self
|
1311
|
+
end
|
1312
|
+
end
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
def to_s
|
1316
|
+
core = [@left.code, @op.downcase, @right.code].join(" ")
|
1317
|
+
if @parent && @op != @parent.op
|
1318
|
+
return "(#{core})"
|
1319
|
+
else
|
1320
|
+
return core
|
1321
|
+
end
|
1322
|
+
end
|
1323
|
+
|
1324
|
+
def code
|
1325
|
+
return to_s
|
1326
|
+
end
|
1327
|
+
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
class LogicParseError < ArgumentError
|
1331
|
+
end
|
1332
|
+
|
1333
|
+
class LogicParser
|
1334
|
+
|
1335
|
+
class << self; attr_accessor :precedence, :ops end
|
1336
|
+
@precedence = {
|
1337
|
+
"AND" => 2,
|
1338
|
+
"OR" => 1,
|
1339
|
+
"(" => 3,
|
1340
|
+
")" => 3
|
1341
|
+
}
|
1342
|
+
|
1343
|
+
@ops = {
|
1344
|
+
"AND" => "AND",
|
1345
|
+
"&" => "AND",
|
1346
|
+
"&&" => "AND",
|
1347
|
+
"OR" => "OR",
|
1348
|
+
"|" => "OR",
|
1349
|
+
"||" => "OR",
|
1350
|
+
"(" => "(",
|
1351
|
+
")" => ")"
|
1352
|
+
}
|
1353
|
+
|
1354
|
+
def initialize(query)
|
1355
|
+
@query = query
|
1356
|
+
end
|
1357
|
+
|
1358
|
+
def parse_logic(str)
|
1359
|
+
tokens = str.upcase.split(/(?:\s+|\b)/).map do |x|
|
1360
|
+
LogicParser.ops.fetch(x, x.split(//))
|
1361
|
+
end
|
1362
|
+
tokens.flatten!
|
1363
|
+
|
1364
|
+
check_syntax(tokens)
|
1365
|
+
postfix_tokens = infix_to_postfix(tokens)
|
1366
|
+
ast = postfix_to_tree(postfix_tokens)
|
1367
|
+
return ast
|
1368
|
+
end
|
1369
|
+
|
1370
|
+
private
|
1371
|
+
|
1372
|
+
def infix_to_postfix(tokens)
|
1373
|
+
stack = []
|
1374
|
+
postfix_tokens = []
|
1375
|
+
tokens.each do |x|
|
1376
|
+
if !LogicParser.ops.include?(x)
|
1377
|
+
postfix_tokens << x
|
1378
|
+
else
|
1379
|
+
case x
|
1380
|
+
when "("
|
1381
|
+
stack << x
|
1382
|
+
when ")"
|
1383
|
+
while !stack.empty?
|
1384
|
+
last_op = stack.pop
|
1385
|
+
if last_op == "("
|
1386
|
+
if !stack.empty?
|
1387
|
+
previous_op = stack.pop
|
1388
|
+
if previous_op != "("
|
1389
|
+
postfix_tokens << previous_op
|
1390
|
+
break
|
1391
|
+
end
|
1392
|
+
end
|
1393
|
+
else
|
1394
|
+
postfix_tokens << last_op
|
1395
|
+
end
|
1396
|
+
end
|
1397
|
+
else
|
1398
|
+
while !stack.empty? and LogicParser.precedence[stack.last] <= LogicParser.precedence[x]
|
1399
|
+
prev_op = stack.pop
|
1400
|
+
if prev_op != "("
|
1401
|
+
postfix_tokens << prev_op
|
1402
|
+
end
|
1403
|
+
end
|
1404
|
+
stack << x
|
1405
|
+
end
|
1406
|
+
end
|
1407
|
+
end
|
1408
|
+
while !stack.empty?
|
1409
|
+
postfix_tokens << stack.pop
|
1410
|
+
end
|
1411
|
+
return postfix_tokens
|
1412
|
+
end
|
1413
|
+
|
1414
|
+
def check_syntax(tokens)
|
1415
|
+
need_op = false
|
1416
|
+
need_bin_op_or_bracket = false
|
1417
|
+
processed = []
|
1418
|
+
open_brackets = 0
|
1419
|
+
tokens.each do |x|
|
1420
|
+
if !LogicParser.ops.include?(x)
|
1421
|
+
if need_op
|
1422
|
+
raise LogicParseError, "Expected an operator after '#{processed.join(' ')}', but got #{x}"
|
1423
|
+
elsif need_bin_op_or_bracket
|
1424
|
+
raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', expected an operator or closing bracket, but got #{x}"
|
1425
|
+
end
|
1426
|
+
need_op = true
|
1427
|
+
else
|
1428
|
+
need_op = false
|
1429
|
+
case x
|
1430
|
+
when "("
|
1431
|
+
if !processed.empty? && !LogicParser.ops.include?(processed.last)
|
1432
|
+
raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', got #{x}"
|
1433
|
+
elsif need_bin_op_or_bracket
|
1434
|
+
raise LogicParseError, "Logic grouping error after '#{processed.join(' ')}', got #{x}"
|
1435
|
+
end
|
1436
|
+
open_brackets += 1
|
1437
|
+
when ")"
|
1438
|
+
need_bin_op_or_bracket = true
|
1439
|
+
open_brackets -= 1
|
1440
|
+
else
|
1441
|
+
need_bin_op_or_bracket = false
|
1442
|
+
end
|
1443
|
+
end
|
1444
|
+
processed << x
|
1445
|
+
end
|
1446
|
+
if open_brackets < 0
|
1447
|
+
raise LogicParseError, "Unmatched closing bracket in #{tokens.join(' ')}"
|
1448
|
+
elsif open_brackets > 0
|
1449
|
+
raise LogicParseError, "Unmatched opening bracket in #{tokens.join(' ')}"
|
1450
|
+
end
|
1451
|
+
end
|
1452
|
+
|
1453
|
+
def postfix_to_tree(tokens)
|
1454
|
+
stack = []
|
1455
|
+
tokens.each do |x|
|
1456
|
+
if !LogicParser.ops.include?(x)
|
1457
|
+
stack << x
|
1458
|
+
else
|
1459
|
+
right = stack.pop
|
1460
|
+
left = stack.pop
|
1461
|
+
right = (right.is_a?(LogicGroup)) ? right : @query.get_constraint(right)
|
1462
|
+
left = (left.is_a?(LogicGroup)) ? left : @query.get_constraint(left)
|
1463
|
+
stack << LogicGroup.new(left, x, right)
|
1464
|
+
end
|
1465
|
+
end
|
1466
|
+
if stack.size != 1
|
1467
|
+
raise LogicParseError, "Tree does not have a unique root"
|
1468
|
+
end
|
1469
|
+
return stack.pop
|
1470
|
+
end
|
1471
|
+
|
1472
|
+
def precedence_of(op)
|
1473
|
+
return LogicParser.precedence[op]
|
1474
|
+
end
|
1475
|
+
|
1476
|
+
end
|
1477
|
+
|
1478
|
+
class Template < Query
|
1479
|
+
|
1480
|
+
attr_accessor :longDescription, :comment
|
1481
|
+
|
1482
|
+
def initialize(model, root=nil, service=nil)
|
1483
|
+
super
|
1484
|
+
@constraint_factory = TemplateConstraintFactory.new(self)
|
1485
|
+
@url = (@service.nil?) ? nil : @service.root + Service::TEMPLATE_RESULTS_PATH
|
1486
|
+
end
|
1487
|
+
|
1488
|
+
def self.parser(model)
|
1489
|
+
return TemplateLoader.new(model)
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
def to_xml
|
1493
|
+
doc = REXML::Document.new
|
1494
|
+
t = doc.add_element 'template', {"name" => @name, "title" => @title, "longDescription" => @longDescription, "comment" => @comment}.reject {|k,v| v.nil?}
|
1495
|
+
t.add_element super
|
1496
|
+
return t
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
def editable_constraints
|
1500
|
+
return coded_constraints.select {|con| con.editable}
|
1501
|
+
end
|
1502
|
+
|
1503
|
+
def active_constraints
|
1504
|
+
return coded_constraints.select {|con| con.switchable != "off"}
|
1505
|
+
end
|
1506
|
+
|
1507
|
+
def params
|
1508
|
+
p = {"name" => @name}
|
1509
|
+
actives = active_constraints
|
1510
|
+
actives.each_index do |idx|
|
1511
|
+
con = actives[idx]
|
1512
|
+
count = (idx + 1).to_s
|
1513
|
+
p["constraint" + count] = con.path.to_s
|
1514
|
+
p["op" + count] = con.template_param_op
|
1515
|
+
if con.respond_to? :value
|
1516
|
+
p["value" + count] = con.value
|
1517
|
+
elsif con.respond_to? :values
|
1518
|
+
p["value" + count] = con.values
|
1519
|
+
elsif con.respond_to? :loopPath
|
1520
|
+
p["loopPath" + count] = con.loopPath.to_s
|
1521
|
+
end
|
1522
|
+
if con.respond_to? :extra_value and !con.extra_value.nil?
|
1523
|
+
p["extra" + count] = con.extra_value
|
1524
|
+
end
|
1525
|
+
end
|
1526
|
+
return p
|
1527
|
+
end
|
1528
|
+
|
1529
|
+
def each_row(params = {}, start=0, size=nil)
|
1530
|
+
runner = (params.empty?) ? self : get_adjusted(params)
|
1531
|
+
runner.results_reader(start, size).each_row {|r| yield r}
|
1532
|
+
end
|
1533
|
+
|
1534
|
+
def each_result(params = {}, start=0, size=nil)
|
1535
|
+
runner = (params.empty?) ? self : get_adjusted(params)
|
1536
|
+
runner.results_reader(start, size).each_result {|r| yield r}
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
def count(params = {})
|
1540
|
+
runner = (params.empty?) ? self : get_adjusted(params)
|
1541
|
+
runner.results_reader.get_size
|
1542
|
+
end
|
1543
|
+
|
1544
|
+
def clone
|
1545
|
+
other = super
|
1546
|
+
other.instance_variable_set(:@constraints, @constraints.map {|c| c.clone})
|
1547
|
+
return other
|
1548
|
+
end
|
1549
|
+
|
1550
|
+
private
|
1551
|
+
|
1552
|
+
def get_adjusted(params)
|
1553
|
+
adjusted = clone
|
1554
|
+
params.each do |k,v|
|
1555
|
+
con = adjusted.get_constraint(k)
|
1556
|
+
raise ArgumentError, "There is no constraint with code #{k} in this query" unless con
|
1557
|
+
path = con.path.to_s
|
1558
|
+
adjusted.remove_constraint(k)
|
1559
|
+
adjusted.where(path => v)
|
1560
|
+
adjusted.constraints.last.code = k
|
1561
|
+
end
|
1562
|
+
return adjusted
|
1563
|
+
end
|
1564
|
+
end
|
1565
|
+
|
1566
|
+
class TemplateConstraintFactory < ConstraintFactory
|
1567
|
+
end
|
1568
|
+
|
1569
|
+
end
|