expressir 2.1.29 → 2.1.31

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +98 -0
  3. data/.github/workflows/links.yml +100 -0
  4. data/.github/workflows/rake.yml +4 -0
  5. data/.github/workflows/release.yml +5 -0
  6. data/.github/workflows/validate_schemas.yml +1 -1
  7. data/.gitignore +3 -0
  8. data/.rubocop.yml +1 -1
  9. data/.rubocop_todo.yml +209 -55
  10. data/Gemfile +2 -1
  11. data/README.adoc +650 -83
  12. data/docs/Gemfile +12 -0
  13. data/docs/_config.yml +141 -0
  14. data/docs/_guides/changes/changes-format.adoc +778 -0
  15. data/docs/_guides/changes/importing-eengine.adoc +898 -0
  16. data/docs/_guides/changes/index.adoc +396 -0
  17. data/docs/_guides/changes/programmatic-usage.adoc +1038 -0
  18. data/docs/_guides/changes/validating-changes.adoc +681 -0
  19. data/docs/_guides/cli/benchmark-performance.adoc +834 -0
  20. data/docs/_guides/cli/coverage-analysis.adoc +921 -0
  21. data/docs/_guides/cli/format-schemas.adoc +547 -0
  22. data/docs/_guides/cli/index.adoc +8 -0
  23. data/docs/_guides/cli/managing-changes.adoc +927 -0
  24. data/docs/_guides/cli/validate-ascii.adoc +645 -0
  25. data/docs/_guides/cli/validate-schemas.adoc +534 -0
  26. data/docs/_guides/index.adoc +165 -0
  27. data/docs/_guides/ler/creating-packages.adoc +664 -0
  28. data/docs/_guides/ler/index.adoc +305 -0
  29. data/docs/_guides/ler/loading-packages.adoc +707 -0
  30. data/docs/_guides/ler/package-formats.adoc +748 -0
  31. data/docs/_guides/ler/querying-packages.adoc +826 -0
  32. data/docs/_guides/ler/validating-packages.adoc +750 -0
  33. data/docs/_guides/liquid/basic-templates.adoc +813 -0
  34. data/docs/_guides/liquid/documentation-generation.adoc +1042 -0
  35. data/docs/_guides/liquid/drops-reference.adoc +829 -0
  36. data/docs/_guides/liquid/filters-and-tags.adoc +912 -0
  37. data/docs/_guides/liquid/index.adoc +468 -0
  38. data/docs/_guides/manifests/creating-manifests.adoc +483 -0
  39. data/docs/_guides/manifests/index.adoc +307 -0
  40. data/docs/_guides/manifests/resolving-manifests.adoc +557 -0
  41. data/docs/_guides/manifests/validating-manifests.adoc +713 -0
  42. data/docs/_guides/ruby-api/formatting-schemas.adoc +605 -0
  43. data/docs/_guides/ruby-api/index.adoc +257 -0
  44. data/docs/_guides/ruby-api/parsing-files.adoc +421 -0
  45. data/docs/_guides/ruby-api/search-engine.adoc +609 -0
  46. data/docs/_guides/ruby-api/working-with-repository.adoc +577 -0
  47. data/docs/_pages/data-model.adoc +665 -0
  48. data/docs/_pages/express-language.adoc +506 -0
  49. data/docs/_pages/getting-started.adoc +414 -0
  50. data/docs/_pages/index.adoc +116 -0
  51. data/docs/_pages/introduction.adoc +256 -0
  52. data/docs/_pages/ler-packages.adoc +837 -0
  53. data/docs/_pages/parsers.adoc +683 -0
  54. data/docs/_pages/schema-manifests.adoc +431 -0
  55. data/docs/_references/index.adoc +228 -0
  56. data/docs/_tutorials/creating-ler-package.adoc +735 -0
  57. data/docs/_tutorials/documentation-coverage.adoc +795 -0
  58. data/docs/_tutorials/index.adoc +221 -0
  59. data/docs/_tutorials/liquid-templates.adoc +806 -0
  60. data/docs/_tutorials/parsing-your-first-schema.adoc +522 -0
  61. data/docs/_tutorials/querying-schemas.adoc +751 -0
  62. data/docs/_tutorials/working-with-multiple-schemas.adoc +676 -0
  63. data/docs/index.adoc +242 -0
  64. data/docs/lychee.toml +84 -0
  65. data/examples/demo_ler_usage.sh +86 -0
  66. data/examples/ler/README.md +111 -0
  67. data/examples/ler/simple_example.ler +0 -0
  68. data/examples/ler/simple_schema.exp +33 -0
  69. data/examples/ler_build.rb +75 -0
  70. data/examples/ler_cli.rb +79 -0
  71. data/examples/ler_demo_complete.rb +276 -0
  72. data/examples/ler_query.rb +91 -0
  73. data/examples/ler_query_examples.rb +305 -0
  74. data/examples/ler_stats.rb +81 -0
  75. data/examples/phase3_demo.rb +159 -0
  76. data/examples/query_demo_simple.rb +131 -0
  77. data/expressir.gemspec +2 -0
  78. data/lib/expressir/changes/schema_change.rb +32 -22
  79. data/lib/expressir/changes/{edition_change.rb → version_change.rb} +3 -3
  80. data/lib/expressir/cli.rb +12 -4
  81. data/lib/expressir/commands/changes_import_eengine.rb +2 -2
  82. data/lib/expressir/commands/changes_validate.rb +1 -1
  83. data/lib/expressir/commands/manifest.rb +427 -0
  84. data/lib/expressir/commands/package.rb +1274 -0
  85. data/lib/expressir/commands/validate.rb +70 -37
  86. data/lib/expressir/commands/validate_ascii.rb +607 -0
  87. data/lib/expressir/commands/validate_load.rb +88 -0
  88. data/lib/expressir/express/formatter.rb +5 -1
  89. data/lib/expressir/express/formatters/remark_item_formatter.rb +25 -0
  90. data/lib/expressir/express/parser.rb +33 -0
  91. data/lib/expressir/manifest/resolver.rb +213 -0
  92. data/lib/expressir/manifest/validator.rb +195 -0
  93. data/lib/expressir/model/declarations/entity.rb +6 -0
  94. data/lib/expressir/model/dependency_resolver.rb +270 -0
  95. data/lib/expressir/model/indexes/entity_index.rb +103 -0
  96. data/lib/expressir/model/indexes/reference_index.rb +148 -0
  97. data/lib/expressir/model/indexes/type_index.rb +149 -0
  98. data/lib/expressir/model/interface_validator.rb +384 -0
  99. data/lib/expressir/model/repository.rb +400 -5
  100. data/lib/expressir/model/repository_validator.rb +295 -0
  101. data/lib/expressir/model/search_engine.rb +525 -0
  102. data/lib/expressir/model.rb +4 -94
  103. data/lib/expressir/package/builder.rb +200 -0
  104. data/lib/expressir/package/metadata.rb +81 -0
  105. data/lib/expressir/package/reader.rb +165 -0
  106. data/lib/expressir/schema_manifest.rb +11 -1
  107. data/lib/expressir/version.rb +1 -1
  108. data/lib/expressir.rb +16 -3
  109. metadata +115 -5
  110. data/docs/benchmarking.adoc +0 -107
  111. data/docs/liquid_drops.adoc +0 -1547
