lutaml 0.10.2 → 0.10.4

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +40 -35
  4. data/CHANGELOG.md +108 -0
  5. data/lib/lutaml/cli/uml_commands.rb +2 -2
  6. data/lib/lutaml/command_line.rb +1 -1
  7. data/lib/lutaml/converter/xmi_to_uml.rb +12 -3
  8. data/lib/lutaml/model_transformations/parsers/base_parser.rb +1 -1
  9. data/lib/lutaml/model_transformations/transformation_engine.rb +1 -1
  10. data/lib/lutaml/qea/database.rb +157 -4
  11. data/lib/lutaml/qea/factory/association_transformer.rb +1 -5
  12. data/lib/lutaml/qea/factory/attribute_transformer.rb +4 -10
  13. data/lib/lutaml/qea/factory/base_transformer.rb +1 -5
  14. data/lib/lutaml/qea/factory/class_transformer.rb +26 -62
  15. data/lib/lutaml/qea/factory/data_type_transformer.rb +7 -21
  16. data/lib/lutaml/qea/factory/diagram_transformer.rb +6 -20
  17. data/lib/lutaml/qea/factory/document_builder.rb +1 -1
  18. data/lib/lutaml/qea/factory/enum_transformer.rb +3 -5
  19. data/lib/lutaml/qea/factory/generalization_transformer.rb +2 -7
  20. data/lib/lutaml/qea/factory/instance_transformer.rb +2 -10
  21. data/lib/lutaml/qea/factory/operation_transformer.rb +2 -8
  22. data/lib/lutaml/qea/factory/package_transformer.rb +4 -13
  23. data/lib/lutaml/qea/repositories/base_repository.rb +6 -6
  24. data/lib/lutaml/qea/validation/base_validator.rb +2 -3
  25. data/lib/lutaml/qea/validation/validation_message.rb +2 -2
  26. data/lib/lutaml/qea/validation/validation_result.rb +2 -2
  27. data/lib/lutaml/sysml.rb +1 -1
  28. data/lib/lutaml/uml/has_members.rb +1 -1
  29. data/lib/lutaml/uml/parsers/dsl.rb +1 -1
  30. data/lib/lutaml/uml.rb +1 -1
  31. data/lib/lutaml/uml_repository/index_builder.rb +19 -17
  32. data/lib/lutaml/uml_repository/queries/class_query.rb +44 -10
  33. data/lib/lutaml/uml_repository/queries/inheritance_query.rb +0 -1
  34. data/lib/lutaml/uml_repository/repository.rb +14 -10
  35. data/lib/lutaml/uml_repository/statistics_calculator.rb +28 -16
  36. data/lib/lutaml/version.rb +1 -1
  37. data/lib/lutaml/xmi/parsers/xmi_base.rb +28 -5
  38. data/lib/lutaml.rb +3 -1
  39. data/lutaml.gemspec +2 -2
  40. metadata +5 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cec3346a07c0b975dce5f9c786cb64552969f416cb88a11284d48403f98446b2
4
- data.tar.gz: fc493f4256dcd15b88f46848fbf8c4757f66caca5a1dec1cad3f7a1d51115583
3
+ metadata.gz: 9db8def51fe994ba67b18e3e2045df78ebc06fab52a471fb95e7421738e10dcc
4
+ data.tar.gz: 1ba2c6caed21e4a4b35b4a117f39327312505ebe2b72f389db3561f568395577
5
5
  SHA512:
6
- metadata.gz: 2d3bdbfedc6a13e18138d6071add0b13fe0d384e6c3a082151794d55f8942e7efd2d1652883a12754336994af86b7951a5bce82e07799e2fe327c534ee98f263
7
- data.tar.gz: 22269ec395160854295bac9b035e6e9650419ff2e358ce7b756925768d225c3de1245fd691c57e961e07231600109505007c86c1104c8792ccdc3b31bef91283
6
+ metadata.gz: 9a529f0ab5096212dc808a5cd21ca2eba20cbc8f1bc940076398755302af766462e66e3984d33ef9349d0510c0d25aa845af4b8b7046b6cdc93317d2d21aba5d
7
+ data.tar.gz: e26992d6162b77054ef39f915d4633a213452a98b6e0057ae4240ad825d7f008f529024ba91b3dfbd7dc77ac40a02e4ee3c01cbbdeabcbe3e77c2d5898ebe468
data/.rubocop.yml CHANGED
@@ -16,4 +16,4 @@ plugins:
16
16
  - rubocop-rake
17
17
 
18
18
  AllCops:
19
- TargetRubyVersion: 3.0
19
+ TargetRubyVersion: 3.2
data/.rubocop_todo.yml CHANGED
@@ -1,47 +1,32 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-04-20 02:27:04 UTC using RuboCop version 1.86.1.
3
+ # on 2026-04-25 02:05:14 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 11
10
- # This cop supports safe autocorrection (--autocorrect).
11
- # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation.
12
- Bundler/OrderedGems:
13
- Exclude:
14
- - 'Gemfile'
15
-
16
- # Offense count: 2
9
+ # Offense count: 1
17
10
  # This cop supports safe autocorrection (--autocorrect).
18
- Layout/ClosingParenthesisIndentation:
11
+ Layout/ElseAlignment:
19
12
  Exclude:
20
- - 'lib/lutaml/qea/verification/document_verifier.rb'
13
+ - 'lib/lutaml/qea/factory/class_transformer.rb'
21
14
 
22
- # Offense count: 2
15
+ # Offense count: 1
23
16
  # This cop supports safe autocorrection (--autocorrect).
24
- # Configuration parameters: EnforcedStyle, IndentationWidth.
25
- # SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
26
- Layout/FirstArgumentIndentation:
17
+ # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
18
+ # SupportedStylesAlignWith: start_of_line, relative_to_receiver
19
+ Layout/IndentationWidth:
27
20
  Exclude:
28
- - 'lib/lutaml/qea/verification/document_verifier.rb'
21
+ - 'lib/lutaml/qea/factory/class_transformer.rb'
29
22
 
30
- # Offense count: 63
23
+ # Offense count: 80
31
24
  # This cop supports safe autocorrection (--autocorrect).
32
25
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
33
26
  # URISchemes: http, https
34
27
  Layout/LineLength:
35
28
  Enabled: false
36
29
 
37
- # Offense count: 2
38
- # This cop supports safe autocorrection (--autocorrect).
39
- # Configuration parameters: EnforcedStyle.
40
- # SupportedStyles: symmetrical, new_line, same_line
41
- Layout/MultilineMethodCallBraceLayout:
42
- Exclude:
43
- - 'lib/lutaml/qea/verification/document_verifier.rb'
44
-
45
30
  # Offense count: 1
46
31
  Lint/BinaryOperatorWithIdenticalOperands:
47
32
  Exclude:
@@ -58,6 +43,11 @@ Lint/ConstantDefinitionInBlock:
58
43
  - 'spec/lutaml/model_transformations_spec.rb'
59
44
  - 'spec/lutaml/uml_repository/presenters/presenter_factory_spec.rb'
60
45
 
46
+ # Offense count: 1
47
+ Lint/CopDirectiveSyntax:
48
+ Exclude:
49
+ - 'lib/lutaml/uml_repository/repository.rb'
50
+
61
51
  # Offense count: 8
62
52
  # Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
63
53
  Lint/DuplicateBranch:
@@ -107,13 +97,17 @@ Lint/UnusedMethodArgument:
107
97
  Exclude:
108
98
  - 'lib/lutaml/cli/uml/export_command.rb'
109
99
 
110
- # Offense count: 3
100
+ # Offense count: 8
111
101
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
112
102
  Metrics/AbcSize:
113
103
  Exclude:
104
+ - 'lib/lutaml/converter/xmi_to_uml.rb'
114
105
  - 'lib/lutaml/model_transformations/parsers/base_parser.rb'
106
+ - 'lib/lutaml/qea/database.rb'
115
107
  - 'lib/lutaml/uml_repository/index_builder.rb'
108
+ - 'lib/lutaml/uml_repository/queries/class_query.rb'
116
109
  - 'lib/lutaml/uml_repository/queries/inheritance_query.rb'
110
+ - 'lib/lutaml/xmi/parsers/xmi_base.rb'
117
111
 
118
112
  # Offense count: 1
119
113
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
@@ -121,26 +115,36 @@ Metrics/AbcSize:
121
115
  Metrics/BlockLength:
122
116
  Max: 28
123
117
 
124
- # Offense count: 4
118
+ # Offense count: 11
125
119
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
126
120
  Metrics/CyclomaticComplexity:
127
121
  Exclude:
128
122
  - 'lib/lutaml/cli/tree_view_formatter.rb'
129
123
  - 'lib/lutaml/converter/xmi_to_uml.rb'
124
+ - 'lib/lutaml/qea/database.rb'
125
+ - 'lib/lutaml/qea/factory/enum_transformer.rb'
126
+ - 'lib/lutaml/uml_repository/index_builder.rb'
127
+ - 'lib/lutaml/uml_repository/queries/class_query.rb'
130
128
  - 'lib/lutaml/uml_repository/queries/inheritance_query.rb'
131
129
  - 'lib/lutaml/uml_repository/queries/search_query.rb'
130
+ - 'lib/lutaml/xmi/parsers/xmi_base.rb'
132
131
 
133
- # Offense count: 5
132
+ # Offense count: 11
134
133
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
135
134
  Metrics/MethodLength:
136
- Max: 14
135
+ Max: 27
137
136
 
138
- # Offense count: 2
137
+ # Offense count: 8
139
138
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
140
139
  Metrics/PerceivedComplexity:
141
140
  Exclude:
141
+ - 'lib/lutaml/converter/xmi_to_uml.rb'
142
+ - 'lib/lutaml/qea/database.rb'
142
143
  - 'lib/lutaml/qea/factory/enum_transformer.rb'
144
+ - 'lib/lutaml/uml_repository/index_builder.rb'
145
+ - 'lib/lutaml/uml_repository/queries/class_query.rb'
143
146
  - 'lib/lutaml/uml_repository/queries/inheritance_query.rb'
147
+ - 'lib/lutaml/xmi/parsers/xmi_base.rb'
144
148
 
145
149
  # Offense count: 2
146
150
  # Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
@@ -158,10 +162,11 @@ Naming/PredicateMethod:
158
162
  Exclude:
159
163
  - 'lib/lutaml/qea/factory/document_builder.rb'
160
164
 
161
- # Offense count: 4
165
+ # Offense count: 5
162
166
  # Configuration parameters: MinSize.
163
167
  Performance/CollectionLiteralInLoop:
164
168
  Exclude:
169
+ - 'lib/lutaml/xmi/parsers/xmi_base.rb'
165
170
  - 'spec/lutaml/qea/infrastructure/table_reader_spec.rb'
166
171
  - 'spec/lutaml/uml_repository/index_builder_spec.rb'
167
172
  - 'spec/lutaml/uml_repository/queries/inheritance_query_spec.rb'
@@ -206,7 +211,7 @@ RSpec/ContextWording:
206
211
  RSpec/DescribeClass:
207
212
  Enabled: false
208
213
 
209
- # Offense count: 651
214
+ # Offense count: 657
210
215
  # Configuration parameters: CountAsOne.
211
216
  RSpec/ExampleLength:
212
217
  Max: 108
@@ -236,7 +241,7 @@ RSpec/IndexedLet:
236
241
  - 'spec/lutaml/qea/repositories/base_repository_spec.rb'
237
242
  - 'spec/lutaml/qea/repositories/object_repository_spec.rb'
238
243
 
239
- # Offense count: 26
244
+ # Offense count: 48
240
245
  # Configuration parameters: AssignmentOnly.
241
246
  RSpec/InstanceVariable:
242
247
  Exclude:
@@ -274,11 +279,11 @@ RSpec/MessageSpies:
274
279
  - 'spec/lutaml/uml_repository/presenters/diagram_presenter_spec.rb'
275
280
  - 'spec/lutaml/uml_repository/repository_spec.rb'
276
281
 
277
- # Offense count: 943
282
+ # Offense count: 946
278
283
  RSpec/MultipleExpectations:
279
284
  Max: 26
280
285
 
281
- # Offense count: 118
286
+ # Offense count: 130
282
287
  # Configuration parameters: AllowSubject.
283
288
  RSpec/MultipleMemoizedHelpers:
284
289
  Max: 11
data/CHANGELOG.md ADDED
@@ -0,0 +1,108 @@
1
+ # Changelog
2
+
3
+ ## v0.10.3 (2026-04-25)
4
+
5
+ ### Performance
6
+
7
+ - Replace O(n) linear scans with O(1) hash lookups in QEA transformers
8
+ and XMI parsers (connector, diagram, element lookups)
9
+ - Single-pass IndexBuilder replacing 9 separate tree traversals
10
+ - Memoized inheritance depth in StatisticsCalculator
11
+ - Set-based dedup in Repository associations_index
12
+
13
+ ### Bug fixes
14
+
15
+ - Fix Windows EACCES: replace Tempfile.new with temp_lur_path helper
16
+ to avoid file handle conflicts with rubyzip
17
+ - Fix xml_spec: use include matcher for platform-varying content nodes
18
+
19
+ ### Improvements
20
+
21
+ - Consistent error hierarchy: all module error classes now inherit from
22
+ `Lutaml::Error` (single rescue point for consumers)
23
+ - Fix gemspec email typo (mismatched quote)
24
+ - Bump required_ruby_version from >= 2.7 to >= 3.2 (matching CI)
25
+ - Rubocop: 0 offenses remaining
26
+
27
+ ## v0.10.2 (2026-04-24)
28
+
29
+ ### Bug fixes
30
+
31
+ - Fix Windows Errno::EACCES in PackageExporter: retry on file lock race
32
+ - Remove moxml git override (0.1.15 released)
33
+
34
+ ## v0.10.1 (2026-04-23)
35
+
36
+ ### Bug fixes
37
+
38
+ - Fix flaky CI tests: benchmark speedup assertion and Windows Tempfile race
39
+ - Fix xmi 0.5.6 compatibility: rename SparxRoot to Sparx::Root
40
+ - Fix nil @xmi_index in liquid drops: auto-init via xmi_index method
41
+ - Fix infinite loop in resolve_package_path with circular package hierarchy
42
+ - Fix stereotype type bugs, diagram package_id overwrite, and
43
+ DataTypeTransformer crash
44
+ - Fix 46 pending/failing spec tests across QEA, verification, and liquid specs
45
+
46
+ ## v0.10.0 (2026-04-21)
47
+
48
+ ### Breaking changes
49
+
50
+ - Update to lutaml-model 0.8.0, expressir 2.3, and xmi 0.5.x
51
+ - Migrate Lutaml::Uml models to lutaml-model serialization
52
+ - Unify namespace of Sysml module
53
+
54
+ ### Features
55
+
56
+ - Complete QEA to UML document migration
57
+ - Add PackagePresenter for structured output
58
+ - Add option to skip queries
59
+ - Add function to get qualified name
60
+ - Add Lutaml::Uml::Fidelity class
61
+ - Add attributes type, weight and status to Lutaml::Uml::Constraint
62
+ - Add generalization into Lutaml::Uml::Class
63
+ - Transform option keys to symbol
64
+ - Output find result based on format option
65
+ - Implement default sorting for diff_with_score comparison
66
+ - find_by_name returns single object instead of array
67
+
68
+ ### Bug fixes
69
+
70
+ - Fix Windows EACCES file rename issues (Tempfile handle conflicts)
71
+ - Fix YAML disallowed class loading error
72
+ - Fix attribute parsing values
73
+ - Fix association loading by QEA parser
74
+ - Fix association_generalization parsed by xmi parser
75
+ - Fix certificate CRL error
76
+
77
+ ## v0.9.43 (2025-11-20)
78
+
79
+ - Refactor klass_hash to use KlassDrop object in upper_klass
80
+
81
+ ## v0.9.42 (2025-11-18)
82
+
83
+ - Directly convert XMI into UML
84
+
85
+ ## v0.9.41 (2025-11-12)
86
+
87
+ - Remove unneeded version files
88
+ - Fix error when imports key is nil
89
+ - Unify Sysml namespace
90
+
91
+ ## v0.9.40 (2025-11-10)
92
+
93
+ - Convert DSL to UML
94
+ - Change namespace of Formatter and HasAttributes
95
+ - Update GraphViz and related specs
96
+ - Add Lutaml::Uml::Fidelity class
97
+ - Remove duplicated GraphViz code
98
+ - Create Lutaml::Uml models by Xmi hash
99
+ - Migrate Lutaml::Uml models to lutaml-model
100
+
101
+ ## v0.9.39 (2025-11-03)
102
+
103
+ - Change selection criteria for dependencies
104
+
105
+ ## v0.9.38 (2025-10-31)
106
+
107
+ - Add subtype_of to klass and enum drop models
108
+ - Get stereotype by type idref in owned attribute
@@ -139,8 +139,8 @@ module Lutaml
139
139
 
140
140
  desc "diagram ACTION", "Diagram rendering commands"
141
141
  Uml::DiagramCommand.add_options_to(self, :diagram)
142
- def diagram(action, *args)
143
- Uml::DiagramCommand.new(options.to_h).run(action, *args)
142
+ def diagram(action, *)
143
+ Uml::DiagramCommand.new(options.to_h).run(action, *)
144
144
  end
145
145
 
146
146
  # ===================================================================
@@ -10,7 +10,7 @@ require_relative "uml/parsers/yaml"
10
10
 
11
11
  module Lutaml
12
12
  class CommandLine
13
- class Error < StandardError; end
13
+ class Error < Lutaml::Error; end
14
14
  class FileError < Error; end
15
15
  class NotSupportedInputFormat < Error; end
16
16
 
@@ -126,9 +126,7 @@ module Lutaml
126
126
  def create_uml_diagrams(node_id) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
127
127
  return [] if @xmi_root_model.extension&.diagrams&.diagram.nil?
128
128
 
129
- diagrams = @xmi_root_model.extension.diagrams.diagram.select do |d|
130
- d.model.package == node_id
131
- end
129
+ diagrams = diagram_lookup[node_id]
132
130
 
133
131
  diagrams.map do |diagram|
134
132
  ::Lutaml::Uml::Diagram.new.tap do |dia|
@@ -145,6 +143,17 @@ module Lutaml
145
143
  end
146
144
  end
147
145
 
146
+ # Lazy-built hash index for O(1) diagram lookups by package
147
+ # @return [Hash] Mapping of package_id => [diagrams]
148
+ def diagram_lookup
149
+ @diagram_lookup ||= begin
150
+ idx = Hash.new { |h, k| h[k] = [] }
151
+ diagrams = @xmi_root_model.extension&.diagrams&.diagram || []
152
+ diagrams.each { |d| idx[d.model.package] << d if d.model&.package }
153
+ idx
154
+ end
155
+ end
156
+
148
157
  def create_uml_class_attributes(klass) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
149
158
  return [] if klass.owned_attribute.nil?
150
159
 
@@ -421,7 +421,7 @@ module Lutaml
421
421
  end
422
422
 
423
423
  # Custom error class for parsing failures
424
- class ParseError < StandardError
424
+ class ParseError < Lutaml::Error
425
425
  # @return [StandardError] Original error that caused parsing failure
426
426
  attr_reader :original_error
427
427
 
@@ -392,7 +392,7 @@ module Lutaml
392
392
  end
393
393
 
394
394
  # Error class for unsupported file formats
395
- class UnsupportedFormatError < StandardError
395
+ class UnsupportedFormatError < Lutaml::Error
396
396
  # @return [String] Path to the unsupported file
397
397
  attr_reader :file_path
398
398
 
@@ -268,7 +268,9 @@ module Lutaml
268
268
  # @param id [Integer] Package ID
269
269
  # @return [Models::EaPackage, nil] The package or nil if not found
270
270
  def find_package(id)
271
- packages.find { |pkg| pkg.package_id == id }
271
+ @packages_by_id ||= build_group_index(packages, :package_id,
272
+ single: true)
273
+ @packages_by_id[id]
272
274
  end
273
275
 
274
276
  # Find an attribute by ID
@@ -276,7 +278,8 @@ module Lutaml
276
278
  # @param id [Integer] Attribute ID
277
279
  # @return [Models::EaAttribute, nil] The attribute or nil if not found
278
280
  def find_attribute(id)
279
- attributes.find { |attr| attr.id == id }
281
+ @attributes_by_id ||= build_group_index(attributes, :id, single: true)
282
+ @attributes_by_id[id]
280
283
  end
281
284
 
282
285
  # Find a connector by ID
@@ -284,7 +287,9 @@ module Lutaml
284
287
  # @param id [Integer] Connector ID
285
288
  # @return [Models::EaConnector, nil] The connector or nil if not found
286
289
  def find_connector(id)
287
- connectors.find { |conn| conn.connector_id == id }
290
+ @connectors_by_id ||= build_group_index(connectors, :connector_id,
291
+ single: true)
292
+ @connectors_by_id[id]
288
293
  end
289
294
 
290
295
  # Find a diagram by ID
@@ -292,7 +297,9 @@ module Lutaml
292
297
  # @param id [Integer] Diagram ID
293
298
  # @return [Models::EaDiagram, nil] The diagram or nil if not found
294
299
  def find_diagram(id)
295
- diagrams.find { |diag| diag.diagram_id == id }
300
+ @diagrams_by_id ||= build_group_index(diagrams, :diagram_id,
301
+ single: true)
302
+ @diagrams_by_id[id]
296
303
  end
297
304
 
298
305
  # Check if database is empty
@@ -309,6 +316,102 @@ module Lutaml
309
316
  @collections.keys
310
317
  end
311
318
 
319
+ # Find object by ea_guid
320
+ #
321
+ # @param ea_guid [String] Object GUID
322
+ # @return [Models::EaObject, nil] The object or nil if not found
323
+ def find_object_by_guid(ea_guid)
324
+ @objects_by_guid ||= build_group_index(objects, :ea_guid, single: true)
325
+ @objects_by_guid[ea_guid]
326
+ end
327
+
328
+ # Get attributes for a specific object
329
+ #
330
+ # @param object_id [Integer] Object ID
331
+ # @return [Array<Models::EaAttribute>] Attributes for the object
332
+ def attributes_for_object(object_id)
333
+ @attributes_by_object_id ||= build_group_index(attributes,
334
+ :ea_object_id)
335
+ @attributes_by_object_id[object_id] || []
336
+ end
337
+
338
+ # Get operations for a specific object
339
+ #
340
+ # @param object_id [Integer] Object ID
341
+ # @return [Array<Models::EaOperation>] Operations for the object
342
+ def operations_for_object(object_id)
343
+ @operations_by_object_id ||= build_group_index(operations,
344
+ :ea_object_id)
345
+ @operations_by_object_id[object_id] || []
346
+ end
347
+
348
+ # Get operation parameters for a specific operation
349
+ #
350
+ # @param operation_id [Integer] Operation ID
351
+ # @return [Array<Models::EaOperationParam>] Parameters for the operation
352
+ def operation_params_for(operation_id)
353
+ @operation_params_by_id ||= build_group_index(operation_params,
354
+ :operationid)
355
+ @operation_params_by_id[operation_id] || []
356
+ end
357
+
358
+ # Get connectors involving a specific object (start or end)
359
+ #
360
+ # @param object_id [Integer] Object ID
361
+ # @return [Array<Models::EaConnector>] Connectors for the object
362
+ def connectors_for_object(object_id)
363
+ @connectors_by_start ||= build_group_index(connectors, :start_object_id)
364
+ @connectors_by_end ||= build_group_index(connectors, :end_object_id)
365
+ (@connectors_by_start[object_id] || []) +
366
+ (@connectors_by_end[object_id] || [])
367
+ end
368
+
369
+ # Get child packages for a parent
370
+ #
371
+ # @param parent_id [Integer] Parent package ID
372
+ # @return [Array<Models::EaPackage>] Child packages
373
+ def child_packages_for(parent_id)
374
+ @packages_by_parent ||= build_group_index(packages, :parent_id)
375
+ @packages_by_parent[parent_id] || []
376
+ end
377
+
378
+ # Get objects in a specific package
379
+ #
380
+ # @param package_id [Integer] Package ID
381
+ # @return [Array<Models::EaObject>] Objects in the package
382
+ def objects_in_package(package_id)
383
+ @objects_by_package_id ||= build_group_index(objects, :package_id)
384
+ @objects_by_package_id[package_id] || []
385
+ end
386
+
387
+ # Get diagrams in a specific package
388
+ #
389
+ # @param package_id [Integer] Package ID
390
+ # @return [Array<Models::EaDiagram>] Diagrams in the package
391
+ def diagrams_in_package(package_id)
392
+ @diagrams_by_package_id ||= build_group_index(diagrams, :package_id)
393
+ @diagrams_by_package_id[package_id] || []
394
+ end
395
+
396
+ # Get diagram objects for a specific diagram
397
+ #
398
+ # @param diagram_id [Integer] Diagram ID
399
+ # @return [Array<Models::EaDiagramObject>] Diagram objects
400
+ def diagram_objects_for(diagram_id)
401
+ @diagram_objects_by_id ||= build_group_index(diagram_objects,
402
+ :diagram_id)
403
+ @diagram_objects_by_id[diagram_id] || []
404
+ end
405
+
406
+ # Get diagram links for a specific diagram
407
+ #
408
+ # @param diagram_id [Integer] Diagram ID
409
+ # @return [Array<Models::EaDiagramLink>] Diagram links
410
+ def diagram_links_for(diagram_id)
411
+ @diagram_links_by_id ||= build_group_index(diagram_links, :diagramid)
412
+ @diagram_links_by_id[diagram_id] || []
413
+ end
414
+
312
415
  # Freeze all collections to make database immutable
313
416
  #
314
417
  # @return [self]
@@ -316,9 +419,59 @@ module Lutaml
316
419
  # Memoize repositories before freezing
317
420
  objects
318
421
 
422
+ # Eagerly build all lookup indexes before freezing
423
+ build_lookup_indexes
424
+
319
425
  @collections.freeze
320
426
  super
321
427
  end
428
+
429
+ private
430
+
431
+ # Build a group index from a collection by a given attribute.
432
+ # BaseRepository overrides group_by to take a symbol (not a block),
433
+ # so we use each_with_object instead.
434
+ #
435
+ # @param collection [Array] Collection to index
436
+ # @param method [Symbol] Attribute method to group by
437
+ # @param single [Boolean] If true, return single object (last match)
438
+ # instead of array
439
+ # @return [Hash] Group index
440
+ def build_group_index(collection, method, single: false)
441
+ if single
442
+ collection.each_with_object({}) do |item, hash|
443
+ key = item.send(method)
444
+ hash[key] = item if key
445
+ end
446
+ else
447
+ collection.each_with_object({}) do |item, hash|
448
+ key = item.send(method)
449
+ (hash[key] ||= []) << item if key
450
+ end
451
+ end
452
+ end
453
+
454
+ # Eagerly build all lazy lookup indexes before freezing
455
+ def build_lookup_indexes
456
+ @objects_by_guid = build_group_index(objects, :ea_guid, single: true)
457
+ @attributes_by_object_id = build_group_index(attributes, :ea_object_id)
458
+ @operations_by_object_id = build_group_index(operations, :ea_object_id)
459
+ @operation_params_by_id = build_group_index(operation_params,
460
+ :operationid)
461
+ @connectors_by_start = build_group_index(connectors, :start_object_id)
462
+ @connectors_by_end = build_group_index(connectors, :end_object_id)
463
+ @packages_by_parent = build_group_index(packages, :parent_id)
464
+ @objects_by_package_id = build_group_index(objects, :package_id)
465
+ @diagrams_by_package_id = build_group_index(diagrams, :package_id)
466
+ @diagram_objects_by_id = build_group_index(diagram_objects, :diagram_id)
467
+ @diagram_links_by_id = build_group_index(diagram_links, :diagramid)
468
+ # Also build hash indexes for find_* methods
469
+ @packages_by_id = build_group_index(packages, :package_id, single: true)
470
+ @connectors_by_id = build_group_index(connectors, :connector_id,
471
+ single: true)
472
+ @diagrams_by_id = build_group_index(diagrams, :diagram_id, single: true)
473
+ @attributes_by_id = build_group_index(attributes, :id, single: true)
474
+ end
322
475
  end
323
476
  end
324
477
  end
@@ -71,11 +71,7 @@ module Lutaml
71
71
  def find_object(object_id)
72
72
  return nil if object_id.nil?
73
73
 
74
- query = "SELECT * FROM t_object WHERE Object_ID = ?"
75
- rows = database.connection.execute(query, object_id)
76
- return nil if rows.empty?
77
-
78
- Models::EaObject.from_db_row(rows.first)
74
+ database.find_object(object_id)
79
75
  end
80
76
 
81
77
  # Build cardinality from string
@@ -55,16 +55,10 @@ module Lutaml
55
55
  def lookup_type_xmi_id(classifier_id) # rubocop:disable Metrics/AbcSize
56
56
  return nil if classifier_id.nil? || classifier_id.to_i.zero?
57
57
 
58
- query = "SELECT ea_guid FROM t_object WHERE Object_ID = ?"
59
- rows = database.connection.execute(query, [classifier_id])
60
- return nil if rows.empty?
61
-
62
- ea_guid = if rows.first.is_a?(Hash)
63
- rows.first["ea_guid"] || rows.first[:ea_guid]
64
- else
65
- rows.first[0]
66
- end
67
- normalize_guid_to_xmi_format(ea_guid, "EAID")
58
+ obj = database.find_object(classifier_id.to_i)
59
+ return nil unless obj
60
+
61
+ normalize_guid_to_xmi_format(obj.ea_guid, "EAID")
68
62
  end
69
63
 
70
64
  # Build cardinality from lower and upper bounds
@@ -120,11 +120,7 @@ module Lutaml
120
120
  def find_object_by_id(object_id)
121
121
  return nil if object_id.nil?
122
122
 
123
- query = "SELECT * FROM t_object WHERE Object_ID = ?"
124
- rows = database.connection.execute(query, object_id)
125
- return nil if rows.empty?
126
-
127
- Models::EaObject.from_db_row(rows.first)
123
+ database.find_object(object_id)
128
124
  end
129
125
  end
130
126
  end