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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c9039f8bba386333b11237b9914c7fdeee57ad8094ea57058815f2fc008d4a7
4
- data.tar.gz: ea7269d0380b39c1a88a7d064c503c972194a548441c6c32459ab4ca7187cab8
3
+ metadata.gz: affbb3891268851909a6a35684c11a36f6e67d617dea1e977015124ca0bd8118
4
+ data.tar.gz: 920ad82dc52d0bdefe3bd1e08b631e6907d6f13952f5573fc3bb8f392b03521d
5
5
  SHA512:
6
- metadata.gz: 17d2aa3f39b2bee0cb267996467ba8a0fe08d433e3e63c9dbab3df8624cac6ebf45703bc90c606f3c7d90d4c0ab12b72c3794056c11481f008141c456fe27675
7
- data.tar.gz: 0e9faa327c21a51aa3413ed0d90c20f781186c360ae4e33c3d2b6432c1e9779e19c100008197b4fbed8969eefecdf2d9d5776cf18ab72872f0dff95ac334160b
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
- if entity_exists
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
- graph = as_graph(self, validate_dependencies)
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
- def update(data, validate_dependencies = true, top_level = true, sparql_client = nil, patch: false)
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.update(embedded_data, validate_dependencies, false)
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 re-fetching during graph building
332
- known_entities = { id => original_klass }
333
- delete_graph = as_graph(original_klass, false, known_entities)
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
- insert_graph = as_graph(updated_klass, true, known_entities)
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 = Moneta.new(:HashFile, dir: @construct_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
@@ -1,3 +1,3 @@
1
1
  module Solis
2
- VERSION = "0.115.0"
2
+ VERSION = "0.116.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.115.0
4
+ version: 0.116.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mehmet Celik