solis 0.125.0 → 0.126.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c29d1cb5fcfe9ad5579f22c3971aa794edc602b6afa3679a2113e0caecd01aa4
4
- data.tar.gz: e067c8d0ad39a30dd49c921f5beded800d0f93de83817911c6fafd32486b89dd
3
+ metadata.gz: ce5b23eb71b6b7189377d4981fa8e6dc076128d60aec207e58310fbd162b8a16
4
+ data.tar.gz: 57c514df94596d89d76ea012cc946b2b521869f585f1d495609f8e0ec5cbdf71
5
5
  SHA512:
6
- metadata.gz: 939e20fe0909c981ee02d29766b5ac43446db72d94df6a49ca68208a4a7c8d969a543c859b9f8c9e18bb843f5dd34e186356cdd245aa0abb8120b3bee0a7e4ad
7
- data.tar.gz: 5fb1ae2ea0fd011eb0aab4d306252a27f9f238a0b908fe458895bd1d238974da59ee57d4520dc14a34e49e2fe2fa60bb6b4d8366f3cd4b9fed4c63ce15594527
6
+ metadata.gz: 912773a18a5b44da59e1bbb6b7e3472c5d8be9a8044df0b64acbb7da501a3156ead5615363bd23fbd595d42db1b862f3d776235758d92acf8d0942f42f2fde00
7
+ data.tar.gz: 90c434c26f367159078555cf9bfbc715d4179ad8f3ebffba12ba379000e7696bf25020cd9a033d90b1b1e95375574bd4600eb3492d67e703a07aaff901d6b251
data/lib/solis/query.rb CHANGED
@@ -11,6 +11,15 @@ module Solis
11
11
  include Enumerable
12
12
  include Solis::QueryFilter
13
13
 
14
+ # XSD numeric datatypes must be ordered by value; wrapping them in STR() would sort
15
+ # them lexically ("100" < "9"). Every other datatype is STR()-wrapped in the outer
16
+ # ORDER BY (see #sort_key_expression) to dodge a Virtuoso collation bug.
17
+ XSD_NUMERIC_DATATYPES = %w[
18
+ integer decimal float double long int short byte
19
+ nonNegativeInteger nonPositiveInteger negativeInteger positiveInteger
20
+ unsignedLong unsignedInt unsignedShort unsignedByte
21
+ ].map { |t| "http://www.w3.org/2001/XMLSchema##{t}" }.freeze
22
+
14
23
  def self.run(entity, query, options = {})
15
24
  Solis::Query::Runner.run(entity, query, options)
16
25
  end
@@ -96,6 +105,13 @@ module Solis
96
105
  @filter = {values: ["VALUES ?type {#{target_class}}"], concepts: ['?concept a ?type .'] }
97
106
  @sort = 'ORDER BY ?concept'
98
107
  @sort_select = ''
108
+ # @sort_project carries the sort key(s) out of the inner (paginated) subquery so the
109
+ # outer query can re-sort on them; @sort_outer is that outer ORDER BY. A subquery's
110
+ # ORDER BY only decides which rows survive LIMIT/OFFSET — it does NOT propagate
111
+ # through the enclosing join — so the outer query must sort too. See #sort for the
112
+ # datatype-aware STR() handling that the outer ORDER BY needs.
113
+ @sort_project = ''
114
+ @sort_outer = ''
99
115
  @language = Graphiti.context[:object]&.language || Solis::Options.instance.get[:language] || 'en'
100
116
  @query_cache = self.class.shared_query_cache
101
117
  end
@@ -113,19 +129,31 @@ module Solis
113
129
  def sort(params)
114
130
  @sort = ''
115
131
  @sort_select = ''
132
+ @sort_project = ''
133
+ @sort_outer = ''
116
134
  if params.key?(:sort)
117
135
  i = 0
136
+ outer = ''
118
137
  params[:sort].each do |attribute, direction|
119
- path = @model.class.metadata[:attributes][attribute.to_s][:path]
120
- @sort_select += "optional {\n" if @model.class.metadata[:attributes][attribute.to_s][:mincount] == 0
138
+ meta = @model.class.metadata[:attributes][attribute.to_s]
139
+ path = meta[:path]
140
+ @sort_select += "optional {\n" if meta[:mincount] == 0
121
141
  @sort_select += "?concept <#{path}> ?__#{attribute} . "
