Merge lp:~rockwalrus/activerdf/devel into lp:activerdf
- devel
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
ActiveRDF | Pending | ||
Review via email: mp+6713@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
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 |
Support for UNION queries.