mxrb 0.1.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.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+
5
+ module Mxrb
6
+ module Oql
7
+ # Parses SQL Server SHOWPLAN XML without executing or rewriting the query.
8
+ # Missing-index entries are optimizer hypotheses, not DDL instructions.
9
+ class SqlServerPlanAnalyzer # rubocop:disable Metrics/ClassLength
10
+ LARGE_ROWS = 1_000
11
+ HIGH_NESTED_ROWS = 10_000
12
+ MISESTIMATE_RATIO = 10.0
13
+
14
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
15
+ def analyze(xml)
16
+ document = REXML::Document.new(xml.to_s)
17
+ root = document.root
18
+ raise ArgumentError, 'invalid SQL Server SHOWPLAN XML' unless root&.name == 'ShowPlanXML'
19
+
20
+ findings = [*operator_findings(root), *missing_index_findings(root)].freeze
21
+ statement = elements(root, 'StmtSimple').first
22
+ runtime = elements(root, 'QueryTimeStats').first
23
+ PlanReport.new(
24
+ :sql_server, !runtime.nil?, nil, attribute_number(runtime, 'ElapsedTime'),
25
+ attribute_number(statement, 'StatementSubTreeCost'), findings, xml.to_s.freeze
26
+ )
27
+ rescue REXML::ParseException => e
28
+ raise ArgumentError, "invalid SQL Server SHOWPLAN XML: #{e.message}"
29
+ end
30
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
31
+
32
+ private
33
+
34
+ def operator_findings(root)
35
+ elements(root, 'RelOp').flat_map do |operator|
36
+ [
37
+ scan_finding(operator), nested_loop_finding(operator),
38
+ spill_finding(operator), misestimate_finding(operator)
39
+ ].compact
40
+ end
41
+ end
42
+
43
+ def scan_finding(operator)
44
+ physical = operator.attributes['PhysicalOp'].to_s
45
+ return unless ['Table Scan', 'Clustered Index Scan'].include?(physical)
46
+ return unless estimated_rows(operator) >= LARGE_ROWS
47
+
48
+ object = operator_object(operator)
49
+ PlanFinding.new(
50
+ :sql_server_scan, :warning, physical, object[:relation],
51
+ "#{physical} processes a large estimated row set.",
52
+ 'Review predicate selectivity and the actual plan; a scan can still be optimal.',
53
+ operator_metrics(operator).freeze, object[:indexes].freeze
54
+ )
55
+ end
56
+
57
+ def nested_loop_finding(operator)
58
+ return unless operator.attributes['PhysicalOp'] == 'Nested Loops'
59
+ return unless estimated_rows(operator) >= HIGH_NESTED_ROWS
60
+
61
+ operator_finding(
62
+ :sql_server_high_volume_nested_loop, operator,
63
+ 'A nested-loops join processes a high estimated row volume.',
64
+ 'Review indexes on inner join keys and compare hash or merge alternatives in the actual plan.'
65
+ )
66
+ end
67
+
68
+ def spill_finding(operator)
69
+ spill = descendants(operator).find do |element|
70
+ %w[SpillToTempDb SortSpillDetails HashSpillDetails].include?(element.name)
71
+ end
72
+ return unless spill
73
+
74
+ operator_finding(
75
+ :sql_server_tempdb_spill, operator,
76
+ 'The actual plan reports a spill to tempdb.',
77
+ 'Reduce intermediate rows and review memory grants, cardinality estimates, sorts, and hashes.'
78
+ )
79
+ end
80
+
81
+ def misestimate_finding(operator)
82
+ estimated = estimated_rows(operator)
83
+ actual = actual_rows(operator)
84
+ return if estimated.zero? || actual.zero?
85
+ return unless [estimated, actual].max >= 100 && ratio(estimated, actual) >= MISESTIMATE_RATIO
86
+
87
+ operator_finding(
88
+ :sql_server_cardinality_misestimation, operator,
89
+ 'Estimated and actual operator rows differ by at least 10x.',
90
+ 'Update statistics and inspect parameter sensitivity, predicates, and correlated columns.'
91
+ )
92
+ end
93
+
94
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
95
+ def missing_index_findings(root)
96
+ elements(root, 'MissingIndexGroup').map do |group|
97
+ index = elements(group, 'MissingIndex').first
98
+ columns = elements(group, 'Column').map do |column|
99
+ { name: clean_identifier(column.attributes['Name']),
100
+ usage: column.parent.attributes['Usage'] }.freeze
101
+ end
102
+ relation = qualified_object(index)
103
+ impact = attribute_number(group, 'Impact')
104
+ PlanFinding.new(
105
+ :sql_server_missing_index, :hint, 'MissingIndex', relation,
106
+ "SQL Server estimates a missing-index impact of #{impact}%.",
107
+ 'Treat this as a hypothesis: compare with existing indexes, write cost, and representative workload.',
108
+ { impact_percent: impact }.freeze, columns.freeze
109
+ )
110
+ end
111
+ end
112
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
113
+
114
+ def operator_finding(rule, operator, message, suggestion)
115
+ object = operator_object(operator)
116
+ PlanFinding.new(
117
+ rule, :warning, operator.attributes['PhysicalOp'], object[:relation], message, suggestion,
118
+ operator_metrics(operator).freeze, object[:indexes].freeze
119
+ )
120
+ end
121
+
122
+ def operator_object(operator)
123
+ object = descendants(operator).find { _1.name == 'Object' }
124
+ return { relation: nil, indexes: [] } unless object
125
+
126
+ index = clean_identifier(object.attributes['Index'])
127
+ {
128
+ relation: qualified_object(object),
129
+ indexes: index ? [{ name: index, definition: nil }.freeze] : []
130
+ }
131
+ end
132
+
133
+ def qualified_object(element)
134
+ return unless element
135
+
136
+ %w[Database Schema Table].filter_map do |name|
137
+ clean_identifier(element.attributes[name])
138
+ end.join('.')
139
+ end
140
+
141
+ def clean_identifier(value)
142
+ text = value.to_s
143
+ return if text.empty?
144
+
145
+ text.delete_prefix('[').delete_suffix(']')
146
+ end
147
+
148
+ def operator_metrics(operator)
149
+ {
150
+ estimated_rows: estimated_rows(operator), actual_rows: actual_rows(operator),
151
+ estimated_cost: attribute_number(operator, 'EstimatedTotalSubtreeCost'),
152
+ actual_executions: runtime_counters(operator).sum do |counter|
153
+ attribute_number(counter, 'ActualExecutions')
154
+ end
155
+ }
156
+ end
157
+
158
+ def estimated_rows(operator)
159
+ attribute_number(operator, 'EstimateRows')
160
+ end
161
+
162
+ def actual_rows(operator)
163
+ runtime_counters(operator).sum { attribute_number(_1, 'ActualRows') }
164
+ end
165
+
166
+ def runtime_counters(operator)
167
+ descendants(operator).take_while { _1.name != 'RelOp' }
168
+ .select { _1.name == 'RunTimeCountersPerThread' }
169
+ end
170
+
171
+ def descendants(element)
172
+ result = []
173
+ element.each_recursive { result << _1 }
174
+ result
175
+ end
176
+
177
+ def elements(element, name)
178
+ descendants(element).select { _1.name == name }
179
+ end
180
+
181
+ def attribute_number(element, name)
182
+ element ? element.attributes[name].to_f : 0.0
183
+ end
184
+
185
+ def ratio(left, right)
186
+ [left, right].max / [left, right].min
187
+ end
188
+ end # rubocop:enable Metrics/ClassLength
189
+ end
190
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Oql
5
+ SqlServerWorkloadQuery = Data.define(
6
+ :query_id, :query, :calls, :total_time_ms, :mean_time_ms, :rows,
7
+ :logical_reads, :physical_reads, :temp_writes, :io_time_ms, :cache_hit_ratio
8
+ )
9
+
10
+ # Normalizes read-only SQL Server DMV snapshots into the shared workload report.
11
+ class SqlServerWorkloadAnalyzer
12
+ def analyze(query_rows:, table_rows:, index_rows:)
13
+ queries = query_rows.map { query_entry(_1) }.freeze
14
+ findings = [
15
+ *queries.filter_map { query_finding(_1) },
16
+ *table_rows.filter_map { table_finding(_1) },
17
+ *index_rows.filter_map { index_finding(_1) }
18
+ ].freeze
19
+ WorkloadReport.new(:sql_server, queries, table_rows.freeze, index_rows.freeze, findings)
20
+ end
21
+
22
+ private
23
+
24
+ def query_entry(row) # rubocop:disable Metrics/AbcSize
25
+ calls = row['execution_count'].to_i
26
+ total = row['total_elapsed_time'].to_f / 1_000.0
27
+ SqlServerWorkloadQuery.new(
28
+ row['query_hash'], row['query_text'], calls, total,
29
+ calls.zero? ? 0.0 : total / calls, row['total_rows'].to_i,
30
+ row['total_logical_reads'].to_i, row['total_physical_reads'].to_i,
31
+ 0, 0.0, cache_ratio(row)
32
+ )
33
+ end
34
+
35
+ def cache_ratio(row)
36
+ logical = row['total_logical_reads'].to_i
37
+ physical = row['total_physical_reads'].to_i
38
+ return 1.0 if logical.zero?
39
+
40
+ (1.0 - physical.to_f / logical).clamp(0.0, 1.0).round(4)
41
+ end
42
+
43
+ def query_finding(query)
44
+ return unless query.total_time_ms >= 1_000 || query.mean_time_ms >= 100
45
+
46
+ WorkloadFinding.new(
47
+ :sql_server_expensive_query, :warning, query.query_id,
48
+ 'The DMV snapshot reports substantial elapsed time for this query fingerprint.',
49
+ 'Inspect Query Store history and the actual plan for regressions and parameter sensitivity.',
50
+ query.to_h.freeze
51
+ )
52
+ end
53
+
54
+ def table_finding(row)
55
+ scans = row['user_scans'].to_i
56
+ seeks = row['user_seeks'].to_i
57
+ return unless scans > seeks && scans >= 100
58
+
59
+ WorkloadFinding.new(
60
+ :sql_server_scan_pressure, :warning, row['relation'],
61
+ 'Index usage DMVs report more scans than seeks.',
62
+ 'Correlate the table with expensive fingerprints before changing indexes.',
63
+ { user_scans: scans, user_seeks: seeks }.freeze
64
+ )
65
+ end
66
+
67
+ def index_finding(row)
68
+ return unless row['user_seeks'].to_i.zero? && row['user_scans'].to_i.zero?
69
+ return unless row['index_bytes'].to_i >= 1_048_576
70
+
71
+ WorkloadFinding.new(
72
+ :sql_server_unused_large_index, :warning, row['index_name'],
73
+ 'A large index has no seeks or scans in the current DMV window.',
74
+ 'Confirm restart time and representative workload before considering removal.',
75
+ { index_bytes: row['index_bytes'].to_i }.freeze
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Oql
5
+ WorkloadQuery = Data.define(
6
+ :query_id, :query, :calls, :total_time_ms, :mean_time_ms, :rows,
7
+ :shared_hits, :shared_reads, :temp_writes, :io_time_ms, :cache_hit_ratio
8
+ )
9
+ WorkloadFinding = Data.define(:rule, :severity, :subject, :message, :suggestion, :metrics)
10
+ WorkloadReport = Data.define(:engine, :queries, :table_stats, :index_stats, :findings) do
11
+ def clean? = findings.none? { _1.severity == :warning }
12
+ def warnings? = !clean?
13
+ end
14
+
15
+ # Interprets cumulative PostgreSQL statistics without pretending that one
16
+ # threshold is universally optimal. Findings retain the measured values so
17
+ # callers can rank them against their own latency and throughput budgets.
18
+ class WorkloadAnalyzer # rubocop:disable Metrics/ClassLength
19
+ TOTAL_TIME_MS = 1_000.0
20
+ MEAN_TIME_MS = 100.0
21
+ MIN_BUFFER_BLOCKS = 100
22
+ MIN_CACHE_HIT_RATIO = 0.90
23
+ HIGH_ROWS_PER_CALL = 10_000
24
+ LARGE_UNUSED_INDEX_BYTES = 1_048_576
25
+ HEAVY_SEQ_ROWS = 100_000
26
+
27
+ DEFAULT_THRESHOLDS = {
28
+ total_time_ms: TOTAL_TIME_MS, mean_time_ms: MEAN_TIME_MS,
29
+ min_buffer_blocks: MIN_BUFFER_BLOCKS, min_cache_hit_ratio: MIN_CACHE_HIT_RATIO,
30
+ high_rows_per_call: HIGH_ROWS_PER_CALL,
31
+ large_unused_index_bytes: LARGE_UNUSED_INDEX_BYTES, heavy_seq_rows: HEAVY_SEQ_ROWS
32
+ }.freeze
33
+
34
+ def initialize(**thresholds)
35
+ unknown = thresholds.keys - DEFAULT_THRESHOLDS.keys
36
+ raise ArgumentError, "unknown workload thresholds: #{unknown.join(', ')}" unless unknown.empty?
37
+
38
+ @thresholds = DEFAULT_THRESHOLDS.merge(thresholds).freeze
39
+ end
40
+
41
+ def analyze(query_rows:, table_rows:, index_rows:)
42
+ queries = query_rows.map { query_entry(_1) }.freeze
43
+ findings = [
44
+ *queries.flat_map { query_findings(_1) },
45
+ *table_rows.filter_map { table_finding(_1) },
46
+ *index_rows.filter_map { index_finding(_1) }
47
+ ].freeze
48
+ WorkloadReport.new(:postgresql, queries, table_rows.freeze, index_rows.freeze, findings)
49
+ end
50
+
51
+ private
52
+
53
+ def query_entry(row) # rubocop:disable Metrics/AbcSize
54
+ hits = integer(row['shared_blks_hit'])
55
+ reads = integer(row['shared_blks_read'])
56
+ total_blocks = hits + reads
57
+ ratio = total_blocks.zero? ? 1.0 : hits.to_f / total_blocks
58
+ WorkloadQuery.new(
59
+ row['queryid'], row['query'], integer(row['calls']), float(row['total_exec_time']),
60
+ float(row['mean_exec_time']), integer(row['rows']), hits, reads,
61
+ integer(row['temp_blks_written']), float(row['blk_read_time']) + float(row['blk_write_time']),
62
+ ratio.round(4)
63
+ )
64
+ end
65
+
66
+ def query_findings(entry)
67
+ [
68
+ cumulative_time(entry), slow_mean(entry), low_cache_hit(entry),
69
+ temporary_writes(entry), high_rows(entry)
70
+ ].compact
71
+ end
72
+
73
+ def cumulative_time(entry)
74
+ return unless entry.total_time_ms >= threshold(:total_time_ms)
75
+
76
+ query_finding(
77
+ :high_cumulative_time, entry,
78
+ 'This query fingerprint consumes substantial cumulative execution time.',
79
+ 'Prioritize it by total time, then inspect its real plan with db explain --analyze.'
80
+ )
81
+ end
82
+
83
+ def slow_mean(entry)
84
+ return unless entry.mean_time_ms >= threshold(:mean_time_ms)
85
+
86
+ query_finding(
87
+ :high_mean_time, entry, 'Mean execution time exceeds 100 ms.',
88
+ 'Inspect plan stability, cardinality estimates, locks, I/O, and returned row volume.'
89
+ )
90
+ end
91
+
92
+ def low_cache_hit(entry)
93
+ blocks = entry.shared_hits + entry.shared_reads
94
+ return unless blocks >= threshold(:min_buffer_blocks) &&
95
+ entry.cache_hit_ratio < threshold(:min_cache_hit_ratio)
96
+
97
+ query_finding(
98
+ :low_cache_hit, entry, 'A significant share of shared blocks came from storage.',
99
+ 'Check working-set size, access locality, indexes, and whether the query reads excess rows.'
100
+ )
101
+ end
102
+
103
+ def temporary_writes(entry)
104
+ return unless entry.temp_writes.positive?
105
+
106
+ query_finding(
107
+ :temporary_block_writes, entry, 'The query wrote temporary blocks.',
108
+ 'Inspect sorts and hashes, reduce intermediate rows, and review work_mem per workload.'
109
+ )
110
+ end
111
+
112
+ def high_rows(entry)
113
+ return if entry.calls.zero? || entry.rows / entry.calls < threshold(:high_rows_per_call)
114
+
115
+ query_finding(
116
+ :high_rows_per_call, entry, 'The query returns or processes many rows per call.',
117
+ 'Project only required columns and rows; consider pagination or a narrower aggregation.'
118
+ )
119
+ end
120
+
121
+ def query_finding(rule, entry, message, suggestion)
122
+ WorkloadFinding.new(
123
+ rule, :warning, entry.query_id, message, suggestion,
124
+ {
125
+ calls: entry.calls, total_time_ms: entry.total_time_ms,
126
+ mean_time_ms: entry.mean_time_ms, rows: entry.rows,
127
+ cache_hit_ratio: entry.cache_hit_ratio, temp_writes: entry.temp_writes,
128
+ io_time_ms: entry.io_time_ms
129
+ }.freeze
130
+ )
131
+ end
132
+
133
+ def table_finding(row) # rubocop:disable Metrics/MethodLength
134
+ seq_rows = integer(row['seq_tup_read'])
135
+ seq_scans = integer(row['seq_scan'])
136
+ index_scans = integer(row['idx_scan'])
137
+ return unless seq_rows >= threshold(:heavy_seq_rows) && seq_scans > index_scans
138
+
139
+ WorkloadFinding.new(
140
+ :table_sequential_pressure, :warning, qualified(row, 'relname'),
141
+ 'Cumulative table statistics show more sequential than index scans.',
142
+ 'Inspect the highest-cost query fingerprints before adding or changing indexes.',
143
+ { seq_scan: seq_scans, seq_tup_read: seq_rows, idx_scan: index_scans,
144
+ live_rows: integer(row['n_live_tup']) }.freeze
145
+ )
146
+ end
147
+
148
+ def index_finding(row)
149
+ return if truthy?(row['indisunique']) || truthy?(row['indisprimary'])
150
+
151
+ size = integer(row['index_bytes'])
152
+ return unless integer(row['idx_scan']).zero? && size >= threshold(:large_unused_index_bytes)
153
+
154
+ WorkloadFinding.new(
155
+ :unused_large_index, :warning, qualified(row, 'indexrelname'),
156
+ 'A non-unique index occupies at least 1 MiB but has no scans in the current statistics window.',
157
+ 'Confirm the statistics reset time and production workload before considering removal.',
158
+ { idx_scan: 0, index_bytes: size, idx_tup_read: integer(row['idx_tup_read']) }.freeze
159
+ )
160
+ end
161
+
162
+ def qualified(row, name_key)
163
+ [row['schemaname'], row[name_key]].compact.join('.')
164
+ end
165
+
166
+ def truthy?(value)
167
+ %w[t true 1].include?(value.to_s.downcase)
168
+ end
169
+
170
+ def integer(value) = value.to_i
171
+ def float(value) = value.to_f
172
+ def threshold(name) = @thresholds.fetch(name)
173
+ end # rubocop:enable Metrics/ClassLength
174
+ end
175
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+
6
+ module Mxrb
7
+ module Oql
8
+ WorkloadDelta = Data.define(:query_id, :metric, :before, :after, :change_percent)
9
+ WorkloadComparison = Data.define(:deltas) do
10
+ def regressions = deltas.select { _1.change_percent.positive? }
11
+ def improvements = deltas.select { _1.change_percent.negative? }
12
+ end
13
+
14
+ # Serializable workload snapshot and deterministic regression comparison.
15
+ module WorkloadBaseline
16
+ module_function
17
+
18
+ METRICS = %w[total_time_ms mean_time_ms io_time_ms temp_writes rows].freeze
19
+
20
+ def dump(report)
21
+ JSON.pretty_generate(
22
+ version: 1, engine: report.engine, captured_at: Time.now.utc.iso8601,
23
+ queries: report.queries.to_h { [_1.query_id.to_s, _1.to_h] }
24
+ ) << "\n"
25
+ end
26
+
27
+ def load(source)
28
+ payload = source.is_a?(Hash) ? source : JSON.parse(File.read(source))
29
+ raise ArgumentError, 'unsupported workload baseline version' unless payload['version'] == 1
30
+
31
+ payload
32
+ rescue JSON::ParserError, Errno::ENOENT => e
33
+ raise ArgumentError, "cannot read workload baseline: #{e.message}"
34
+ end
35
+
36
+ def compare(report, baseline)
37
+ previous = load(baseline).fetch('queries')
38
+ deltas = report.queries.flat_map { query_deltas(_1, previous[_1.query_id.to_s]) }
39
+ WorkloadComparison.new(deltas.freeze)
40
+ end
41
+
42
+ def query_deltas(query, before)
43
+ return [] unless before
44
+
45
+ METRICS.filter_map do |metric|
46
+ delta(query.query_id, metric, Float(before.fetch(metric)), Float(query.to_h.fetch(metric.to_sym)))
47
+ end
48
+ end
49
+ private_class_method :query_deltas
50
+
51
+ def delta(query_id, metric, before, after)
52
+ return if before.zero? && after.zero?
53
+
54
+ percent = before.zero? ? 100.0 : ((after - before) * 100.0 / before).round(2)
55
+ WorkloadDelta.new(query_id, metric.to_sym, before, after, percent)
56
+ end
57
+ private_class_method :delta
58
+ end
59
+ end
60
+ end