Merge lp:~rockwalrus/activerdf/devel into lp:activerdf

Proposed by Rockwalrus
Status: Needs review
Proposed branch: lp:~rockwalrus/activerdf/devel
Merge into: lp:activerdf
Diff against target: None lines
To merge this branch: bzr merge lp:~rockwalrus/activerdf/devel
Reviewer Review Type Date Requested Status
ActiveRDF Pending
Review via email: mp+6713@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Rockwalrus (rockwalrus) wrote :

Support for UNION queries.

Unmerged revisions

466. By Rockwalrus

Support for UNION queries.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'activerdf-rdflite/lib/activerdf_rdflite/rdflite.rb'
2--- activerdf-rdflite/lib/activerdf_rdflite/rdflite.rb 2008-06-11 12:11:39 +0000
3+++ activerdf-rdflite/lib/activerdf_rdflite/rdflite.rb 2009-05-20 03:36:52 +0000
4@@ -27,7 +27,7 @@
5 class RDFLite < ActiveRdfAdapter
6 ConnectionPool.register_adapter(:rdflite,self)
7 bool_accessor :keyword_search, :reasoning
8-
9+
10 # instantiates RDFLite database
11 # available parameters:
12 # * :location => filepath (defaults to memory)
13@@ -235,7 +235,20 @@
14 # translates ActiveRDF query into internal sqlite query string
15 def translate(query)
16 where, conditions = construct_where(query)
17- [construct_select(query) + construct_join(query) + where + construct_sort(query) + construct_limit(query), conditions]
18+
19+ statement = construct_select(query) + construct_join(query.where_clauses) + where
20+
21+ query.union_groups.each do |union|
22+ where, new_conditions = construct_union_where(union, query.reasoning?)
23+
24+ conditions += new_conditions
25+
26+ statement << "union " + construct_select(query, union) + construct_join(union.clauses) + where
27+ end
28+
29+
30+ statement << construct_sort(query) + construct_limit(query)
31+ [statement, conditions]
32 end
33
34 private
35@@ -243,14 +256,18 @@
36 SPOC = ['s','p','o','c']
37
38 # construct select clause
39- def construct_select(query)
40+ def construct_select(query, union = nil)
41 # ASK queries counts the results, and return true if results > 0
42 return "select count(*)" if query.ask?
43
44 # add select terms for each selectclause in the query
45 # the term names depend on the join conditions, e.g. t0.s or t1.p
46 select = query.select_clauses.collect do |term|
47- variable_name(query, term)
48+ if union.nil?
49+ variable_name(query, term)
50+ else
51+ union_variable_name(union, term)
52+ end
53 end
54
55 # add possible distinct and count functions to select clause
56@@ -274,7 +291,7 @@
57
58 clause << " limit #{limit} offset #{offset}"
59 clause
60- end
61+ end
62
63 # sort query results on variable clause (optionally)
64 def construct_sort(query)
65@@ -294,13 +311,13 @@
66 # only, and we should only alias tables we didnt alias yet)
67 # we should only look for one join clause in each where-clause: when we find
68 # one, we skip the rest of the variables in this clause.
69- def construct_join(query)
70+ def construct_join(clauses)
71 join_stmt = ''
72
73 # no join necessary if only one where clause given
74- return ' from triple as t0 ' if query.where_clauses.size == 1
75+ return ' from triple as t0 ' if clauses.size == 1
76
77- where_clauses = query.where_clauses.flatten
78+ where_clauses = clauses.flatten
79 considering = where_clauses.uniq.select{|w| w.is_a?(Symbol)}
80
81 # constructing hash with indices for all terms
82@@ -370,32 +387,7 @@
83
84 # construct where clause
85 def construct_where(query)
86- # collecting where clauses, these will be added to the sql string later
87- where = []
88-
89- # collecting all the right-hand sides of where clauses (e.g. where name =
90- # 'abc'), to add to query string later using ?-notation, because then
91- # sqlite will automatically encode quoted literals correctly
92- right_hand_sides = []
93-
94- # convert each where clause to SQL:
95- # add where clause for each subclause, except if it's a variable
96- query.where_clauses.each_with_index do |clause,level|
97- raise ActiveRdfError, "where clause #{clause} is not a triple" unless clause.is_a?(Array)
98- clause.each_with_index do |subclause, i|
99- # dont add where clause for variables
100- unless subclause.is_a?(Symbol) || subclause.nil?
101- conditions = compute_where_condition(i, subclause, query.reasoning? && reasoning?)
102- if conditions.size == 1
103- where << "t#{level}.#{SPOC[i]} = ?"
104- right_hand_sides << conditions.first
105- else
106- conditions = conditions.collect {|c| "'#{c}'"}
107- where << "t#{level}.#{SPOC[i]} in (#{conditions.join(',')})"
108- end
109- end
110- end
111- end
112+ where, right_hand_sides = convert_where_clauses(query.where_clauses, query.reasoning?)
113
114 # if keyword clause given, convert it using keyword index
115 if query.keyword? && keyword_search?
116@@ -418,6 +410,48 @@
117 ["where " + where.join(' and '), right_hand_sides]
118 end
119 end
120+
121+# construct union where clause
122+ def construct_union_where(union, reasoning)
123+ where, right_hand_sides = convert_where_clauses(union.clauses, reasoning)
124+
125+ if where.empty?
126+ ['',[]]
127+ else
128+ ["where " + where.join(' and '), right_hand_sides]
129+ end
130+ end
131+
132+ def convert_where_clauses(clauses, reasoning)
133+ # collecting where clauses, these will be added to the sql string later
134+ where = []
135+
136+ # collecting all the right-hand sides of where clauses (e.g. where name =
137+ # 'abc'), to add to query string later using ?-notation, because then
138+ # sqlite will automatically encode quoted literals correctly
139+ right_hand_sides = []
140+
141+ # convert each where clause to SQL:
142+ # add where clause for each subclause, except if it's a variable
143+ clauses.each_with_index do |clause,level|
144+ raise ActiveRdfError, "where clause #{clause} is not a triple" unless clause.is_a?(Array)
145+ clause.each_with_index do |subclause, i|
146+ # dont add where clause for variables
147+ unless subclause.is_a?(Symbol) || subclause.nil?
148+ conditions = compute_where_condition(i, subclause, reasoning && reasoning?)
149+ if conditions.size == 1
150+ where << "t#{level}.#{SPOC[i]} = ?"
151+ right_hand_sides << conditions.first
152+ else
153+ conditions = conditions.collect {|c| "'#{c}'"}
154+ where << "t#{level}.#{SPOC[i]} in (#{conditions.join(',')})"
155+ end
156+ end
157+ end
158+ end
159+
160+ return where, right_hand_sides
161+ end
162
163 def compute_where_condition(index, subclause, reasoning)
164 conditions = [subclause]
165@@ -490,6 +524,20 @@
166 termspo = SPOC[index % 4]
167 return "#{termtable}.#{termspo}"
168 end
169+
170+ def union_variable_name(union,term)
171+ # look up the first occurence of this term in the where clauses, and compute
172+ # the level and s/p/o position of it
173+ index = union.clauses.flatten.index(term)
174+
175+ if index.nil?
176+ raise ActiveRdfError, "unbound variable :#{term.to_s} in select of #{union}"
177+ end
178+
179+ termtable = "t#{index / 4}"
180+ termspo = SPOC[index % 4]
181+ return "#{termtable}.#{termspo}"
182+ end
183
184 # wrap resources into ActiveRDF resources, literals into Strings
185 def wrap(query, results)
186
187=== modified file 'activerdf-redland/lib/activerdf_redland/redland.rb'
188--- activerdf-redland/lib/activerdf_redland/redland.rb 2008-11-28 09:29:34 +0000
189+++ activerdf-redland/lib/activerdf_redland/redland.rb 2009-05-20 03:36:52 +0000
190@@ -106,6 +106,7 @@
191 def query(query)
192 qs = Query2SPARQL.translate(query)
193 $activerdflog.debug "RedlandAdapter: executing SPARQL query #{qs}"
194+ $activerdflog.warn "RedlandAdapter: UNION not supported before Rasqal 0.9.17" unless query.union_groups.empty? || Redland::rasqal_version_decimal > 916
195
196 clauses = query.select_clauses.size
197 redland_query = Redland::Query.new(qs, 'sparql')
198
199=== modified file 'lib/active_rdf/queryengine/query.rb'
200--- lib/active_rdf/queryengine/query.rb 2007-09-20 20:35:26 +0000
201+++ lib/active_rdf/queryengine/query.rb 2009-05-20 03:36:52 +0000
202@@ -6,7 +6,7 @@
203 # data source. In all clauses symbols represent variables:
204 # Query.new.select(:s).where(:s,:p,:o).
205 class Query
206- attr_reader :select_clauses, :where_clauses, :sort_clauses, :keywords, :limits, :offsets, :reverse_sort_clauses, :filter_clauses
207+ attr_reader :select_clauses, :where_clauses, :sort_clauses, :keywords, :limits, :offsets, :reverse_sort_clauses, :filter_clauses, :union_groups
208
209 bool_accessor :distinct, :ask, :select, :count, :keyword, :reasoning
210
211@@ -21,6 +21,7 @@
212 @keywords = {}
213 @reasoning = true
214 @reverse_sort_clauses = []
215+ @union_groups = []
216 end
217
218 # Clears the select clauses
219@@ -159,6 +160,72 @@
220 self
221 end
222
223+ class UnionGroup
224+ attr_reader :clauses, :filter_clauses
225+
226+ def initialize
227+ @clauses = []
228+ @filter_clauses = []
229+ end
230+
231+ # adds a clause to this union group
232+ def where s,p,o,c=nil
233+ unless s.respond_to?(:uri) or s.is_a?(Symbol)
234+ raise(ActiveRdfError, "cannot add a where clause with s #{s}: s must be a resource or a variable")
235+ end
236+ unless p.respond_to?(:uri) or p.is_a?(Symbol)
237+ raise(ActiveRdfError, "cannot add a where clause with p #{p}: p must be a resource or a variable")
238+ end
239+
240+ @clauses << [s,p,o,c].collect{|arg| parametrise(arg)}
241+
242+ self
243+ end
244+
245+ # adds one or more generic filters
246+ # NOTE: you have to use SPARQL syntax for variables, eg. regex(?s, 'abc')
247+ def filter *s
248+ # add filter clauses
249+ @filter_clauses << s
250+ @filter_clauses.uniq!
251+
252+ self
253+ end
254+
255+ private
256+ #borrowed from outer class
257+ def parametrise s
258+ case s
259+ when Symbol, RDFS::Resource, Literal, Class
260+ s
261+ when nil
262+ nil
263+ else
264+ '"' + s.to_s + '"'
265+ end
266+ end
267+ end
268+
269+ # adds a union group to this query
270+ #
271+ # usage:: union_group = Query.new.select(:s).where(:s, :p, 'eyal').union
272+ # usage:: Query.new.select(:s).where(:s, :p, 'eyal').union {|g| g.where(:s, :p, 'benjamin')}.execute
273+ # usage:: Query.new.select(:s).where(:s, :p, 'eyal').union(:s, :p, 'benjamin').execute
274+ def union *args
275+ union_group = UnionGroup.new
276+ @union_groups << union_group
277+
278+ union_group.where *args unless args.empty?
279+
280+ yield(union_group) if block_given?
281+
282+ if !args.empty? || block_given?
283+ self
284+ else
285+ union_group
286+ end
287+ end
288+
289 # Adds keyword constraint to the query. You can use all Ferret query syntax in
290 # the constraint (e.g. keyword_where(:s,'eyal|benjamin')
291 def keyword_where s,o
292
293=== modified file 'lib/active_rdf/queryengine/query2sparql.rb'
294--- lib/active_rdf/queryengine/query2sparql.rb 2008-02-08 12:37:42 +0000
295+++ lib/active_rdf/queryengine/query2sparql.rb 2009-05-20 03:36:52 +0000
296@@ -13,7 +13,8 @@
297 select_clauses = query.select_clauses.collect{|s| construct_clause(s)}
298
299 str << "SELECT #{distinct}#{select_clauses.join(' ')} "
300- str << "WHERE { #{where_clauses(query)} #{filter_clauses(query)}} "
301+ str << "WHERE { { #{where_clauses(query)} #{filter_clauses(query.filter_clauses)}} #{union_groups(query)}} "
302+ str <<
303 str << "LIMIT #{query.limits} " if query.limits
304 str << "OFFSET #{query.offsets} " if query.offsets
305 elsif query.ask?
306@@ -24,8 +25,8 @@
307 end
308
309 # concatenate filters in query
310- def self.filter_clauses(query)
311- "FILTER (#{query.filter_clauses.join(" && ")})" unless query.filter_clauses.empty?
312+ def self.filter_clauses(filter_clauses)
313+ "FILTER (#{filter_clauses.join(" && ")})" unless filter_clauses.empty?
314 end
315
316 # concatenate each where clause using space (e.g. 's p o')
317@@ -44,18 +45,22 @@
318 end
319 end
320
321- where_clauses = query.where_clauses.collect do |s,p,o,c|
322+ statements(query.where_clauses)
323+ end
324+
325+ def self.statements(clauses)
326+ where_clauses = clauses.collect do |s,p,o,c|
327 # does there where clause use a context ?
328- if c.nil?
329- [s,p,o].collect {|term| construct_clause(term) }.join(' ')
330- else
331- "GRAPH #{construct_clause(c)} { #{construct_clause(s)} #{construct_clause(p)} #{construct_clause(o)} }"
332- end
333- end
334+ if c.nil?
335+ [s,p,o].collect {|term| construct_clause(term) }.join(' ')
336+ else
337+ "GRAPH #{construct_clause(c)} { #{construct_clause(s)} #{construct_clause(p)} #{construct_clause(o)} }"
338+ end
339+ end
340
341 "#{where_clauses.join(' . ')} ."
342 end
343-
344+
345 def self.construct_clause(term)
346 if term.is_a?(Symbol)
347 "?#{term}"
348@@ -63,6 +68,10 @@
349 term.to_ntriple
350 end
351 end
352+
353+ def self.union_groups(query)
354+ query.union_groups.collect {|union| "UNION { #{statements(union.clauses)} #{filter_clauses(union.filter_clauses)}} "}.join
355+ end
356
357 def self.sparql_engine
358 sparql_adapters = ConnectionPool.read_adapters.select{|adp| adp.is_a? SparqlAdapter}
359
360=== modified file 'test/queryengine/test_query2sparql.rb'
361--- test/queryengine/test_query2sparql.rb 2008-02-08 12:37:42 +0000
362+++ test/queryengine/test_query2sparql.rb 2009-05-20 03:36:52 +0000
363@@ -19,7 +19,7 @@
364 query.where(:s, RDFS::Resource.new('predicate'), 30)
365
366 generated = Query2SPARQL.translate(query)
367- expected = "SELECT ?s WHERE { ?s <predicate> \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> . } "
368+ expected = "SELECT ?s WHERE { { ?s <predicate> \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> . } } "
369 assert_equal expected, generated
370
371 query = Query.new
372@@ -27,7 +27,7 @@
373 query.where(:s, RDFS::Resource.new('foaf:age'), :a)
374 query.where(:a, RDFS::Resource.new('rdf:type'), RDFS::Resource.new('xsd:int'))
375 generated = Query2SPARQL.translate(query)
376- expected = "SELECT ?s WHERE { ?s <foaf:age> ?a . ?a <rdf:type> <xsd:int> . } "
377+ expected = "SELECT ?s WHERE { { ?s <foaf:age> ?a . ?a <rdf:type> <xsd:int> . } } "
378 assert_equal expected, generated
379 end
380
381@@ -36,7 +36,7 @@
382 query.distinct(:s)
383 query.where(:s, RDFS::Resource.new('foaf:age'), :a)
384 generated = Query2SPARQL.translate(query)
385- expected = "SELECT DISTINCT ?s WHERE { ?s <foaf:age> ?a . } "
386+ expected = "SELECT DISTINCT ?s WHERE { { ?s <foaf:age> ?a . } } "
387 assert_equal expected, generated
388 end
389
390@@ -46,4 +46,15 @@
391 q2 = Query.new.select(:s).select(:a)
392 assert_equal Query2SPARQL.translate(q1),Query2SPARQL.translate(q2)
393 end
394+
395+ def test_union
396+ query = Query.new
397+ query.distinct(:s)
398+ query.where(:s, RDFS::Resource.new('foaf:age'), :a)
399+ query.union.where(:s, RDFS::Resource.new('foaf:name'), :n)
400+ query.union{|u| u.where(:s, RDFS::Resource.new('foaf:mbox'), :m)}
401+ generated = Query2SPARQL.translate(query)
402+ expected = "SELECT DISTINCT ?s WHERE { { ?s <foaf:age> ?a . } UNION { ?s <foaf:name> ?n . } UNION { ?s <foaf:mbox> ?m . } } "
403+ assert_equal expected, generated
404+ end
405 end

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: