Module: Legion::Apollo::Local::Graph
- Extended by:
- Logging::Helper
- Defined in:
- lib/legion/apollo/local/graph.rb
Overview
Entity-relationship graph layer backed by local SQLite tables. Entities are schema-flexible (type + name + domain + JSON attributes). Relationships are directional typed edges between two entities. Graph traversal expands one frontier batch per depth to avoid per-node queries.
Constant Summary collapse
- VALID_RELATION_TYPES =
rubocop:disable Metrics/ModuleLength
%w[AFFECTS OWNED_BY DEPENDS_ON RELATED_TO SUPERSEDES].freeze
Class Method Summary collapse
-
.create_entity(type:, name:, domain: nil, attributes: {}) ⇒ Object
— Entity CRUD —.
-
.create_relationship(source_id:, target_id:, relation_type:, attributes: {}) ⇒ Object
— Relationship CRUD —.
- .delete_entity(id:) ⇒ Object
- .delete_relationship(id:) ⇒ Object
-
.find_entities_by_name(name:, limit: 50) ⇒ Object
rubocop:disable Metrics/MethodLength.
-
.find_entities_by_type(type:, limit: 50) ⇒ Object
rubocop:disable Metrics/MethodLength.
- .find_entity(id:) ⇒ Object
-
.find_relationships(entity_id:, relation_type: nil, direction: :outbound) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize.
-
.traverse(entity_id:, relation_type: nil, depth: 3, direction: :outbound) ⇒ Hash
Traverse from an entity following edges of the given relation_type.
-
.update_entity(id:, **fields) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize.
Class Method Details
.create_entity(type:, name:, domain: nil, attributes: {}) ⇒ Object
— Entity CRUD —
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/legion/apollo/local/graph.rb', line 21 def create_entity(type:, name:, domain: nil, attributes: {}) # rubocop:disable Metrics/MethodLength now = id = db.transaction do db[:local_entities].insert( entity_type: type.to_s, name: name.to_s, domain: domain&.to_s, attributes: encode(attributes), created_at: now, updated_at: now ) end log.info { "Apollo::Local::Graph created entity id=#{id} type=#{type} name=#{name}" } { success: true, id: id } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.create_entity', entity_type: type, name: name ) { success: false, error: e. } end |
.create_relationship(source_id:, target_id:, relation_type:, attributes: {}) ⇒ Object
— Relationship CRUD —
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/legion/apollo/local/graph.rb', line 122 def create_relationship(source_id:, target_id:, relation_type:, attributes: {}) # rubocop:disable Metrics/MethodLength normalized_relation_type = normalize_relation_type(relation_type) return invalid_relation_type_error(relation_type) unless normalized_relation_type existing = existing_relationship(source_id, target_id, normalized_relation_type) return deduplicated_relationship(existing) if existing id = insert_relationship(source_id, target_id, normalized_relation_type, attributes) log.info do "Apollo::Local::Graph created relationship id=#{id} source_id=#{source_id} " \ "target_id=#{target_id} relation_type=#{normalized_relation_type}" end { success: true, id: id } rescue Sequel::UniqueConstraintViolation => e duplicate = handle_duplicate_relationship(source_id, target_id, normalized_relation_type) return duplicate if duplicate handle_exception( e, level: :error, operation: 'apollo.local.graph.create_relationship', source_id: source_id, target_id: target_id, relation_type: normalized_relation_type ) { success: false, error: e. } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.create_relationship', source_id: source_id, target_id: target_id, relation_type: relation_type ) { success: false, error: e. } end |
.delete_entity(id:) ⇒ Object
109 110 111 112 113 114 115 116 117 118 |
# File 'lib/legion/apollo/local/graph.rb', line 109 def delete_entity(id:) result = delete_entity_transaction(id) return result unless result[:success] log.info { "Apollo::Local::Graph deleted entity id=#{id}" } result rescue Sequel::Error => e handle_exception(e, level: :error, operation: 'apollo.local.graph.delete_entity', entity_id: id) { success: false, error: e. } end |
.delete_relationship(id:) ⇒ Object
185 186 187 188 189 190 191 192 193 194 |
# File 'lib/legion/apollo/local/graph.rb', line 185 def delete_relationship(id:) count = db.transaction { db[:local_relationships].where(id: id).delete } return { success: false, error: :not_found } if count.zero? log.info { "Apollo::Local::Graph deleted relationship id=#{id}" } { success: true, id: id } rescue Sequel::Error => e handle_exception(e, level: :error, operation: 'apollo.local.graph.delete_relationship', relationship_id: id) { success: false, error: e. } end |
.find_entities_by_name(name:, limit: 50) ⇒ Object
rubocop:disable Metrics/MethodLength
72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/legion/apollo/local/graph.rb', line 72 def find_entities_by_name(name:, limit: 50) # rubocop:disable Metrics/MethodLength rows = db[:local_entities].where(name: name.to_s).limit(limit).all log.debug { "Apollo::Local::Graph found entities name=#{name} count=#{rows.size}" } { success: true, entities: rows.map { |r| decode_entity(r) }, count: rows.size } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.find_entities_by_name', name: name, limit: limit ) { success: false, error: e. } end |
.find_entities_by_type(type:, limit: 50) ⇒ Object
rubocop:disable Metrics/MethodLength
57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/legion/apollo/local/graph.rb', line 57 def find_entities_by_type(type:, limit: 50) # rubocop:disable Metrics/MethodLength rows = db[:local_entities].where(entity_type: type.to_s).limit(limit).all log.debug { "Apollo::Local::Graph found entities type=#{type} count=#{rows.size}" } { success: true, entities: rows.map { |r| decode_entity(r) }, count: rows.size } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.find_entities_by_type', entity_type: type, limit: limit ) { success: false, error: e. } end |
.find_entity(id:) ⇒ Object
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/legion/apollo/local/graph.rb', line 46 def find_entity(id:) row = db[:local_entities].where(id: id).first return { success: false, error: :not_found } unless row log.debug { "Apollo::Local::Graph found entity id=#{id}" } { success: true, entity: decode_entity(row) } rescue Sequel::Error => e handle_exception(e, level: :error, operation: 'apollo.local.graph.find_entity', entity_id: id) { success: false, error: e. } end |
.find_relationships(entity_id:, relation_type: nil, direction: :outbound) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/legion/apollo/local/graph.rb', line 160 def find_relationships(entity_id:, relation_type: nil, direction: :outbound) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize ds = case direction when :inbound then db[:local_relationships].where(target_entity_id: entity_id) when :both then relationship_both_directions(entity_id) else db[:local_relationships].where(source_entity_id: entity_id) end ds = ds.where(relation_type: relation_type.to_s.upcase) if relation_type rows = ds.all log.debug do "Apollo::Local::Graph found relationships entity_id=#{entity_id} direction=#{direction} " \ "relation_type=#{relation_type || 'any'} count=#{rows.size}" end { success: true, relationships: rows.map { |r| decode_relationship(r) }, count: rows.size } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.find_relationships', entity_id: entity_id, relation_type: relation_type, direction: direction ) { success: false, error: e. } end |
.traverse(entity_id:, relation_type: nil, depth: 3, direction: :outbound) ⇒ Hash
Traverse from an entity following edges of the given relation_type. Returns all reachable entities within max_depth hops by expanding one frontier batch per depth level instead of querying neighbors per node.
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
# File 'lib/legion/apollo/local/graph.rb', line 207 def traverse(entity_id:, relation_type: nil, depth: 3, direction: :outbound) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize max_depth = [depth.to_i.clamp(1, 10), 10].min rel_filter = relation_type&.to_s&.upcase visited_ids, edge_rows = run_traversal(entity_id, rel_filter, max_depth, direction) entity_rows = visited_ids.empty? ? [] : db[:local_entities].where(id: visited_ids).all { success: true, nodes: entity_rows.map { |r| decode_entity(r) }, edges: edge_rows.map { |r| decode_relationship(r) }, count: entity_rows.size } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.traverse', entity_id: entity_id, relation_type: relation_type, depth: depth, direction: direction ) { success: false, error: e. } end |
.update_entity(id:, **fields) ⇒ Object
rubocop:disable Metrics/MethodLength,Metrics/AbcSize
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/legion/apollo/local/graph.rb', line 87 def update_entity(id:, **fields) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize now = updates = fields.slice(:entity_type, :name, :domain).transform_values(&:to_s) updates[:attributes] = encode(fields[:attributes]) if fields.key?(:attributes) updates[:updated_at] = now count = db[:local_entities].where(id: id).update(updates) return { success: false, error: :not_found } if count.zero? log.info { "Apollo::Local::Graph updated entity id=#{id}" } { success: true, id: id } rescue Sequel::Error => e handle_exception( e, level: :error, operation: 'apollo.local.graph.update_entity', entity_id: id, fields: fields.keys ) { success: false, error: e. } end |