solis 0.115.0 → 0.117.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 +4 -4
- data/lib/solis/graph.rb +4 -0
- data/lib/solis/model.rb +154 -123
- data/lib/solis/query.rb +38 -1
- data/lib/solis/shape/reader/sheet.rb +50 -29
- data/lib/solis/shape.rb +5 -1
- data/lib/solis/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa9ba6d86b36ebd75e9e0280928ce458f1ae70e4c64f44832c0963870f319b2f
|
|
4
|
+
data.tar.gz: 23c8bd2bcbbb4fe97dc133b3310d1527bab161dbce43e2939b45a8fa1a9b7712
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f15183cf04d168b3d06425bfe987d4cc446d0fad1e1b1f94532b8dfedf39b3401d39641226d91a63400f4a4e1c7096a0c995b64e2629d6d07424bae7e4c14375
|
|
7
|
+
data.tar.gz: d7219d14c2290aa045ca80a32a97df1f12fcc4aa0df31fc3628fe91dbd5e2ee7e6239474aef27f4f43caf45fb9347e8cd059a0deabff12a3b2bd754ad1e98342
|
data/lib/solis/graph.rb
CHANGED
|
@@ -369,6 +369,10 @@ module Solis
|
|
|
369
369
|
|
|
370
370
|
@sparql_client = Solis::Store::Sparql::Client.new(@sparql_endpoint)
|
|
371
371
|
result = @sparql_client.query("with <#{graph_name}> delete {?s ?p ?o} where{?s ?p ?o}")
|
|
372
|
+
|
|
373
|
+
# Clear the query cache since all data has been flushed
|
|
374
|
+
Solis::Query.shared_query_cache.clear if defined?(Solis::Query)
|
|
375
|
+
|
|
372
376
|
LOGGER.info(result)
|
|
373
377
|
true
|
|
374
378
|
end
|
data/lib/solis/model.rb
CHANGED
|
@@ -140,6 +140,9 @@ values ?s {<#{self.graph_id}>}
|
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
|
|
143
|
+
# Invalidate cached queries for this entity type
|
|
144
|
+
Solis::Query.invalidate_cache_for(self.class.name)
|
|
145
|
+
|
|
143
146
|
result
|
|
144
147
|
end
|
|
145
148
|
|
|
@@ -172,6 +175,10 @@ values ?s {<#{self.graph_id}>}
|
|
|
172
175
|
# Batch check existence of all embedded entities in one query
|
|
173
176
|
existing_ids = self.class.batch_exists?(sparql, all_embedded)
|
|
174
177
|
|
|
178
|
+
# Separate embedded entities into creates and updates
|
|
179
|
+
to_create_embedded = []
|
|
180
|
+
to_update_embedded = []
|
|
181
|
+
|
|
175
182
|
all_embedded.each do |embedded|
|
|
176
183
|
entity_exists = existing_ids.include?(embedded.graph_id)
|
|
177
184
|
|
|
@@ -179,23 +186,39 @@ values ?s {<#{self.graph_id}>}
|
|
|
179
186
|
unless entity_exists
|
|
180
187
|
Solis::LOGGER.warn("#{embedded.class.name} (id: #{embedded.id}) is readonly but does not exist in database. Skipping.")
|
|
181
188
|
end
|
|
189
|
+
elsif entity_exists
|
|
190
|
+
to_update_embedded << embedded
|
|
182
191
|
else
|
|
183
|
-
|
|
184
|
-
embedded_data = properties_to_hash(embedded)
|
|
185
|
-
embedded.update(embedded_data, validate_dependencies, false)
|
|
186
|
-
else
|
|
187
|
-
embedded.save(validate_dependencies, false)
|
|
188
|
-
end
|
|
192
|
+
to_create_embedded << embedded
|
|
189
193
|
end
|
|
190
194
|
end
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
# Batch insert all new embedded entities in one SPARQL INSERT
|
|
197
|
+
unless to_create_embedded.empty?
|
|
198
|
+
embedded_graph = RDF::Graph.new
|
|
199
|
+
embedded_graph.name = RDF::URI(self.class.graph_name)
|
|
200
|
+
known = {}
|
|
201
|
+
to_create_embedded.each { |e| collect_known_entities(e, known) }
|
|
202
|
+
to_create_embedded.each { |e| build_ttl_objekt(embedded_graph, e, [], validate_dependencies, known, skip_store_fetch: true) }
|
|
203
|
+
sparql.insert_data(embedded_graph, graph: embedded_graph.name)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Updates still processed individually (each needs its own DELETE/INSERT)
|
|
207
|
+
to_update_embedded.each do |embedded|
|
|
208
|
+
embedded_data = properties_to_hash(embedded)
|
|
209
|
+
embedded.update(embedded_data, validate_dependencies, false)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
graph = as_graph(self, validate_dependencies, {}, skip_store_fetch: true)
|
|
193
213
|
|
|
194
214
|
Solis::LOGGER.info SPARQL::Client::Update::InsertData.new(graph, graph: graph.name).to_s if ConfigFile[:debug]
|
|
195
215
|
|
|
196
216
|
result = sparql.insert_data(graph, graph: graph.name)
|
|
197
217
|
end
|
|
198
218
|
|
|
219
|
+
# Invalidate cached queries for this entity type
|
|
220
|
+
Solis::Query.invalidate_cache_for(self.class.name)
|
|
221
|
+
|
|
199
222
|
after_create_proc&.call(self)
|
|
200
223
|
self
|
|
201
224
|
rescue StandardError => e
|
|
@@ -217,7 +240,8 @@ values ?s {<#{self.graph_id}>}
|
|
|
217
240
|
# When false (default), uses PUT semantics:
|
|
218
241
|
# - Embedded entity arrays are fully replaced
|
|
219
242
|
# - Entities removed from the array are orphaned and deleted if unreferenced
|
|
220
|
-
|
|
243
|
+
# @param prefetched_original [Solis::Model, nil] optional pre-fetched original entity to avoid re-querying
|
|
244
|
+
def update(data, validate_dependencies = true, top_level = true, sparql_client = nil, patch: false, prefetched_original: nil)
|
|
221
245
|
raise Solis::Error::GeneralError, "I need a SPARQL endpoint" if self.class.sparql_endpoint.nil?
|
|
222
246
|
|
|
223
247
|
attributes = data.include?('attributes') ? data['attributes'] : data
|
|
@@ -226,7 +250,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
226
250
|
id = attributes.delete('id')
|
|
227
251
|
sparql = sparql_client || SPARQL::Client.new(self.class.sparql_endpoint)
|
|
228
252
|
|
|
229
|
-
original_klass = self.query.filter({ language: self.class.language, filters: { id: [id] } }).find_all.map { |m| m }&.first
|
|
253
|
+
original_klass = prefetched_original || self.query.filter({ language: self.class.language, filters: { id: [id] } }).find_all.map { |m| m }&.first
|
|
230
254
|
raise Solis::Error::NotFoundError if original_klass.nil?
|
|
231
255
|
updated_klass = original_klass.deep_dup
|
|
232
256
|
|
|
@@ -250,6 +274,16 @@ values ?s {<#{self.graph_id}>}
|
|
|
250
274
|
all_embedded = embedded_by_key.values.flatten
|
|
251
275
|
existing_ids = self.class.batch_exists?(sparql, all_embedded)
|
|
252
276
|
|
|
277
|
+
# Build lookup of original embedded entities by ID for pre-fetched updates
|
|
278
|
+
original_embedded_lookup = {}
|
|
279
|
+
embedded_by_key.each_key do |key|
|
|
280
|
+
orig = original_klass.instance_variable_get("@#{key}")
|
|
281
|
+
next if orig.nil?
|
|
282
|
+
Array(orig).each do |e|
|
|
283
|
+
original_embedded_lookup[e.id] = e if solis_model?(e) && e.id
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
253
287
|
# Second pass: process embedded entities using batched results
|
|
254
288
|
embedded_by_key.each do |key, embedded_list|
|
|
255
289
|
value = attributes[key]
|
|
@@ -280,7 +314,9 @@ values ?s {<#{self.graph_id}>}
|
|
|
280
314
|
else
|
|
281
315
|
if entity_exists
|
|
282
316
|
embedded_data = properties_to_hash(embedded)
|
|
283
|
-
embedded
|
|
317
|
+
# Pass pre-fetched original to avoid N+1 query in embedded update
|
|
318
|
+
prefetched = original_embedded_lookup[embedded.id]
|
|
319
|
+
embedded.update(embedded_data, validate_dependencies, false, nil, prefetched_original: prefetched)
|
|
284
320
|
new_embedded_values << embedded
|
|
285
321
|
else
|
|
286
322
|
embedded_value = embedded.save(validate_dependencies, false)
|
|
@@ -328,9 +364,10 @@ values ?s {<#{self.graph_id}>}
|
|
|
328
364
|
Solis::LOGGER.info("#{original_klass.class.name} unchanged, skipping")
|
|
329
365
|
data = original_klass
|
|
330
366
|
else
|
|
331
|
-
# Pre-populate known entities to avoid
|
|
332
|
-
|
|
333
|
-
|
|
367
|
+
# Pre-populate known entities separately to avoid cross-contamination
|
|
368
|
+
# between delete and insert graphs (same ID, different attribute values)
|
|
369
|
+
delete_known = collect_known_entities(original_klass)
|
|
370
|
+
delete_graph = as_graph(original_klass, false, delete_known)
|
|
334
371
|
where_graph = RDF::Graph.new(graph_name: RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), data: RDF::Repository.new)
|
|
335
372
|
|
|
336
373
|
if id.is_a?(Array)
|
|
@@ -341,13 +378,17 @@ values ?s {<#{self.graph_id}>}
|
|
|
341
378
|
where_graph << [RDF::URI("#{self.class.graph_name}#{tableized_class_name(self)}/#{id}"), :p, :o]
|
|
342
379
|
end
|
|
343
380
|
|
|
344
|
-
|
|
381
|
+
insert_known = collect_known_entities(updated_klass)
|
|
382
|
+
insert_graph = as_graph(updated_klass, true, insert_known)
|
|
345
383
|
|
|
346
384
|
delete_insert_query = SPARQL::Client::Update::DeleteInsert.new(delete_graph, insert_graph, where_graph, graph: insert_graph.name).to_s
|
|
347
385
|
delete_insert_query.gsub!('_:p', '?p')
|
|
348
386
|
|
|
349
387
|
sparql.query(delete_insert_query)
|
|
350
388
|
|
|
389
|
+
# Invalidate cache before verification to avoid stale reads
|
|
390
|
+
Solis::Query.invalidate_cache_for(self.class.name)
|
|
391
|
+
|
|
351
392
|
# Verify the update succeeded by re-fetching; fallback to insert if needed
|
|
352
393
|
data = self.query.filter({ filters: { id: [id] } }).find_all.map { |m| m }&.first
|
|
353
394
|
if data.nil?
|
|
@@ -359,6 +400,9 @@ values ?s {<#{self.graph_id}>}
|
|
|
359
400
|
delete_orphaned_entities(entities_to_check_for_deletion, sparql) unless patch
|
|
360
401
|
end
|
|
361
402
|
|
|
403
|
+
# Invalidate cached queries for this entity type
|
|
404
|
+
Solis::Query.invalidate_cache_for(self.class.name)
|
|
405
|
+
|
|
362
406
|
after_update_proc&.call(updated_klass, data)
|
|
363
407
|
|
|
364
408
|
data
|
|
@@ -384,6 +428,65 @@ values ?s {<#{self.graph_id}>}
|
|
|
384
428
|
sparql.query("ASK WHERE { <#{self.graph_id}> ?p ?o }")
|
|
385
429
|
end
|
|
386
430
|
|
|
431
|
+
# Save multiple entities in a single SPARQL INSERT operation.
|
|
432
|
+
# Entities that already exist are updated individually.
|
|
433
|
+
# @param entities [Array<Solis::Model>] entities to save
|
|
434
|
+
# @param validate_dependencies [Boolean] whether to validate dependencies
|
|
435
|
+
# @param batch_size [Integer] max entities per INSERT (default 100)
|
|
436
|
+
# @return [Array<Solis::Model>] the saved entities
|
|
437
|
+
def self.batch_save(entities, validate_dependencies: true, batch_size: 100)
|
|
438
|
+
raise "I need a SPARQL endpoint" if sparql_endpoint.nil?
|
|
439
|
+
return [] if entities.empty?
|
|
440
|
+
|
|
441
|
+
sparql = SPARQL::Client.new(sparql_endpoint)
|
|
442
|
+
|
|
443
|
+
# Batch check existence of all entities
|
|
444
|
+
existing_ids = batch_exists?(sparql, entities)
|
|
445
|
+
|
|
446
|
+
to_create = []
|
|
447
|
+
to_update = []
|
|
448
|
+
|
|
449
|
+
entities.each do |entity|
|
|
450
|
+
if existing_ids.include?(entity.graph_id)
|
|
451
|
+
to_update << entity
|
|
452
|
+
else
|
|
453
|
+
to_create << entity
|
|
454
|
+
end
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Batch insert: combine new entity graphs into single INSERT DATA operations
|
|
458
|
+
unless to_create.empty?
|
|
459
|
+
to_create.each_slice(batch_size) do |batch|
|
|
460
|
+
combined_graph = RDF::Graph.new
|
|
461
|
+
combined_graph.name = RDF::URI(graph_name)
|
|
462
|
+
|
|
463
|
+
# Pre-collect known entities from all entities being created
|
|
464
|
+
known = {}
|
|
465
|
+
batch.each { |e| e.send(:collect_known_entities, e, known) }
|
|
466
|
+
|
|
467
|
+
batch.each do |entity|
|
|
468
|
+
entity.before_create_proc&.call(entity)
|
|
469
|
+
entity.send(:build_ttl_objekt, combined_graph, entity, [], validate_dependencies, known, skip_store_fetch: true)
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
sparql.insert_data(combined_graph, graph: combined_graph.name)
|
|
473
|
+
|
|
474
|
+
batch.each { |entity| entity.after_create_proc&.call(entity) }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Invalidate cache once for the entity type
|
|
478
|
+
Solis::Query.invalidate_cache_for(name)
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Updates still processed individually (DELETE/INSERT requires per-entity WHERE)
|
|
482
|
+
to_update.each do |entity|
|
|
483
|
+
data = entity.send(:properties_to_hash, entity)
|
|
484
|
+
entity.update(data, validate_dependencies, true, sparql)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
entities
|
|
488
|
+
end
|
|
489
|
+
|
|
387
490
|
# Check existence of multiple entities in a single SPARQL query
|
|
388
491
|
# Returns a Set of graph_ids that exist
|
|
389
492
|
def self.batch_exists?(sparql, entities)
|
|
@@ -471,20 +574,23 @@ values ?s {<#{self.graph_id}>}
|
|
|
471
574
|
end
|
|
472
575
|
|
|
473
576
|
def self.model(level = 0)
|
|
474
|
-
m = { type: self.name.tableize, attributes:
|
|
577
|
+
m = { type: self.name.tableize, attributes: [] }
|
|
475
578
|
self.metadata[:attributes].each do |attribute, attribute_metadata|
|
|
476
|
-
is_array = ((attribute_metadata[:maxcount].nil? || (attribute_metadata[:maxcount].to_i > 1)) && !attribute_metadata[:datatype].eql?(:lang_string))
|
|
477
|
-
attribute_name = is_array ? "#{attribute}[]" : attribute
|
|
478
|
-
attribute_name = attribute_metadata[:mincount].to_i > 0 ? "#{attribute_name}*" : attribute_name
|
|
479
579
|
if attribute_metadata.key?(:class) && !attribute_metadata[:class].nil? && attribute_metadata[:class].value =~ /#{self.graph_name}/ && level == 0
|
|
480
580
|
cm = self.graph.shape_as_model(self.metadata[:attributes][attribute][:datatype].to_s).model(level + 1)
|
|
481
|
-
m[:attributes][attribute_name.to_sym] = cm[:attributes]
|
|
482
|
-
else
|
|
483
|
-
m[:attributes][attribute_name.to_sym] = { description: attribute_metadata[:comment]&.value,
|
|
484
|
-
mandatory: (attribute_metadata[:mincount].to_i > 0),
|
|
485
|
-
data_type: attribute_metadata[:datatype] }
|
|
486
|
-
m[:attributes][attribute_name.to_sym][:order] = attribute_metadata[:order]&.value.to_i if attribute_metadata.key?(:order) && !attribute_metadata[:order].nil?
|
|
487
581
|
end
|
|
582
|
+
|
|
583
|
+
attribute_data = { name: attribute,
|
|
584
|
+
data_type: attribute_metadata[:datatype],
|
|
585
|
+
mandatory: (attribute_metadata[:mincount].to_i > 0),
|
|
586
|
+
repeatable: (attribute_metadata[:maxcount].to_i > 1 || attribute_metadata[:maxcount].nil?),
|
|
587
|
+
description: attribute_metadata[:comment]&.value
|
|
588
|
+
}
|
|
589
|
+
attribute_data[:order] = attribute_metadata[:order]&.value.to_i if attribute_metadata.key?(:order) && !attribute_metadata[:order].nil?
|
|
590
|
+
attribute_data[:group] = attribute_metadata[:group]&.value.gsub(graph_name,'').gsub(/Group$/,'') if attribute_metadata.key?(:group) && !attribute_metadata[:group].nil?
|
|
591
|
+
attribute_data[:attributes] = cm[:attributes] if cm && cm[:attributes]
|
|
592
|
+
|
|
593
|
+
m[:attributes] << attribute_data
|
|
488
594
|
end
|
|
489
595
|
|
|
490
596
|
m
|
|
@@ -543,6 +649,21 @@ values ?s {<#{self.graph_id}>}
|
|
|
543
649
|
|
|
544
650
|
private
|
|
545
651
|
|
|
652
|
+
# Walk the entity tree and collect all in-memory entities by UUID.
|
|
653
|
+
# Prevents redundant store fetches during recursive graph building.
|
|
654
|
+
def collect_known_entities(entity, collected = {})
|
|
655
|
+
uuid = entity.instance_variable_get("@id")
|
|
656
|
+
return collected if uuid.nil? || collected.key?(uuid)
|
|
657
|
+
collected[uuid] = entity
|
|
658
|
+
entity.class.metadata[:attributes].each do |attr, meta|
|
|
659
|
+
next if meta[:node_kind].nil?
|
|
660
|
+
val = entity.instance_variable_get("@#{attr}")
|
|
661
|
+
next if val.nil?
|
|
662
|
+
Array(val).each { |v| collect_known_entities(v, collected) if solis_model?(v) }
|
|
663
|
+
end
|
|
664
|
+
collected
|
|
665
|
+
end
|
|
666
|
+
|
|
546
667
|
# Helper method to check if an object is a Solis model
|
|
547
668
|
def solis_model?(obj)
|
|
548
669
|
obj.class.ancestors.include?(Solis::Model)
|
|
@@ -648,15 +769,15 @@ values ?s {<#{self.graph_id}>}
|
|
|
648
769
|
Set.new(results.map { |r| r[:o].to_s })
|
|
649
770
|
end
|
|
650
771
|
|
|
651
|
-
def as_graph(klass = self, resolve_all = true, known_entities = {})
|
|
772
|
+
def as_graph(klass = self, resolve_all = true, known_entities = {}, skip_store_fetch: false)
|
|
652
773
|
graph = RDF::Graph.new
|
|
653
774
|
graph.name = RDF::URI(self.class.graph_name)
|
|
654
|
-
id = build_ttl_objekt(graph, klass, [], resolve_all, known_entities)
|
|
775
|
+
id = build_ttl_objekt(graph, klass, [], resolve_all, known_entities, skip_store_fetch: skip_store_fetch)
|
|
655
776
|
|
|
656
777
|
graph
|
|
657
778
|
end
|
|
658
779
|
|
|
659
|
-
def build_ttl_objekt(graph, klass, hierarchy = [], resolve_all = true, known_entities = {})
|
|
780
|
+
def build_ttl_objekt(graph, klass, hierarchy = [], resolve_all = true, known_entities = {}, skip_store_fetch: false)
|
|
660
781
|
hierarchy.push("#{klass.class.name}(#{klass.instance_variables.include?(:@id) ? klass.instance_variable_get("@id") : ''})")
|
|
661
782
|
|
|
662
783
|
graph_name = self.class.graph_name
|
|
@@ -667,9 +788,9 @@ values ?s {<#{self.graph_id}>}
|
|
|
667
788
|
|
|
668
789
|
graph << [id, RDF::RDFV.type, klass_metadata[:target_class]]
|
|
669
790
|
|
|
670
|
-
# Use cached entity if available, otherwise query the store
|
|
791
|
+
# Use cached entity if available, otherwise query the store (unless skip_store_fetch)
|
|
671
792
|
original_klass = known_entities[uuid]
|
|
672
|
-
if original_klass.nil?
|
|
793
|
+
if original_klass.nil? && !skip_store_fetch
|
|
673
794
|
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
674
795
|
end
|
|
675
796
|
|
|
@@ -689,7 +810,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
689
810
|
known_entities[uuid] = original_klass
|
|
690
811
|
|
|
691
812
|
begin
|
|
692
|
-
make_graph(graph, hierarchy, id, original_klass, klass_metadata, resolve_all, known_entities)
|
|
813
|
+
make_graph(graph, hierarchy, id, original_klass, klass_metadata, resolve_all, known_entities, skip_store_fetch: skip_store_fetch)
|
|
693
814
|
rescue => e
|
|
694
815
|
Solis::LOGGER.error(e.message)
|
|
695
816
|
raise e
|
|
@@ -699,16 +820,16 @@ values ?s {<#{self.graph_id}>}
|
|
|
699
820
|
id
|
|
700
821
|
end
|
|
701
822
|
|
|
702
|
-
def make_graph(graph, hierarchy, id, klass, klass_metadata, resolve_all, known_entities = {})
|
|
823
|
+
def make_graph(graph, hierarchy, id, klass, klass_metadata, resolve_all, known_entities = {}, skip_store_fetch: false)
|
|
703
824
|
klass_metadata[:attributes].each do |attribute, metadata|
|
|
704
825
|
data = klass.instance_variable_get("@#{attribute}")
|
|
705
826
|
|
|
706
827
|
if data.nil? && metadata.key?(:mincount) && (metadata[:mincount].nil? || metadata[:mincount] > 0) && graph.query(RDF::Query.new({ attribute.to_sym => { RDF.type => metadata[:node] } })).size == 0
|
|
707
828
|
if data.nil?
|
|
708
829
|
uuid = id.value.split('/').last
|
|
709
|
-
# Use cached entity if available
|
|
830
|
+
# Use cached entity if available (skip store fetch for new entities)
|
|
710
831
|
original_klass = known_entities[uuid]
|
|
711
|
-
if original_klass.nil?
|
|
832
|
+
if original_klass.nil? && !skip_store_fetch
|
|
712
833
|
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
713
834
|
known_entities[uuid] = original_klass if original_klass
|
|
714
835
|
end
|
|
@@ -824,96 +945,6 @@ values ?s {<#{self.graph_id}>}
|
|
|
824
945
|
raise e
|
|
825
946
|
end
|
|
826
947
|
|
|
827
|
-
def build_ttl_objekt_old(graph, klass, hierarchy = [], resolve_all = true)
|
|
828
|
-
hierarchy.push("#{klass.class.name}(#{klass.instance_variables.include?(:@id) ? klass.instance_variable_get("@id") : ''})")
|
|
829
|
-
sparql_endpoint = self.class.sparql_endpoint
|
|
830
|
-
if klass.instance_variables.include?(:@id) && hierarchy.length > 1
|
|
831
|
-
unless sparql_endpoint.nil?
|
|
832
|
-
existing_klass = klass.query.filter({ filters: { id: [klass.instance_variable_get("@id")] } }).find_all { |f| f.id == klass.instance_variable_get("@id") }
|
|
833
|
-
if !existing_klass.nil? && !existing_klass.empty? && existing_klass.first.is_a?(klass.class)
|
|
834
|
-
klass = existing_klass.first
|
|
835
|
-
end
|
|
836
|
-
end
|
|
837
|
-
end
|
|
838
|
-
|
|
839
|
-
uuid = klass.instance_variable_get("@id") || SecureRandom.uuid
|
|
840
|
-
id = RDF::URI("#{self.class.graph_name}#{klass.class.name.tableize}/#{uuid}")
|
|
841
|
-
graph << [id, RDF::RDFV.type, klass.class.metadata[:target_class]]
|
|
842
|
-
|
|
843
|
-
klass.class.metadata[:attributes].each do |attribute, metadata|
|
|
844
|
-
data = klass.instance_variable_get("@#{attribute}")
|
|
845
|
-
if data.nil? && metadata[:datatype_rdf].eql?('http://www.w3.org/2001/XMLSchema#boolean')
|
|
846
|
-
data = false
|
|
847
|
-
end
|
|
848
|
-
|
|
849
|
-
if metadata[:datatype_rdf].eql?("http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON")
|
|
850
|
-
data = data.to_json
|
|
851
|
-
end
|
|
852
|
-
|
|
853
|
-
if data.nil? && metadata[:mincount] > 0
|
|
854
|
-
raise Solis::Error::InvalidAttributeError, "#{hierarchy.join('.')}.#{attribute} min=#{metadata[:mincount]} and max=#{metadata[:maxcount]}"
|
|
855
|
-
end
|
|
856
|
-
|
|
857
|
-
next if data.nil? || ([Hash, Array, String].include?(data.class) && data&.empty?)
|
|
858
|
-
|
|
859
|
-
data = [data] unless data.is_a?(Array)
|
|
860
|
-
model = nil
|
|
861
|
-
model = klass.class.graph.shape_as_model(klass.class.metadata[:attributes][attribute][:datatype].to_s) unless klass.class.metadata[:attributes][attribute][:node_kind].nil?
|
|
862
|
-
|
|
863
|
-
data.each do |d|
|
|
864
|
-
original_d = d
|
|
865
|
-
if model
|
|
866
|
-
target_node = model.metadata[:target_node].value.split('/').last.gsub(/Shape$/, '')
|
|
867
|
-
if model.ancestors[0..model.ancestors.find_index(Solis::Model) - 1].map { |m| m.name }.include?(target_node)
|
|
868
|
-
parent_model = model.graph.shape_as_model(target_node)
|
|
869
|
-
end
|
|
870
|
-
end
|
|
871
|
-
|
|
872
|
-
if model && d.is_a?(Hash)
|
|
873
|
-
model_instance = model.descendants.map { |m| m&.new(d) rescue nil }.compact.first || nil
|
|
874
|
-
model_instance = model.new(d) if model_instance.nil?
|
|
875
|
-
|
|
876
|
-
if resolve_all
|
|
877
|
-
d = build_ttl_objekt(graph, model_instance, hierarchy, false, known_entities)
|
|
878
|
-
else
|
|
879
|
-
real_model = known_entities[model_instance.id] || model_instance.query.filter({ filters: { id: model_instance.id } }).find_all { |f| f.id == model_instance.id }&.first
|
|
880
|
-
d = RDF::URI("#{self.class.graph_name}#{real_model ? real_model.class.name.tableize : model_instance.class.name.tableize}/#{model_instance.id}")
|
|
881
|
-
end
|
|
882
|
-
elsif model && d.is_a?(model)
|
|
883
|
-
if resolve_all
|
|
884
|
-
if parent_model
|
|
885
|
-
model_instance = parent_model.new({ id: d.id })
|
|
886
|
-
d = build_ttl_objekt(graph, model_instance, hierarchy, false, known_entities)
|
|
887
|
-
else
|
|
888
|
-
d = build_ttl_objekt(graph, d, hierarchy, false, known_entities)
|
|
889
|
-
end
|
|
890
|
-
else
|
|
891
|
-
real_model = known_entities[d.id] || model.new.query.filter({ filters: { id: d.id } }).find_all { |f| f.id == d.id }&.first
|
|
892
|
-
d = RDF::URI("#{self.class.graph_name}#{real_model ? real_model.class.name.tableize : model.name.tableize}/#{d.id}")
|
|
893
|
-
end
|
|
894
|
-
else
|
|
895
|
-
datatype = RDF::Vocabulary.find_term(metadata[:datatype_rdf] || metadata[:node])
|
|
896
|
-
if datatype && datatype.datatype?
|
|
897
|
-
d = if metadata[:datatype_rdf].eql?('http://www.w3.org/1999/02/22-rdf-syntax-ns#langString')
|
|
898
|
-
RDF::Literal.new(d, language: self.class.language)
|
|
899
|
-
else
|
|
900
|
-
if metadata[:datatype_rdf].eql?('http://www.w3.org/2001/XMLSchema#anyURI')
|
|
901
|
-
RDF::Literal.new(d.to_s, datatype: RDF::XSD.anyURI)
|
|
902
|
-
else
|
|
903
|
-
RDF::Literal.new(d, datatype: datatype)
|
|
904
|
-
end
|
|
905
|
-
end
|
|
906
|
-
d = (d.object.value rescue d.object) unless d.valid?
|
|
907
|
-
end
|
|
908
|
-
end
|
|
909
|
-
|
|
910
|
-
graph << [id, RDF::URI("#{self.class.graph_name}#{attribute}"), d]
|
|
911
|
-
end
|
|
912
|
-
end
|
|
913
|
-
hierarchy.pop
|
|
914
|
-
id
|
|
915
|
-
end
|
|
916
|
-
|
|
917
948
|
def properties_to_hash(model)
|
|
918
949
|
n = {}
|
|
919
950
|
model.class.metadata[:attributes].each_key do |m|
|
data/lib/solis/query.rb
CHANGED
|
@@ -58,6 +58,30 @@ module Solis
|
|
|
58
58
|
Solis::Options.instance.get.key?(:graphs) ? Solis::Options.instance.get[:graphs].select{|s| s['type'].eql?(:main)}&.first['name'] : ''
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# Shared class-level query cache to ensure consistent reads/writes/invalidations
|
|
62
|
+
def self.shared_query_cache
|
|
63
|
+
cache_dir = File.absolute_path(Solis::Options.instance.get[:cache])
|
|
64
|
+
@shared_query_cache ||= Moneta.new(:HashFile, dir: cache_dir)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Reset the shared cache (useful when config changes, e.g., in tests)
|
|
68
|
+
def self.reset_shared_query_cache!
|
|
69
|
+
@shared_query_cache = nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Invalidate all cached query results for a given model type.
|
|
73
|
+
def self.invalidate_cache_for(model_class_name, cache_dir = nil)
|
|
74
|
+
cache = shared_query_cache
|
|
75
|
+
tag_key = "TAG:#{model_class_name}"
|
|
76
|
+
if cache.key?(tag_key)
|
|
77
|
+
cache[tag_key].each { |key| cache.delete(key) }
|
|
78
|
+
cache.delete(tag_key)
|
|
79
|
+
Solis::LOGGER.info("CACHE: invalidated entries for #{model_class_name}") if ConfigFile[:debug]
|
|
80
|
+
end
|
|
81
|
+
rescue StandardError => e
|
|
82
|
+
Solis::LOGGER.warn("CACHE: invalidation failed for #{model_class_name}: #{e.message}")
|
|
83
|
+
end
|
|
84
|
+
|
|
61
85
|
def initialize(model)
|
|
62
86
|
@construct_cache = File.absolute_path(Solis::Options.instance.get[:cache])
|
|
63
87
|
@model = model
|
|
@@ -73,7 +97,7 @@ module Solis
|
|
|
73
97
|
@sort = 'ORDER BY ?s'
|
|
74
98
|
@sort_select = ''
|
|
75
99
|
@language = Graphiti.context[:object]&.language || Solis::Options.instance.get[:language] || 'en'
|
|
76
|
-
@query_cache =
|
|
100
|
+
@query_cache = self.class.shared_query_cache
|
|
77
101
|
end
|
|
78
102
|
|
|
79
103
|
def each(&block)
|
|
@@ -135,6 +159,16 @@ module Solis
|
|
|
135
159
|
|
|
136
160
|
private
|
|
137
161
|
|
|
162
|
+
# Track a cache key under its model type tag for targeted invalidation
|
|
163
|
+
def track_cache_key(query_key)
|
|
164
|
+
tag_key = "TAG:#{@model.model_class_name}"
|
|
165
|
+
existing_keys = @query_cache.key?(tag_key) ? @query_cache[tag_key] : []
|
|
166
|
+
unless existing_keys.include?(query_key)
|
|
167
|
+
existing_keys << query_key
|
|
168
|
+
@query_cache[tag_key] = existing_keys
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
138
172
|
def model_construct?
|
|
139
173
|
construct_name = @model.model_class_name.tableize.singularize rescue @model.class.name.tableize.singularize
|
|
140
174
|
File.exist?("#{ConfigFile.path}/constructs/#{construct_name}.sparql")
|
|
@@ -208,6 +242,9 @@ order by ?s
|
|
|
208
242
|
Solis::LOGGER.info("CACHE: to #{query_key}") if ConfigFile[:debug]
|
|
209
243
|
end
|
|
210
244
|
|
|
245
|
+
# Always ensure the cache key is tracked under its model type tag
|
|
246
|
+
track_cache_key(query_key)
|
|
247
|
+
|
|
211
248
|
result
|
|
212
249
|
rescue StandardError => e
|
|
213
250
|
Solis::LOGGER.error(e.message)
|
|
@@ -52,28 +52,20 @@ module Solis
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def read_sheets(key, spreadsheet_id, options)
|
|
55
|
-
data = nil
|
|
56
55
|
prefixes = options[:prefixes] || nil
|
|
57
56
|
metadata = options[:metadata] || nil
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
Solis::LOGGER.info("from source #{spreadsheet_id}")
|
|
67
|
-
session = SimpleSheets.new(key, spreadsheet_id)
|
|
68
|
-
session.key = key
|
|
69
|
-
sheets = {}
|
|
70
|
-
session.worksheets.each do |worksheet|
|
|
71
|
-
sheet = ::Sheet.new(worksheet)
|
|
72
|
-
sheets[sheet.title] = sheet
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
validate(sheets, prefixes, metadata)
|
|
58
|
+
Solis::LOGGER.info("from source #{spreadsheet_id}")
|
|
59
|
+
session = SimpleSheets.new(key, spreadsheet_id)
|
|
60
|
+
session.key = key
|
|
61
|
+
sheets = {}
|
|
62
|
+
session.worksheets.each do |worksheet|
|
|
63
|
+
sheet = ::Sheet.new(worksheet)
|
|
64
|
+
sheets[sheet.title] = sheet
|
|
76
65
|
end
|
|
66
|
+
|
|
67
|
+
validate(sheets, prefixes, metadata)
|
|
68
|
+
|
|
77
69
|
sheets
|
|
78
70
|
end
|
|
79
71
|
|
|
@@ -118,6 +110,7 @@ module Solis
|
|
|
118
110
|
|
|
119
111
|
entities.store(e['name'].to_sym, { description: e['description'],
|
|
120
112
|
order: e['order'],
|
|
113
|
+
category: e['categories'],
|
|
121
114
|
plural: e['nameplural'],
|
|
122
115
|
label: e['name'].to_s.strip,
|
|
123
116
|
sub_class_of: e['subclassof'].nil? || e['subclassof'].empty? ? [] : [e['subclassof']],
|
|
@@ -137,12 +130,6 @@ module Solis
|
|
|
137
130
|
metadata: ontology_metadata
|
|
138
131
|
}
|
|
139
132
|
|
|
140
|
-
cache_dir = ConfigFile.include?(:cache) ? ConfigFile[:cache] : '/tmp'
|
|
141
|
-
# ::File.open("#{::File.absolute_path(cache_dir)}/#{spreadsheet_id}.json", 'wb') do |f|
|
|
142
|
-
::File.open("#{::File.absolute_path(cache_dir)}/#{sheet_id}.json", 'wb') do |f|
|
|
143
|
-
f.puts data.to_json
|
|
144
|
-
end
|
|
145
|
-
|
|
146
133
|
data
|
|
147
134
|
rescue StandardError => e
|
|
148
135
|
raise Solis::Error::GeneralError, e.message
|
|
@@ -180,6 +167,7 @@ module Solis
|
|
|
180
167
|
cardinality: { min: min_max['min'], max: min_max['max'] },
|
|
181
168
|
same_as: p['sameas'],
|
|
182
169
|
order: p['order'],
|
|
170
|
+
category: p['categories'],
|
|
183
171
|
description: p['description']
|
|
184
172
|
}
|
|
185
173
|
|
|
@@ -302,6 +290,8 @@ hide empty members
|
|
|
302
290
|
node = target_class
|
|
303
291
|
end
|
|
304
292
|
|
|
293
|
+
groups = {}
|
|
294
|
+
|
|
305
295
|
out += %(
|
|
306
296
|
#{graph_prefix}:#{entity_name}Shape
|
|
307
297
|
a #{shacl_prefix}:NodeShape ;
|
|
@@ -322,11 +312,19 @@ hide empty members
|
|
|
322
312
|
min_count = property_metadata[:cardinality][:min].strip
|
|
323
313
|
max_count = property_metadata[:cardinality][:max].strip
|
|
324
314
|
order = property_metadata.key?(:order) && property_metadata[:order] ? property_metadata[:order]&.strip : nil
|
|
315
|
+
category = property_metadata.key?(:category) && property_metadata[:category] ? property_metadata[:category]&.strip : nil
|
|
316
|
+
category = category.nil? || category.empty? ? nil : category
|
|
317
|
+
|
|
318
|
+
unless category.nil?
|
|
319
|
+
group = "#{category.classify}Group"
|
|
320
|
+
groups[group] = category unless groups.key?(group)
|
|
321
|
+
end
|
|
325
322
|
|
|
326
323
|
if datatype =~ /^#{graph_prefix}:/ || datatype =~ /^<#{graph_name}/
|
|
327
324
|
out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
|
|
328
325
|
#{shacl_prefix}:name "#{attribute}" ;
|
|
329
326
|
#{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
|
|
327
|
+
#{category.nil? ? '' : "\n #{shacl_prefix}:group #{graph_prefix}:#{group} ;"}
|
|
330
328
|
#{shacl_prefix}:nodeKind #{shacl_prefix}:IRI ;
|
|
331
329
|
#{shacl_prefix}:class #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
|
|
332
330
|
] ;
|
|
@@ -344,6 +342,7 @@ hide empty members
|
|
|
344
342
|
out += %( #{shacl_prefix}:property [#{shacl_prefix}:path #{path} ;
|
|
345
343
|
#{shacl_prefix}:name "#{attribute}";
|
|
346
344
|
#{shacl_prefix}:description "#{description}" ;#{order.nil? ? '' : "\n #{shacl_prefix}:order #{order} ;"}
|
|
345
|
+
#{category.nil? ? '' : "\n #{shacl_prefix}:group #{graph_prefix}:#{group} ;"}
|
|
347
346
|
#{shacl_prefix}:datatype #{datatype} ;#{min_count =~ /\d+/ ? "\n #{shacl_prefix}:minCount #{min_count} ;" : ''}#{max_count =~ /\d+/ ? "\n #{shacl_prefix}:maxCount #{max_count} ;" : ''}
|
|
348
347
|
] ;
|
|
349
348
|
)
|
|
@@ -359,6 +358,15 @@ hide empty members
|
|
|
359
358
|
end
|
|
360
359
|
end
|
|
361
360
|
out += ".\n"
|
|
361
|
+
|
|
362
|
+
groups.each do |group, category|
|
|
363
|
+
out += %(
|
|
364
|
+
#{graph_prefix}:#{group}
|
|
365
|
+
a sh:PropertyGroup ;
|
|
366
|
+
rdfs:label "#{category}" .
|
|
367
|
+
|
|
368
|
+
)
|
|
369
|
+
end
|
|
362
370
|
end
|
|
363
371
|
end
|
|
364
372
|
out
|
|
@@ -716,6 +724,14 @@ hide empty members
|
|
|
716
724
|
end
|
|
717
725
|
end
|
|
718
726
|
|
|
727
|
+
cache_dir = ConfigFile.include?(:cache) ? ConfigFile[:cache] : '/tmp'
|
|
728
|
+
cache_file = "#{::File.absolute_path(cache_dir)}/#{spreadsheet_id}.json"
|
|
729
|
+
|
|
730
|
+
if options[:from_cache] && ::File.exist?(cache_file)
|
|
731
|
+
Solis::LOGGER.info("Loading cached result from #{cache_file}")
|
|
732
|
+
return JSON.parse(::File.read(cache_file), symbolize_names: true)
|
|
733
|
+
end
|
|
734
|
+
|
|
719
735
|
sheet_data = read_sheets(key, spreadsheet_id, options)
|
|
720
736
|
prefixes = sheet_data['_PREFIXES']
|
|
721
737
|
metadata = sheet_data['_METADATA']
|
|
@@ -736,10 +752,6 @@ hide empty members
|
|
|
736
752
|
{ sheet_url: reference['sheeturl'], description: reference['description'] }
|
|
737
753
|
end
|
|
738
754
|
|
|
739
|
-
cache_dir = ConfigFile.include?(:cache) ? ConfigFile[:cache] : '/tmp'
|
|
740
|
-
::File.open("#{::File.absolute_path(cache_dir)}/#{spreadsheet_id}.json", 'wb') do |f|
|
|
741
|
-
f.puts references.to_json
|
|
742
|
-
end
|
|
743
755
|
else
|
|
744
756
|
references = sheet_data
|
|
745
757
|
end
|
|
@@ -778,7 +790,16 @@ hide empty members
|
|
|
778
790
|
Solis::LOGGER.info('Generating JSON SCHEMA')
|
|
779
791
|
json_schema = build_json_schema(shacl)
|
|
780
792
|
|
|
781
|
-
{ inflections: inflections, shacl: shacl, schema: schema, plantuml: plantuml, json_schema: json_schema }
|
|
793
|
+
result = { inflections: inflections, shacl: shacl, schema: schema, plantuml: plantuml, json_schema: json_schema }
|
|
794
|
+
|
|
795
|
+
begin
|
|
796
|
+
::File.open(cache_file, 'wb') { |f| f.puts result.to_json }
|
|
797
|
+
Solis::LOGGER.info("Cached result to #{cache_file}")
|
|
798
|
+
rescue StandardError => e
|
|
799
|
+
Solis::LOGGER.warn("Failed to write cache: #{e.message}")
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
result
|
|
782
803
|
end
|
|
783
804
|
end
|
|
784
805
|
end
|
data/lib/solis/shape.rb
CHANGED
|
@@ -104,6 +104,7 @@ module Solis
|
|
|
104
104
|
attribute_class = solution.attributeClass if solution.bound?(:attributeClass)
|
|
105
105
|
attribute_comment = solution.attributeComment if solution.bound?(:attributeComment)
|
|
106
106
|
attribute_order = solution.attributeOrder if solution.bound?(:attributeOrder)
|
|
107
|
+
attribute_group = solution.attributeGroup if solution.bound?(:attributeGroup)
|
|
107
108
|
|
|
108
109
|
attribute_max_count = 1 if solution.bound?(:attributeUniqueLang) && solution.attributeUniqueLang.value.eql?('true')
|
|
109
110
|
|
|
@@ -125,6 +126,7 @@ module Solis
|
|
|
125
126
|
mincount: attribute_min_count,
|
|
126
127
|
maxcount: attribute_max_count,
|
|
127
128
|
order: attribute_order,
|
|
129
|
+
group: attribute_group,
|
|
128
130
|
node: attribute_node,
|
|
129
131
|
node_kind: attribute_node_kind,
|
|
130
132
|
class: attribute_class,
|
|
@@ -142,7 +144,7 @@ PREFIX rdfv: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
|
|
|
142
144
|
|
|
143
145
|
SELECT ?targetClass ?targetNode ?comment ?className ?attributePath ?attributeName ?attributeDatatype
|
|
144
146
|
?attributeMinCount ?attributeMaxCount ?attributeOr ?attributeClass
|
|
145
|
-
?attributeNode ?attributeNodeKind ?attributeComment ?attributeOrder ?attributeUniqueLang ?o
|
|
147
|
+
?attributeNode ?attributeNodeKind ?attributeComment ?attributeOrder ?attributeGroup ?attributeUniqueLang ?o
|
|
146
148
|
WHERE {
|
|
147
149
|
|
|
148
150
|
?s a sh:NodeShape;
|
|
@@ -162,6 +164,7 @@ WHERE {
|
|
|
162
164
|
OPTIONAL{ ?attributes sh:node ?attributeNode } .
|
|
163
165
|
OPTIONAL{ ?attributes sh:description ?attributeComment } .
|
|
164
166
|
OPTIONAL{ ?attributes sh:order ?attributeOrder } .
|
|
167
|
+
OPTIONAL{ ?attributes sh:group ?attributeGroup } .
|
|
165
168
|
OPTIONAL{ ?attributes sh:uniqueLang ?attributeUniqueLang } .
|
|
166
169
|
}.
|
|
167
170
|
}
|
|
@@ -182,6 +185,7 @@ WHERE {
|
|
|
182
185
|
"mincount": 1,
|
|
183
186
|
"maxcount": 1,
|
|
184
187
|
"order": nil,
|
|
188
|
+
"group": nil,
|
|
185
189
|
"node": nil,
|
|
186
190
|
"node_kind": nil,
|
|
187
191
|
"class": nil,
|
data/lib/solis/version.rb
CHANGED