solis 0.115.0 → 0.116.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 +153 -123
- data/lib/solis/query.rb +38 -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: affbb3891268851909a6a35684c11a36f6e67d617dea1e977015124ca0bd8118
|
|
4
|
+
data.tar.gz: 920ad82dc52d0bdefe3bd1e08b631e6907d6f13952f5573fc3bb8f392b03521d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b42751289ba605e41d7e02c85a6a7df99a41a80237ca282a4502d3c4a48198ba8057a35b884a51914b828657a88b1d6c3899d3dd703f10c30a0b9ff779dd364
|
|
7
|
+
data.tar.gz: 8d627312e95e719ab5ddabbd8bdbd0f964aa0a98ed6372df07f18462ecf69a9bdd4ce1e53f21b29392ad450a5874ebfb6bc5b510bc2adbb5fe634a1edf325cc0
|
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,22 @@ 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
|
+
description: attribute_metadata[:comment]&.value
|
|
587
|
+
}
|
|
588
|
+
attribute_data[:order] = attribute_metadata[:order]&.value.to_i if attribute_metadata.key?(:order) && !attribute_metadata[:order].nil?
|
|
589
|
+
attribute_data[:category] = attribute_metadata[:category]&.value if attribute_metadata.key?(:category) && !attribute_metadata[:category].nil?
|
|
590
|
+
attribute_data[:attributes] = cm[:attributes] if cm && cm[:attributes]
|
|
591
|
+
|
|
592
|
+
m[:attributes] << attribute_data
|
|
488
593
|
end
|
|
489
594
|
|
|
490
595
|
m
|
|
@@ -543,6 +648,21 @@ values ?s {<#{self.graph_id}>}
|
|
|
543
648
|
|
|
544
649
|
private
|
|
545
650
|
|
|
651
|
+
# Walk the entity tree and collect all in-memory entities by UUID.
|
|
652
|
+
# Prevents redundant store fetches during recursive graph building.
|
|
653
|
+
def collect_known_entities(entity, collected = {})
|
|
654
|
+
uuid = entity.instance_variable_get("@id")
|
|
655
|
+
return collected if uuid.nil? || collected.key?(uuid)
|
|
656
|
+
collected[uuid] = entity
|
|
657
|
+
entity.class.metadata[:attributes].each do |attr, meta|
|
|
658
|
+
next if meta[:node_kind].nil?
|
|
659
|
+
val = entity.instance_variable_get("@#{attr}")
|
|
660
|
+
next if val.nil?
|
|
661
|
+
Array(val).each { |v| collect_known_entities(v, collected) if solis_model?(v) }
|
|
662
|
+
end
|
|
663
|
+
collected
|
|
664
|
+
end
|
|
665
|
+
|
|
546
666
|
# Helper method to check if an object is a Solis model
|
|
547
667
|
def solis_model?(obj)
|
|
548
668
|
obj.class.ancestors.include?(Solis::Model)
|
|
@@ -648,15 +768,15 @@ values ?s {<#{self.graph_id}>}
|
|
|
648
768
|
Set.new(results.map { |r| r[:o].to_s })
|
|
649
769
|
end
|
|
650
770
|
|
|
651
|
-
def as_graph(klass = self, resolve_all = true, known_entities = {})
|
|
771
|
+
def as_graph(klass = self, resolve_all = true, known_entities = {}, skip_store_fetch: false)
|
|
652
772
|
graph = RDF::Graph.new
|
|
653
773
|
graph.name = RDF::URI(self.class.graph_name)
|
|
654
|
-
id = build_ttl_objekt(graph, klass, [], resolve_all, known_entities)
|
|
774
|
+
id = build_ttl_objekt(graph, klass, [], resolve_all, known_entities, skip_store_fetch: skip_store_fetch)
|
|
655
775
|
|
|
656
776
|
graph
|
|
657
777
|
end
|
|
658
778
|
|
|
659
|
-
def build_ttl_objekt(graph, klass, hierarchy = [], resolve_all = true, known_entities = {})
|
|
779
|
+
def build_ttl_objekt(graph, klass, hierarchy = [], resolve_all = true, known_entities = {}, skip_store_fetch: false)
|
|
660
780
|
hierarchy.push("#{klass.class.name}(#{klass.instance_variables.include?(:@id) ? klass.instance_variable_get("@id") : ''})")
|
|
661
781
|
|
|
662
782
|
graph_name = self.class.graph_name
|
|
@@ -667,9 +787,9 @@ values ?s {<#{self.graph_id}>}
|
|
|
667
787
|
|
|
668
788
|
graph << [id, RDF::RDFV.type, klass_metadata[:target_class]]
|
|
669
789
|
|
|
670
|
-
# Use cached entity if available, otherwise query the store
|
|
790
|
+
# Use cached entity if available, otherwise query the store (unless skip_store_fetch)
|
|
671
791
|
original_klass = known_entities[uuid]
|
|
672
|
-
if original_klass.nil?
|
|
792
|
+
if original_klass.nil? && !skip_store_fetch
|
|
673
793
|
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
674
794
|
end
|
|
675
795
|
|
|
@@ -689,7 +809,7 @@ values ?s {<#{self.graph_id}>}
|
|
|
689
809
|
known_entities[uuid] = original_klass
|
|
690
810
|
|
|
691
811
|
begin
|
|
692
|
-
make_graph(graph, hierarchy, id, original_klass, klass_metadata, resolve_all, known_entities)
|
|
812
|
+
make_graph(graph, hierarchy, id, original_klass, klass_metadata, resolve_all, known_entities, skip_store_fetch: skip_store_fetch)
|
|
693
813
|
rescue => e
|
|
694
814
|
Solis::LOGGER.error(e.message)
|
|
695
815
|
raise e
|
|
@@ -699,16 +819,16 @@ values ?s {<#{self.graph_id}>}
|
|
|
699
819
|
id
|
|
700
820
|
end
|
|
701
821
|
|
|
702
|
-
def make_graph(graph, hierarchy, id, klass, klass_metadata, resolve_all, known_entities = {})
|
|
822
|
+
def make_graph(graph, hierarchy, id, klass, klass_metadata, resolve_all, known_entities = {}, skip_store_fetch: false)
|
|
703
823
|
klass_metadata[:attributes].each do |attribute, metadata|
|
|
704
824
|
data = klass.instance_variable_get("@#{attribute}")
|
|
705
825
|
|
|
706
826
|
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
827
|
if data.nil?
|
|
708
828
|
uuid = id.value.split('/').last
|
|
709
|
-
# Use cached entity if available
|
|
829
|
+
# Use cached entity if available (skip store fetch for new entities)
|
|
710
830
|
original_klass = known_entities[uuid]
|
|
711
|
-
if original_klass.nil?
|
|
831
|
+
if original_klass.nil? && !skip_store_fetch
|
|
712
832
|
original_klass = klass.query.filter({ filters: { id: [uuid] } }).find_all { |f| f.id == uuid }.first || nil
|
|
713
833
|
known_entities[uuid] = original_klass if original_klass
|
|
714
834
|
end
|
|
@@ -824,96 +944,6 @@ values ?s {<#{self.graph_id}>}
|
|
|
824
944
|
raise e
|
|
825
945
|
end
|
|
826
946
|
|
|
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
947
|
def properties_to_hash(model)
|
|
918
948
|
n = {}
|
|
919
949
|
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)
|
data/lib/solis/version.rb
CHANGED