@@ -0,0 +1,751 @@
1
+ ---
2
+ title: Querying Schemas
3
+ nav_order: 5
4
+ ---
5
+
6
+ == Querying Schemas
7
+
8
+ === Prerequisites
9
+
10
+ Before starting this tutorial, ensure you have:
11
+
12
+ * Completed link:creating-ler-package.html[Creating LER Packages]
13
+ * Understanding of EXPRESS entities and types
14
+ * A LER package or parsed repository to query
15
+ * Basic Ruby knowledge for API usage
16
+
17
+ === Learning Objectives
18
+
19
+ By the end of this tutorial, you will be able to:
20
+
21
+ * Use the SearchEngine API to find entities and types
22
+ * Apply pattern matching with wildcards and regex
23
+ * Filter results by schema, type, and category
24
+ * Build complex queries with multiple criteria
25
+ * Extract and analyze schema statistics
26
+ * Find relationships between elements
27
+
28
+ === What You'll Build
29
+
30
+ You'll create various query utilities to explore and analyze EXPRESS schemas, from simple searches to complex relationship analysis.
31
+
32
+ === Step 1: Understanding the SearchEngine
33
+
34
+ ==== What is SearchEngine?
35
+
36
+ The [`SearchEngine`](../../lib/expressir/model/search_engine.rb:1) provides a powerful API for querying repositories:
37
+
38
+ * **Fast lookups**: Pre-built indexes for instant results
39
+ * **Pattern matching**: Wildcards, regex, exact matches
40
+ * **Type filtering**: Search by element type
41
+ * **Schema scoping**: Limit to specific schemas
42
+ * **Category filtering**: Filter types by category
43
+
44
+ ==== Element Types
45
+
46
+ SearchEngine can find these element types:
47
+
48
+ * `entity` - Entity declarations
49
+ * `type` - Type declarations
50
+ * `function` - Function declarations
51
+ * `procedure` - Procedure declarations
52
+ * `rule` - Rule declarations
53
+ * `constant` - Constant declarations
54
+ * `attribute` - Entity attributes
55
+ * `derived_attribute` - Derived attributes
56
+ * `inverse_attribute` - Inverse attributes
57
+ * `parameter` - Function/procedure parameters
58
+ * `variable` - Local variables
59
+ * `where_rule` - WHERE rules
60
+ * `unique_rule` - UNIQUE rules
61
+
62
+ === Step 2: Basic Queries
63
+
64
+ ==== Setup
65
+
66
+ Create a sample LER package first, or use an existing one.
67
+
68
+ Create `basic_queries.rb`:
69
+
70
+ [source,ruby]
71
+ ----
72
+ require 'expressir'
73
+
74
+ # Load package or parse schemas
75
+ repo = Expressir::Model::Repository.from_package('catalog.ler')
76
+
77
+ # Create search engine
78
+ search = Expressir::Model::SearchEngine.new(repo)
79
+
80
+ puts "Repository loaded:"
81
+ puts " Schemas: #{repo.schemas.size}"
82
+ puts " Ready to query!"
83
+ ----
84
+
85
+ ==== List All Entities
86
+
87
+ [source,ruby]
88
+ ----
89
+ # List all entities
90
+ entities = search.list(type: 'entity')
91
+
92
+ puts "\nAll entities (#{entities.size}):"
93
+ entities.each do |entity|
94
+ puts " #{entity[:schema]}.#{entity[:id]}"
95
+ end
96
+ ----
97
+
98
+ **Output**:
99
+ [source]
100
+ ----
101
+ All entities (4):
102
+ base_schema.person
103
+ base_schema.organization
104
+ product_schema.product
105
+ product_schema.product_category
106
+ ----
107
+
108
+ ==== List All Types
109
+
110
+ [source,ruby]
111
+ ----
112
+ # List all types
113
+ types = search.list(type: 'type')
114
+
115
+ puts "\nAll types (#{types.size}):"
116
+ types.each do |type_info|
117
+ category = type_info[:category] || 'simple'
118
+ puts " #{type_info[:schema]}.#{type_info[:id]} [#{category}]"
119
+ end
120
+ ----
121
+
122
+ **Output**:
123
+ [source]
124
+ ----
125
+ All types (2):
126
+ base_schema.identifier [simple]
127
+ base_schema.label [simple]
128
+ ----
129
+
130
+ ==== Count Elements
131
+
132
+ [source,ruby]
133
+ ----
134
+ # Count different element types
135
+ element_types = ['entity', 'type', 'function', 'constant']
136
+
137
+ puts "\nElement counts:"
138
+ element_types.each do |elem_type|
139
+ count = search.count(type: elem_type)
140
+ puts " #{elem_type}: #{count}"
141
+ end
142
+ ----
143
+
144
+ === Step 3: Pattern Matching
145
+
146
+ ==== Simple Name Search
147
+
148
+ [source,ruby]
149
+ ----
150
+ # Search by name (case-insensitive by default)
151
+ results = search.search(pattern: 'product')
152
+
153
+ puts "\nMatching 'product':"
154
+ results.each do |result|
155
+ puts " #{result[:type]}: #{result[:path]}"
156
+ end
157
+ ----
158
+
159
+ **Output**:
160
+ [source]
161
+ ----
162
+ Matching 'product':
163
+ entity: product_schema.product
164
+ entity: product_schema.product_category
165
+ ----
166
+
167
+ ==== Wildcard Patterns
168
+
169
+ [source,ruby]
170
+ ----
171
+ # Prefix matching
172
+ results = search.search(pattern: 'product*', type: 'entity')
173
+ puts "\nEntities starting with 'product':"
174
+ results.each { |r| puts " #{r[:path]}" }
175
+
176
+ # Suffix matching
177
+ results = search.search(pattern: '*_category', type: 'entity')
178
+ puts "\nEntities ending with '_category':"
179
+ results.each { |r| puts " #{r[:path]}" }
180
+
181
+ # Contains
182
+ results = search.search(pattern: '*org*', type: 'entity')
183
+ puts "\nEntities containing 'org':"
184
+ results.each { |r| puts " #{r[:path]}" }
185
+ ----
186
+
187
+ ==== Qualified Name Search
188
+
189
+ [source,ruby]
190
+ ----
191
+ # Search in specific schema
192
+ results = search.search(pattern: 'product_schema.product')
193
+ puts "\nIn product_schema:"
194
+ results.each { |r| puts " #{r[:id]}" }
195
+
196
+ # Wildcard schema
197
+ results = search.search(pattern: '*.product', type: 'entity')
198
+ puts "\nEntity named 'product' in any schema:"
199
+ results.each { |r| puts " #{r[:schema]}.#{r[:id]}" }
200
+
201
+ # Schema wildcard
202
+ results = search.search(pattern: 'product_schema.*', type: 'entity')
203
+ puts "\nAll entities in product_schema:"
204
+ results.each { |r| puts " #{r[:id]}" }
205
+ ----
206
+
207
+ ==== Regex Patterns
208
+
209
+ [source,ruby]
210
+ ----
211
+ # Regex search
212
+ results = search.search(
213
+ pattern: '^product_[a-z]+$',
214
+ type: 'entity',
215
+ regex: true
216
+ )
217
+
218
+ puts "\nRegex match '^product_[a-z]+$':"
219
+ results.each { |r| puts " #{r[:id]}" }
220
+
221
+ # Complex regex
222
+ results = search.search(
223
+ pattern: '(person|organization)',
224
+ regex: true
225
+ )
226
+
227
+ puts "\nMatching 'person' or 'organization':"
228
+ results.each { |r| puts " #{r[:type]}: #{r[:id]}" }
229
+ ----
230
+
231
+ === Step 4: Filtering Results
232
+
233
+ ==== Filter by Type
234
+
235
+ [source,ruby]
236
+ ----
237
+ # Only entities
238
+ entities = search.search(pattern: '*', type: 'entity')
239
+ puts "Entities: #{entities.size}"
240
+
241
+ # Only types
242
+ types = search.search(pattern: '*', type: 'type')
243
+ puts "Types: #{types.size}"
244
+
245
+ # Multiple queries
246
+ ['entity', 'type', 'function'].each do |elem_type|
247
+ count = search.count(type: elem_type)
248
+ puts "#{elem_type}: #{count}"
249
+ end
250
+ ----
251
+
252
+ ==== Filter by Schema
253
+
254
+ [source,ruby]
255
+ ----
256
+ # Entities in specific schema
257
+ results = search.list(type: 'entity', schema: 'base_schema')
258
+
259
+ puts "\nEntities in base_schema:"
260
+ results.each do |entity|
261
+ puts " #{entity[:id]}"
262
+ end
263
+
264
+ # Search within schema
265
+ results = search.search(
266
+ pattern: 'p*',
267
+ type: 'entity',
268
+ schema: 'base_schema'
269
+ )
270
+
271
+ puts "\nbase_schema entities starting with 'p':"
272
+ results.each { |r| puts " #{r[:id]}" }
273
+ ----
274
+
275
+ ==== Filter by Category
276
+
277
+ [source,ruby]
278
+ ----
279
+ # Find SELECT types
280
+ select_types = search.list(type: 'type', category: 'select')
281
+ puts "\nSELECT types: #{select_types.size}"
282
+
283
+ # Find ENUMERATION types
284
+ enum_types = search.list(type: 'type', category: 'enumeration')
285
+ puts "ENUMERATION types: #{enum_types.size}"
286
+
287
+ # Categories available
288
+ categories = ['select', 'enumeration', 'aggregate', 'simple']
289
+ categories.each do |cat|
290
+ count = search.count(type: 'type', category: cat)
291
+ puts " #{cat}: #{count}" if count > 0
292
+ end
293
+ ----
294
+
295
+ === Step 5: Advanced Queries
296
+
297
+ ==== Find Attributes
298
+
299
+ [source,ruby]
300
+ ----
301
+ # All attributes
302
+ attributes = search.list(type: 'attribute')
303
+ puts "\nTotal attributes: #{attributes.size}"
304
+
305
+ # Attributes in specific entity
306
+ results = search.search(
307
+ pattern: 'product_schema.product.*',
308
+ type: 'attribute'
309
+ )
310
+
311
+ puts "\nAttributes of 'product' entity:"
312
+ results.each do |attr|
313
+ puts " #{attr[:id]}"
314
+ end
315
+
316
+ # Attributes named 'name'
317
+ results = search.search(
318
+ pattern: '*.*name',
319
+ type: 'attribute'
320
+ )
321
+
322
+ puts "\nAttributes named 'name':"
323
+ results.each { |r| puts " #{r[:path]}" }
324
+ ----
325
+
326
+ ==== Find Functions and Parameters
327
+
328
+ [source,ruby]
329
+ ----
330
+ # All functions
331
+ functions = search.list(type: 'function')
332
+ puts "\nFunctions: #{functions.size}"
333
+ functions.each { |f| puts " #{f[:schema]}.#{f[:id]}" }
334
+
335
+ # Function parameters
336
+ if functions.any?
337
+ func = functions.first[:object]
338
+ puts "\nParameters of #{functions.first[:id]}:"
339
+
340
+ params = search.search(
341
+ pattern: "#{functions.first[:schema]}.#{functions.first[:id]}.*",
342
+ type: 'parameter'
343
+ )
344
+ params.each { |p| puts " #{p[:id]}" }
345
+ end
346
+ ----
347
+
348
+ ==== Complex Combined Queries
349
+
350
+ Create `complex_queries.rb`:
351
+
352
+ [source,ruby]
353
+ ----
354
+ require 'expressir'
355
+
356
+ repo = Expressir::Model::Repository.from_package('catalog.ler')
357
+ search = Expressir::Model::SearchEngine.new(repo)
358
+
359
+ # Query 1: Find all assignment-related entities
360
+ puts "Assignment entities:"
361
+ results = search.search(
362
+ pattern: '*assignment*',
363
+ type: 'entity'
364
+ )
365
+ results.each { |r| puts " #{r[:path]}" }
366
+
367
+ # Query 2: SELECT types starting with 'action'
368
+ puts "\nSELECT types starting with 'action':"
369
+ results = search.search(
370
+ pattern: 'action*',
371
+ type: 'type',
372
+ category: 'select'
373
+ )
374
+ results.each { |r| puts " #{r[:path]}" }
375
+
376
+ # Query 3: Entities in specific schema matching pattern
377
+ puts "\nMatching pattern in schema:"
378
+ results = search.search(
379
+ pattern: '*product*',
380
+ type: 'entity',
381
+ schema: 'product_schema'
382
+ )
383
+ results.each { |r| puts " #{r[:id]}" }
384
+
385
+ # Query 4: Case-sensitive exact match
386
+ results = search.search(
387
+ pattern: 'Product',
388
+ type: 'entity',
389
+ case_sensitive: true,
390
+ exact: true
391
+ )
392
+ puts "\nCase-sensitive 'Product': #{results.size} results"
393
+ ----
394
+
395
+ === Step 6: Extracting Full Objects
396
+
397
+ ==== Get Entity Details
398
+
399
+ [source,ruby]
400
+ ----
401
+ # Search and get full entity object
402
+ results = search.search(pattern: 'product', type: 'entity', exact: true)
403
+
404
+ if results.any?
405
+ entity = results.first[:object]
406
+
407
+ puts "\nEntity: #{entity.id}"
408
+ puts "Attributes:"
409
+ entity.attributes.each do |attr|
410
+ optional = attr.optional ? " (optional)" : ""
411
+ puts " #{attr.id}: #{attr.type}#{optional}"
412
+ end
413
+
414
+ if entity.subtype_of && !entity.subtype_of.empty?
415
+ puts "Supertypes:"
416
+ entity.subtype_of.each { |st| puts " - #{st}" }
417
+ end
418
+ end
419
+ ----
420
+
421
+ ==== Analyze Type Details
422
+
423
+ [source,ruby]
424
+ ----
425
+ # Get type details
426
+ results = search.search(pattern: 'identifier', type: 'type', exact: true)
427
+
428
+ if results.any?
429
+ type_decl = results.first[:object]
430
+
431
+ puts "\nType: #{type_decl.id}"
432
+ puts "Underlying: #{type_decl.underlying_type.class.name}"
433
+
434
+ if type_decl.where_rules && !type_decl.where_rules.empty?
435
+ puts "WHERE rules:"
436
+ type_decl.where_rules.each do |rule|
437
+ puts " #{rule.id}: #{rule.expression}"
438
+ end
439
+ end
440
+ end
441
+ ----
442
+
443
+ === Step 7: Relationship Analysis
444
+
445
+ ==== Find Entity Dependencies
446
+
447
+ Create `find_dependencies.rb`:
448
+
449
+ [source,ruby]
450
+ ----
451
+ require 'expressir'
452
+
453
+ repo = Expressir::Model::Repository.from_package('catalog.ler')
454
+ search = Expressir::Model::SearchEngine.new(repo)
455
+
456
+ def find_entity_dependencies(search, entity_name)
457
+ results = search.search(pattern: entity_name, type: 'entity', exact: true)
458
+ return if results.empty?
459
+
460
+ entity = results.first[:object]
461
+ dependencies = []
462
+
463
+ entity.attributes.each do |attr|
464
+ if attr.type.respond_to?(:ref) && attr.type.ref
465
+ ref = attr.type.ref
466
+ if ref.is_a?(Expressir::Model::Declarations::Entity)
467
+ dependencies << ref.id
468
+ elsif ref.is_a?(Expressir::Model::Declarations::Type)
469
+ dependencies << ref.id
470
+ end
471
+ end
472
+ end
473
+
474
+ dependencies.uniq
475
+ end
476
+
477
+ # Find dependencies for 'product' entity
478
+ deps = find_entity_dependencies(search, 'product')
479
+ puts "Dependencies of 'product':"
480
+ deps.each { |d| puts " - #{d}" }
481
+ ----
482
+
483
+ ==== Find All Uses of a Type
484
+
485
+ [source,ruby]
486
+ ----
487
+ def find_type_usage(repo, type_name)
488
+ usages = []
489
+
490
+ repo.schemas.each do |schema|
491
+ schema.entities.each do |entity|
492
+ entity.attributes.each do |attr|
493
+ if attr.type.respond_to?(:id) && attr.type.id == type_name
494
+ usages << {
495
+ schema: schema.id,
496
+ entity: entity.id,
497
+ attribute: attr.id
498
+ }
499
+ end
500
+ end
501
+ end
502
+ end
503
+
504
+ usages
505
+ end
506
+
507
+ # Find where 'identifier' type is used
508
+ usages = find_type_usage(repo, 'identifier')
509
+ puts "\nUsages of 'identifier' type:"
510
+ usages.each do |usage|
511
+ puts " #{usage[:schema]}.#{usage[:entity]}.#{usage[:attribute]}"
512
+ end
513
+ ----
514
+
515
+ === Step 8: Statistics and Aggregations
516
+
517
+ ==== Element Distribution
518
+
519
+ Create `statistics.rb`:
520
+
521
+ [source,ruby]
522
+ ----
523
+ require 'expressir'
524
+
525
+ repo = Expressir::Model::Repository.from_package('catalog.ler')
526
+ search = Expressir::Model::SearchEngine.new(repo)
527
+
528
+ puts "Repository Statistics"
529
+ puts "=" * 60
530
+
531
+ # Element counts
532
+ element_types = [
533
+ 'entity', 'type', 'function', 'procedure',
534
+ 'rule', 'constant', 'attribute'
535
+ ]
536
+
537
+ puts "\nElement counts:"
538
+ element_types.each do |elem_type|
539
+ count = search.count(type: elem_type)
540
+ puts " #{elem_type.ljust(20)}: #{count}" if count > 0
541
+ end
542
+
543
+ # Per-schema breakdown
544
+ puts "\nEntities per schema:"
545
+ repo.schemas.each do |schema|
546
+ count = search.count(type: 'entity', schema: schema.id)
547
+ puts " #{schema.id.ljust(25)}: #{count}"
548
+ end
549
+
550
+ # Type categories
551
+ puts "\nTypes by category:"
552
+ categories = ['select', 'enumeration', 'aggregate', 'simple']
553
+ categories.each do |cat|
554
+ count = search.count(type: 'type', category: cat)
555
+ puts " #{cat.ljust(20)}: #{count}" if count > 0
556
+ end
557
+ ----
558
+
559
+ ==== Top Entities by Attribute Count
560
+
561
+ [source,ruby]
562
+ ----
563
+ # Find entities with most attributes
564
+ entities = search.list(type: 'entity')
565
+
566
+ entity_attrs = entities.map do |e|
567
+ obj = e[:object]
568
+ {
569
+ path: e[:path],
570
+ count: obj.attributes.size
571
+ }
572
+ end
573
+
574
+ puts "\nTop entities by attribute count:"
575
+ entity_attrs.sort_by { |e| -e[:count] }
576
+ .take(10)
577
+ .each do |e|
578
+ puts " #{e[:path]}: #{e[:count]} attributes"
579
+ end
580
+ ----
581
+
582
+ === Step 9: Building Query Utilities
583
+
584
+ ==== Create a Query Helper
585
+
586
+ Create `query_helper.rb`:
587
+
588
+ [source,ruby]
589
+ ----
590
+ require 'expressir'
591
+
592
+ class SchemaQuery
593
+ def initialize(package_or_repo)
594
+ if package_or_repo.is_a?(String)
595
+ @repo = Expressir::Model::Repository.from_package(package_or_repo)
596
+ else
597
+ @repo = package_or_repo
598
+ end
599
+ @search = Expressir::Model::SearchEngine.new(@repo)
600
+ end
601
+
602
+ def find_entity(name, schema: nil)
603
+ opts = { pattern: name, type: 'entity', exact: true }
604
+ opts[:schema] = schema if schema
605
+ results = @search.search(opts)
606
+ results.first[:object] if results.any?
607
+ end
608
+
609
+ def find_entities_using_type(type_name)
610
+ results = []
611
+ @repo.schemas.each do |schema|
612
+ schema.entities.each do |entity|
613
+ entity.attributes.each do |attr|
614
+ if attr.type.respond_to?(:id) && attr.type.id == type_name
615
+ results << entity
616
+ end
617
+ end
618
+ end
619
+ end
620
+ results.uniq
621
+ end
622
+
623
+ def list_schemas
624
+ @repo.schemas.map(&:id)
625
+ end
626
+
627
+ def schema_stats(schema_id)
628
+ {
629
+ entities: @search.count(type: 'entity', schema: schema_id),
630
+ types: @search.count(type: 'type', schema: schema_id),
631
+ functions: @search.count(type: 'function', schema: schema_id)
632
+ }
633
+ end
634
+ end
635
+
636
+ # Usage
637
+ query = SchemaQuery.new('catalog.ler')
638
+
639
+ puts "Schemas: #{query.list_schemas.join(', ')}"
640
+
641
+ entity = query.find_entity('product')
642
+ puts "\nFound entity: #{entity.id}" if entity
643
+
644
+ query.list_schemas.each do |schema|
645
+ stats = query.schema_stats(schema)
646
+ puts "\n#{schema}:"
647
+ puts " Entities: #{stats[:entities]}"
648
+ puts " Types: #{stats[:types]}"
649
+ puts " Functions: #{stats[:functions]}"
650
+ end
651
+ ----
652
+
653
+ === Step 10: Practice Exercises
654
+
655
+ ==== Exercise 1: Find Orphan Entities
656
+
657
+ Write a query to find entities that are not referenced by any other entity.
658
+
659
+ Hint: Check all entity attributes to see which entity types are referenced.
660
+
661
+ ==== Exercise 2: Circular References
662
+
663
+ Detect if any entities have circular references (entity A references entity B, which references entity A).
664
+
665
+ ==== Exercise 3: Schema Dependency Graph
666
+
667
+ Create a visualization showing which schemas depend on which other schemas through interfaces.
668
+
669
+ ==== Exercise 4: Entity Complexity Score
670
+
671
+ Calculate a complexity score for each entity based on:
672
+ * Number of attributes (1 point each)
673
+ * Number of supertypes (2 points each)
674
+ * Number of WHERE rules (3 points each)
675
+
676
+ Find the 5 most complex entities.
677
+
678
+ === Common Patterns
679
+
680
+ ==== Safe Object Access
681
+
682
+ [source,ruby]
683
+ ----
684
+ # Always check if results exist
685
+ results = search.search(pattern: 'foo', type: 'entity')
686
+
687
+ if results.any?
688
+ entity = results.first[:object]
689
+ # Use entity safely
690
+ else
691
+ puts "Entity not found"
692
+ end
693
+ ----
694
+
695
+ ==== Handling Multiple Results
696
+
697
+ [source,ruby]
698
+ ----
699
+ results = search.search(pattern: 'product*', type: 'entity')
700
+
701
+ case results.size
702
+ when 0
703
+ puts "No matches found"
704
+ when 1
705
+ puts "Found: #{results.first[:path]}"
706
+ else
707
+ puts "Multiple matches (#{results.size}):"
708
+ results.each { |r| puts " - #{r[:path]}" }
709
+ end
710
+ ----
711
+
712
+ ==== Case-Insensitive by Default
713
+
714
+ [source,ruby]
715
+ ----
716
+ # Case-insensitive (default)
717
+ results = search.search(pattern: 'PRODUCT') # Finds 'product'
718
+
719
+ # Case-sensitive
720
+ results = search.search(pattern: 'PRODUCT', case_sensitive: true) # No match
721
+ ----
722
+
723
+ === Next Steps
724
+
725
+ Congratulations! You now know how to query EXPRESS schemas effectively.
726
+
727
+ **Continue learning**:
728
+
729
+ * link:liquid-templates.html[Liquid Templates] - Generate documentation from queries
730
+ * link:documentation-coverage.html[Documentation Coverage] - Analyze documentation
731
+ * link:../guides/ruby-api/[Ruby API Guides] - Advanced programmatic usage
732
+
733
+ **Read more**:
734
+
735
+ * link:../pages/data-model.html[Data Model] - Understanding the structure
736
+ * link:../references/[API References] - Complete API documentation
737
+
738
+ === Summary
739
+
740
+ In this tutorial, you learned to:
741
+
742
+ * ✅ Use SearchEngine for fast queries
743
+ * ✅ Apply pattern matching with wildcards and regex
744
+ * ✅ Filter by type, schema, and category
745
+ * ✅ Build complex combined queries
746
+ * ✅ Extract and analyze full objects
747
+ * ✅ Find relationships and dependencies
748
+ * ✅ Generate statistics and aggregations
749
+ * ✅ Create reusable query utilities
750
+
751
+ You can now effectively explore and analyze any EXPRESS schema!