122
- @sort_select += "}\n" if @model.class.metadata[:attributes][attribute.to_s][:mincount] == 0
142
+ @sort_select += "}\n" if meta[:mincount] == 0
123
143
  @sort += ',' if i.positive?
124
144
  @sort += "#{direction.to_s.upcase}(?__#{attribute})"
145
+ # Carry the sort key out of the subquery so the outer query can re-sort on it.
146
+ @sort_project += " ?__#{attribute}"
147
+ outer += ',' if i.positive?
148
+ outer += "#{direction.to_s.upcase}(#{sort_key_expression(attribute, meta)})"
125
149
  i += 1
126
150
  end
127
151
 
128
- @sort = "ORDER BY #{@sort}" if i.positive?
152
+ if i.positive?
153
+ @sort = "ORDER BY #{@sort}"
154
+ # ?s tiebreaker keeps each subject's triples contiguous and ties deterministic.
155
+ @sort_outer = "ORDER BY #{outer} ?s"
156
+ end
129
157
  end
130
158
 
131
159
  self
@@ -219,6 +247,11 @@ module Solis
219
247
  # object when the open `?s ?p ?o` pattern is combined with the disjunctive
220
248
  # language_filter, returning the rdf:type object for every row. Aliasing through
221
249
  # BIND blocks that optimizer rewrite and is a no-op on engines without the bug.
250
+ # Outer ordering: the inner subquery's ORDER BY only selects which rows survive
251
+ # LIMIT/OFFSET; it does NOT propagate through the enclosing join. The outer query
252
+ # therefore re-sorts on the same key(s) (@sort_outer, datatype-aware STR()). When
253
+ # no sort is requested, `order by ?s` gives a cheap deterministic default order.
254
+ outer_order = @sort_outer.empty? ? 'order by ?s' : @sort_outer
222
255
  query = %(
223
256
  #{prefixes}
224
257
  SELECT ?s ?p ?o WHERE {
@@ -229,7 +262,7 @@ SELECT ?s ?p ?o WHERE {
229
262
  }
230
263
  #{language_filter}
231
264
  }
232
- order by ?s
265
+ #{outer_order}
233
266
  )
234
267
 
235
268
  Solis::LOGGER.info(query) if ConfigFile[:debug]
@@ -274,8 +307,12 @@ order by ?s
274
307
  end
275
308
 
276
309
  def core_query(relationship)
310
+ # @sort_project re-exposes the sort key(s) bound in @sort_select so the OUTER query
311
+ # can ORDER BY them. NB: for a multi-valued sort attribute this turns DISTINCT into
312
+ # one row per (concept, value) pair, which would skew LIMIT/OFFSET — sort attributes
313
+ # are expected to be single-valued.
277
314
  core_query = %(
278
- SELECT distinct (?concept AS ?s) WHERE {
315
+ SELECT distinct (?concept AS ?s)#{@sort_project} WHERE {
279
316
  #{@filter[:values].join("\n")}
280
317
  #{relationship}
281
318
  #{@filter[:concepts].join("\n")}
@@ -286,6 +323,21 @@ order by ?s
286
323
  )
287
324
  end
288
325
 
326
+ # SPARQL expression to sort an attribute by in the OUTER query.
327
+ #
328
+ # The outer (post-join) ORDER BY on this Virtuoso build (08.03.3335) mis-collates
329
+ # language-tagged literals, so string-ish keys are wrapped in STR() — which fixes the
330
+ # ordering and is order-preserving for xsd:string, dates, gYear, booleans and EDTF.
331
+ # Numeric keys must NOT be wrapped: STR() would order them lexically ("100" < "9"),
332
+ # and numbers carry no language tag so they are unaffected by the collation bug.
333
+ def sort_key_expression(attribute, meta = @model.class.metadata[:attributes][attribute.to_s])
334
+ if XSD_NUMERIC_DATATYPES.include?(meta[:datatype_rdf].to_s)
335
+ "?__#{attribute}"
336
+ else
337
+ "STR(?__#{attribute})"
338
+ end
339
+ end
340
+
289
341
  def prefixes
290
342
  "
291
343
  PREFIX sh: <http://www.w3.org/ns/shacl#>
data/lib/solis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.125.0"
2
+ VERSION = "0.126.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.125.0
4
+ version: 0.126.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik