sparql 1.0.8 → 1.1.0p0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +14 -6
  2. data/README.md +57 -25
  3. data/VERSION +1 -1
  4. data/lib/sinatra/sparql.rb +5 -3
  5. data/lib/sparql/algebra/aggregate.rb +67 -0
  6. data/lib/sparql/algebra/evaluatable.rb +49 -4
  7. data/lib/sparql/algebra/expression.rb +6 -4
  8. data/lib/sparql/algebra/extensions.rb +99 -9
  9. data/lib/sparql/algebra/operator/and.rb +7 -4
  10. data/lib/sparql/algebra/operator/asc.rb +6 -3
  11. data/lib/sparql/algebra/operator/avg.rb +36 -0
  12. data/lib/sparql/algebra/operator/bnode.rb +5 -4
  13. data/lib/sparql/algebra/operator/bound.rb +5 -2
  14. data/lib/sparql/algebra/operator/coalesce.rb +6 -3
  15. data/lib/sparql/algebra/operator/concat.rb +6 -3
  16. data/lib/sparql/algebra/operator/count.rb +30 -0
  17. data/lib/sparql/algebra/operator/exists.rb +39 -0
  18. data/lib/sparql/algebra/operator/exprlist.rb +6 -3
  19. data/lib/sparql/algebra/operator/extend.rb +3 -1
  20. data/lib/sparql/algebra/operator/filter.rb +2 -1
  21. data/lib/sparql/algebra/operator/group.rb +101 -0
  22. data/lib/sparql/algebra/operator/group_concat.rb +55 -0
  23. data/lib/sparql/algebra/operator/if.rb +5 -5
  24. data/lib/sparql/algebra/operator/in.rb +7 -4
  25. data/lib/sparql/algebra/operator/max.rb +38 -0
  26. data/lib/sparql/algebra/operator/min.rb +38 -0
  27. data/lib/sparql/algebra/operator/minus.rb +51 -16
  28. data/lib/sparql/algebra/operator/negate.rb +31 -0
  29. data/lib/sparql/algebra/operator/notexists.rb +37 -0
  30. data/lib/sparql/algebra/operator/notin.rb +7 -4
  31. data/lib/sparql/algebra/operator/or.rb +7 -4
  32. data/lib/sparql/algebra/operator/order.rb +2 -2
  33. data/lib/sparql/algebra/operator/sample.rb +32 -0
  34. data/lib/sparql/algebra/operator/strlang.rb +1 -1
  35. data/lib/sparql/algebra/operator/sum.rb +36 -0
  36. data/lib/sparql/algebra/operator/table.rb +44 -0
  37. data/lib/sparql/algebra/operator.rb +37 -2
  38. data/lib/sparql/algebra.rb +1 -0
  39. data/lib/sparql/grammar/meta.rb +1143 -696
  40. data/lib/sparql/grammar/parser11.rb +178 -32
  41. data/lib/sparql/grammar/terminals11.rb +2 -2
  42. data/lib/sparql/grammar.rb +0 -2
  43. data/lib/sparql/results.rb +55 -0
  44. metadata +78 -81
@@ -180,7 +180,7 @@ module SPARQL::Grammar
180
180
  |MAX|MD5|MINUTES|MIN|MONTH|NOW|RAND|REPLACE|ROUND|SAMPLE|SECONDS|SEPARATOR
181
181
  |SHA1|SHA224|SHA256|SHA384|SHA512
182
182
  |STRAFTER|STRBEFORE|STRDT|STRENDS|STRLANG|STRLEN|STRSTARTS|STRUUID|SUBSTR|STR|SUM
183
- |TIMEZONE|TZ|UCASE|URI|UUID|YEAR
183
+ |TIMEZONE|TZ|UCASE|UNDEF|URI|UUID|YEAR
184
184
  |isBLANK|isIRI|isURI|isLITERAL|isNUMERIC|sameTerm
185
185
  }x
186
186
  add_prod_datum(token.value.downcase.to_sym, token.value.downcase.to_sym)
@@ -191,19 +191,24 @@ module SPARQL::Grammar
191
191
 
192
192
  # Productions
193
193
  # [2] Query ::= Prologue
194
- # ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery ) ValuesClause
194
+ # ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery )
195
195
  production(:Query) do |input, data, callback|
196
- if data[:query]
197
- query = data[:query].first
198
- if data[:PrefixDecl]
199
- pfx = data[:PrefixDecl].shift
200
- data[:PrefixDecl].each {|p| pfx.merge!(p)}
201
- pfx.operands[1] = query
202
- query = pfx
203
- end
204
- query = SPARQL::Algebra::Expression[:base, data[:BaseDecl].first, query] if data[:BaseDecl]
205
- add_prod_datum(:query, query)
196
+ return unless data[:query]
197
+
198
+ query = data[:query].first
199
+
200
+ # Add prefix
201
+ if data[:PrefixDecl]
202
+ pfx = data[:PrefixDecl].shift
203
+ data[:PrefixDecl].each {|p| pfx.merge!(p)}
204
+ pfx.operands[1] = query
205
+ query = pfx
206
206
  end
207
+
208
+ # Add base
209
+ query = SPARQL::Algebra::Expression[:base, data[:BaseDecl].first, query] if data[:BaseDecl]
210
+
211
+ add_prod_datum(:query, query)
207
212
  end
208
213
 
209
214
  # [4] Prologue ::= ( BaseDecl | PrefixDecl )*
@@ -233,13 +238,17 @@ module SPARQL::Grammar
233
238
  end
234
239
  end
235
240
 
236
- # [7] SelectQuery ::= SelectClause DatasetClause* WhereClause SolutionModifier
241
+ # [7] SelectQuery ::= SelectClause DatasetClause* WhereClause SolutionModifier ValuesClause
237
242
  production(:SelectQuery) do |input, data, callback|
238
243
  query = merge_modifiers(data)
239
244
  add_prod_datum :query, query
240
245
  end
241
246
 
242
247
  # [8] SubSelect ::= SelectClause WhereClause SolutionModifier
248
+ production(:SubSelect) do |input, data, callback|
249
+ query = merge_modifiers(data)
250
+ add_prod_datum :query, query
251
+ end
243
252
 
244
253
  # [9] SelectClause ::= 'SELECT' ( 'DISTINCT' | 'REDUCED' )? ( ( Var | ( '(' Expression 'AS' Var ')' ) )+ | '*' )
245
254
 
@@ -256,6 +265,7 @@ module SPARQL::Grammar
256
265
  # 'WHERE' '{' TriplesTemplate? '}'
257
266
  # SolutionModifier
258
267
  # )
268
+ # ValuesClause
259
269
  production(:ConstructQuery) do |input, data, callback|
260
270
  data[:query] ||= [SPARQL::Algebra::Operator::BGP.new(*data[:pattern])]
261
271
  query = merge_modifiers(data)
@@ -264,14 +274,14 @@ module SPARQL::Grammar
264
274
  end
265
275
 
266
276
  # [11] DescribeQuery ::= 'DESCRIBE' ( VarOrIri+ | '*' )
267
- # DatasetClause* WhereClause? SolutionModifier
277
+ # DatasetClause* WhereClause? SolutionModifier ValuesClause
268
278
  production(:DescribeQuery) do |input, data, callback|
269
279
  query = merge_modifiers(data)
270
280
  to_describe = data[:VarOrIri] || []
271
281
  add_prod_datum :query, SPARQL::Algebra::Expression[:describe, to_describe, query]
272
282
  end
273
283
 
274
- # [12] AskQuery ::= 'ASK' DatasetClause* WhereClause
284
+ # [12] AskQuery ::= 'ASK' DatasetClause* WhereClause ValuesClause
275
285
  production(:AskQuery) do |input, data, callback|
276
286
  query = merge_modifiers(data)
277
287
  add_prod_datum :query, SPARQL::Algebra::Expression[:ask, query]
@@ -311,8 +321,9 @@ module SPARQL::Grammar
311
321
  end
312
322
 
313
323
  # [21] HavingClause ::= 'HAVING' HavingCondition+
314
- #production(:GroupClause) do |input, data, callback|
315
- #end
324
+ production(:HavingClause) do |input, data, callback|
325
+ add_prod_datum(:having, data[:Constraint])
326
+ end
316
327
 
317
328
  # [23] OrderClause ::= 'ORDER' 'BY' OrderCondition+
318
329
  production(:OrderClause) do |input, data, callback|
@@ -353,6 +364,19 @@ module SPARQL::Grammar
353
364
  add_prod_datum(:offset, data[:literal])
354
365
  end
355
366
 
367
+ # [28] ValuesClause ::= ( 'VALUES' DataBlock )?
368
+ production(:ValuesClause) do |input, data, callback|
369
+ debug("ValuesClause") {"vars: #{data[:Var].inspect}, row: #{data[:row].inspect}"}
370
+ if data[:row]
371
+ add_prod_datum :ValuesClause, SPARQL::Algebra::Expression.for(:table,
372
+ data[:Var].to_a.unshift(:vars),
373
+ *data[:row]
374
+ )
375
+ else
376
+ add_prod_datum :ValuesClause, SPARQL::Algebra::Expression.for(:table, :empty)
377
+ end
378
+ end
379
+
356
380
  # [54] GroupGraphPatternSub ::= TriplesBlock?
357
381
  # ( GraphPatternNotTriples '.'? TriplesBlock? )*
358
382
  production(:GroupGraphPatternSub) do |input, data, callback|
@@ -411,6 +435,8 @@ module SPARQL::Grammar
411
435
  add_prod_datum(:query, SPARQL::Algebra::Expression.for(:leftjoin, lhs, *data[:leftjoin]))
412
436
  elsif data[:query] && !lhs.empty?
413
437
  add_prod_datum(:query, SPARQL::Algebra::Expression.for(:join, lhs, *data[:query]))
438
+ elsif data[:minus]
439
+ add_prod_datum(:query, SPARQL::Algebra::Expression.for(:minus, lhs, *data[:minus]))
414
440
  elsif data[:query]
415
441
  add_prod_datum(:query, data[:query])
416
442
  else
@@ -447,6 +473,60 @@ module SPARQL::Grammar
447
473
  add_prod_datum :extend, [data[:Expression].unshift(data[:Var].first)]
448
474
  end
449
475
 
476
+ # [61] InlineData ::= 'VALUES' DataBlock
477
+ production(:InlineData) do |input, data, callback|
478
+ debug("InlineData") {"vars: #{data[:Var].inspect}, row: #{data[:row].inspect}"}
479
+ add_prod_datum :query, SPARQL::Algebra::Expression.for(:table,
480
+ data[:Var].unshift(:vars),
481
+ *data[:row]
482
+ )
483
+ end
484
+
485
+ # [63] InlineDataOneVar ::= Var '{' DataBlockValue* '}'
486
+ production(:InlineDataOneVar) do |input, data, callback|
487
+ add_prod_datum :Var, data[:Var]
488
+
489
+ data[:DataBlockValue].each do |d|
490
+ add_prod_datum :row, [[:row, data[:Var].dup << d]]
491
+ end
492
+ end
493
+
494
+ # [64] InlineDataFull ::= ( NIL | '(' Var* ')' )
495
+ # '{' ( '(' DataBlockValue* ')' | NIL )* '}'
496
+ production(:InlineDataFull) do |input, data, callback|
497
+ vars = data[:Var]
498
+ add_prod_datum :Var, vars
499
+
500
+ if data[:NIL].to_a.length > 1
501
+ add_prod_data :row, [:row]
502
+ else
503
+ data[:DataBlockValue].to_a.each do |ds|
504
+ r = [:row]
505
+ ds.each_with_index do |d, i|
506
+ r << [vars[i], d] if d
507
+ end
508
+ add_prod_data :row, r unless r.empty?
509
+ end
510
+ end
511
+ end
512
+
513
+ # _InlineDataFull_8 ::= '(' DataBlockValue* ')'
514
+ production(:_InlineDataFull_8) do |input, data, callback|
515
+ add_prod_data :DataBlockValue, data[:DataBlockValue].map {|v| v unless v == :undef}
516
+ end
517
+
518
+ # [65] DataBlockValue ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | 'UNDEF'
519
+ production(:DataBlockValue) do |input, data, callback|
520
+ add_prod_datum :DataBlockValue, data.values.first
521
+ end
522
+
523
+ # [66] MinusGraphPattern ::= 'MINUS' GroupGraphPattern
524
+ production(:MinusGraphPattern) do |input, data, callback|
525
+ expr = nil
526
+ query = data[:query] ? data[:query].first : SPARQL::Algebra::Operator::BGP.new
527
+ add_prod_data(:minus, query)
528
+ end
529
+
450
530
  # [67] GroupOrUnionGraphPattern ::= GroupGraphPattern
451
531
  # ( 'UNION' GroupGraphPattern )*
452
532
  production(:GroupOrUnionGraphPattern) do |input, data, callback|
@@ -665,10 +745,10 @@ module SPARQL::Grammar
665
745
  if data[:_Compare_Numeric]
666
746
  add_prod_datum(:Expression, SPARQL::Algebra::Expression.for(data[:_Compare_Numeric].insert(1, *data[:Expression])))
667
747
  elsif data[:in]
668
- expr = (data[:Expression] + data[:in]).reject {|v| v == RDF.nil}
748
+ expr = (data[:Expression] + data[:in]).reject {|v| v.equal?(RDF.nil)}
669
749
  add_prod_datum(:Expression, SPARQL::Algebra::Expression.for(expr.unshift(:in)))
670
750
  elsif data[:notin]
671
- expr = (data[:Expression] + data[:notin]).reject {|v| v == RDF.nil}
751
+ expr = (data[:Expression] + data[:notin]).reject {|v| v.equal?(RDF.nil)}
672
752
  add_prod_datum(:Expression, SPARQL::Algebra::Expression.for(expr.unshift(:notin)))
673
753
  else
674
754
  # NumericExpression with no comparitor
@@ -753,7 +833,7 @@ module SPARQL::Grammar
753
833
  if e.is_a?(RDF::Literal::Numeric)
754
834
  add_prod_datum(:Expression, -e) # Simple optimization to match ARQ generation
755
835
  else
756
- add_prod_datum(:Expression, SPARQL::Algebra::Expression[:minus, e])
836
+ add_prod_datum(:Expression, SPARQL::Algebra::Expression[:"-", e])
757
837
  end
758
838
  else
759
839
  add_prod_datum(:Expression, data[:Expression])
@@ -898,7 +978,7 @@ module SPARQL::Grammar
898
978
  production(:Aggregate) do |input, data, callback|
899
979
  if aggregate_rule = data.keys.detect {|k| AGGREGATE_RULES.include?(k)}
900
980
  parts = [aggregate_rule]
901
- parts << [:separator, data[:string].first] if data[:separator] && data[:string]
981
+ parts << [:separator, RDF::Literal(data[:string].first)] if data[:separator] && data[:string]
902
982
  parts << :distinct if data[:DISTINCT_REDUCED]
903
983
  parts << data[:Expression].first if data[:Expression]
904
984
  add_prod_data(aggregate_rule, SPARQL::Algebra::Expression.for(parts))
@@ -1038,7 +1118,7 @@ module SPARQL::Grammar
1038
1118
  # @param [Symbol, #to_s] prod The starting production for the parser.
1039
1119
  # It may be a URI from the grammar, or a symbol representing the local_name portion of the grammar URI.
1040
1120
  # @return [Array]
1041
- # @see http://www.w3.org/2001/sw/DataAccess/rq23/rq24-algebra.html
1121
+ # @see http://www.w3.org/TR/sparql11-query/#sparqlAlgebra
1042
1122
  # @see http://axel.deri.ie/sparqltutorial/ESWC2007_SPARQL_Tutorial_unit2b.pdf
1043
1123
  def parse(prod = START)
1044
1124
  ll1_parse(@input, prod.to_sym, @options.merge(:branch => BRANCH,
@@ -1048,7 +1128,7 @@ module SPARQL::Grammar
1048
1128
  case context
1049
1129
  when :trace
1050
1130
  level, lineno, depth, *args = data
1051
- message = "#{args.join(': ')}"
1131
+ message = args.to_sse
1052
1132
  d_str = depth > 100 ? ' ' * 100 + '+' : ' ' * depth
1053
1133
  str = "[#{lineno}](#{level})#{d_str}#{message}"
1054
1134
  case @options[:debug]
@@ -1314,27 +1394,93 @@ module SPARQL::Grammar
1314
1394
  #
1315
1395
  # @see http://www.w3.org/TR/sparql11-query/#convertGroupAggSelectExpressions
1316
1396
  def merge_modifiers(data)
1397
+ debug("merge modifiers") {data.inspect}
1317
1398
  query = data[:query] ? data[:query].first : SPARQL::Algebra::Operator::BGP.new
1318
1399
 
1319
1400
  vars = data[:Var] || []
1320
1401
  order = data[:order] ? data[:order].first : []
1402
+ extensions = data.fetch(:extend, [])
1403
+ having = data.fetch(:having, [])
1404
+ values = data.fetch(:ValuesClause, []).first
1405
+
1406
+ # extension variables must not appear in projected variables.
1407
+ # Add them to the projection otherwise
1408
+ extensions.each do |(var, expr)|
1409
+ raise Error, "Extension variable #{var} also in SELECT" if vars.map(&:to_s).include?(var.to_s)
1410
+ vars << var
1411
+ end
1412
+
1413
+ # If any extension contains an aggregate, and there is now group, implicitly group by 1
1414
+ if !data[:group] &&
1415
+ extensions.any? {|(var, function)| function.aggregate?} ||
1416
+ having.any? {|c| c.aggregate? }
1417
+ debug {"Implicit group"}
1418
+ data[:group] = [[]]
1419
+ end
1321
1420
 
1322
1421
  # Add datasets and modifiers in order
1323
1422
  if data[:group]
1324
- query = SPARQL::Algebra::Expression[:group, data[:group].first, query]
1325
- end
1423
+ group_vars = data[:group].first
1424
+
1425
+ # For creating temporary variables
1426
+ agg = 0
1427
+
1428
+ # Find aggregated varirables in extensions
1429
+ aggregates = []
1430
+ aggregated_vars = extensions.map do |(var, function)|
1431
+ var if function.aggregate?
1432
+ end.compact
1433
+
1434
+ # Common function for replacing aggregates with temporary variables,
1435
+ # as defined in http://www.w3.org/TR/2013/REC-sparql11-query-20130321/#convertGroupAggSelectExpressions
1436
+ aggregate_expression = lambda do |expr|
1437
+ # Replace unaggregated variables in expr
1438
+ # - For each unaggregated variable V in X
1439
+ expr.replace_vars! do |v|
1440
+ aggregated_vars.include?(v) ? v : SPARQL::Algebra::Expression[:sample, v]
1441
+ end
1442
+
1443
+ # Replace aggregates in expr as above
1444
+ expr.replace_aggregate! do |function|
1445
+ if avf = aggregates.detect {|(v, f)| f == function}
1446
+ avf.first
1447
+ else
1448
+ # Allocate a temporary variable for this function, and retain the mapping for outside the group
1449
+ av = RDF::Query::Variable.new(".#{agg}")
1450
+ av.distinguished = false
1451
+ agg += 1
1452
+ aggregates << [av, function]
1453
+ av
1454
+ end
1455
+ end
1456
+ end
1457
+
1458
+ # If there are extensions, they are aggregated if necessary and bound
1459
+ # to temporary variables
1460
+ extensions.map! do |(var, expr)|
1461
+ [var, aggregate_expression.call(expr)]
1462
+ end
1326
1463
 
1327
- if data[:extend] # FIXME: needed?
1328
- # extension variables must not appear in projected variables.
1329
- # Add them to the projection otherwise
1330
- data[:extend].each do |(var, expr)|
1331
- raise Error, "Extension variable #{var} also in SELECT" if vars.map(&:to_s).include?(var.to_s)
1332
- vars << var
1464
+ # Having clauses
1465
+ having.map! do |expr|
1466
+ aggregate_expression.call(expr)
1333
1467
  end
1334
1468
 
1335
- query = SPARQL::Algebra::Expression[:extend, data[:extend], query]
1469
+ query = if aggregates.empty?
1470
+ SPARQL::Algebra::Expression[:group, group_vars, query]
1471
+ else
1472
+ SPARQL::Algebra::Expression[:group, group_vars, aggregates, query]
1473
+ end
1474
+ end
1475
+
1476
+ if values
1477
+ query = query ? SPARQL::Algebra::Expression[:join, query, values] : values
1336
1478
  end
1337
1479
 
1480
+ query = SPARQL::Algebra::Expression[:extend, extensions, query] unless extensions.empty?
1481
+
1482
+ query = SPARQL::Algebra::Expression[:filter, *having, query] unless having.empty?
1483
+
1338
1484
  query = SPARQL::Algebra::Expression[:order, data[:order].first, query] unless order.empty?
1339
1485
 
1340
1486
  query = SPARQL::Algebra::Expression[:project, vars, query] unless vars.empty?
@@ -105,7 +105,7 @@ module SPARQL::Grammar
105
105
  |SELECT|SEPARATOR|SERVICE
106
106
  |SHA1|SHA224|SHA256|SHA384|SHA512
107
107
  |STRAFTER|STRBEFORE|STRDT|STRENDS|STRLANG|STRLEN|STRSTARTS|STRUUID|SUBSTR|STR|SUM
108
- |TIMEZONE|TO|TZ|UCASE|UNDEF|UNION|URI|USING|UUID
108
+ |TIMEZONE|TO|TZ|UCASE|UNDEF|UNION|URI|USING|UUID|VALUES
109
109
  |WHERE|WITH|YEAR
110
110
  |isBLANK|isIRI|isURI|isLITERAL|isNUMERIC|sameTerm
111
111
  |true
@@ -129,7 +129,7 @@ module SPARQL::Grammar
129
129
  SHA1 SHA224 SHA256 SHA384 SHA512
130
130
  STRAFTER STRBEFORE STRDT STRENDS STRLANG STRLEN STRSTARTS STRUUID SUBSTR STR SUM
131
131
  TIMEZONE TO TZ UCASE UNDEF UNION URI USING UUID
132
- WHERE WITH YEAR
132
+ VALUES WHERE WITH YEAR
133
133
  isBLANK isIRI isURI isLITERAL isNUMERIC sameTerm
134
134
  true false
135
135
  } + [
@@ -205,7 +205,5 @@ module SPARQL
205
205
  def self.tokenize(query, options = {}, &block)
206
206
  Lexer.tokenize(query, options, &block)
207
207
  end
208
-
209
- class SPARQL_GRAMMAR < RDF::Vocabulary("http://www.w3.org/2000/10/swap/grammar/sparql#"); end
210
208
  end # Grammar
211
209
  end # SPARQL
@@ -7,6 +7,8 @@ module SPARQL
7
7
  :json => 'application/sparql-results+json',
8
8
  :html => 'text/html',
9
9
  :xml => 'application/sparql-results+xml',
10
+ :csv => 'text/csv',
11
+ :tsv => 'text/tab-separated-values'
10
12
  }
11
13
 
12
14
  ##
@@ -112,6 +114,57 @@ module SPARQL
112
114
  end
113
115
  end
114
116
  end
117
+
118
+ ##
119
+ # Generate Solutions as CSV
120
+ # @return [String]
121
+ # @see http://www.w3.org/TR/rdf-sparql-json-res/#results
122
+ def to_csv
123
+ require 'csv' unless defined?(::CSV)
124
+ bnode_map = {}
125
+ bnode_gen = "_:a"
126
+ CSV.generate(:row_sep => "\r\n") do |csv|
127
+ csv << variable_names.to_a
128
+ self.each do |solution|
129
+ csv << variable_names.map do |n|
130
+ case term = solution[n]
131
+ when RDF::Node then bnode_map[term] ||=
132
+ begin
133
+ this = bnode_gen
134
+ bnode_gen = bnode_gen.succ
135
+ this
136
+ end
137
+ else
138
+ solution[n].to_s
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ ##
145
+ # Generate Solutions as TSV
146
+ # @return [String]
147
+ # @see http://www.w3.org/TR/rdf-sparql-json-res/#results
148
+ def to_tsv
149
+ require 'csv' unless defined?(::CSV)
150
+ bnode_map = {}
151
+ bnode_gen = "_:a"
152
+ results = [
153
+ variable_names.map {|v| "?#{v}"}.join("\t")
154
+ ] + self.map do |solution|
155
+ variable_names.map do |n|
156
+ case term = solution[n]
157
+ when RDF::Literal::Integer, RDF::Literal::Decimal, RDF::Literal::Double
158
+ term.canonicalize.to_s
159
+ when nil
160
+ ""
161
+ else
162
+ RDF::NTriples.serialize(term)
163
+ end
164
+ end.join("\t")
165
+ end
166
+ results.join("\n") + "\n"
167
+ end
115
168
  end
116
169
 
117
170
  ##
@@ -189,6 +242,8 @@ module SPARQL
189
242
  when :json then solutions.to_json
190
243
  when :xml then solutions.to_xml
191
244
  when :html then solutions.to_html
245
+ when :csv then solutions.to_csv
246
+ when :tsv then solutions.to_tsv
192
247
  else
193
248
  raise RDF::WriterError, "Unknown format #{(format || content_type).inspect} for #{solutions.class}"
194
249
  end