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
data/lib/mxrb/oql.rb ADDED
@@ -0,0 +1,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ # Read-only discovery and SQL visualization for native OQL stored in an MPR.
5
+ # Generated SQL is never executed by MXRB.
6
+ module Oql
7
+ Query = Data.define(
8
+ :id, :qualified_name, :name, :kind, :oql, :unit_id, :path, :source_type, :parameters
9
+ )
10
+
11
+ SqlView = Data.define(
12
+ :query, :sql, :dialect, :confidence, :warnings, :parameters
13
+ ) do
14
+ def supported? = !sql.nil?
15
+ end
16
+
17
+ Finding = Data.define(:rule, :severity, :fragment, :message, :suggestions)
18
+
19
+ AnalysisReport = Data.define(:query, :findings) do
20
+ def clean? = findings.none? { _1.severity == :error }
21
+ def warnings? = findings.any? { _1.severity == :warning }
22
+
23
+ def for_dialect(dialect)
24
+ key = dialect.to_sym
25
+ findings.map { [_1, _1.suggestions[key]] }.freeze
26
+ end
27
+ end
28
+
29
+ # Finds OQL-bearing native model elements without treating arbitrary strings as queries.
30
+ class Catalog
31
+ def initialize(project)
32
+ @project = project
33
+ end
34
+
35
+ def queries
36
+ @queries ||= begin
37
+ units = @project.all_units
38
+ units_by_id = units.to_h { [_1['UnitID'], _1] }
39
+ module_by_id = @project.modules.to_h { [_1.id, _1.name] }
40
+ units.flat_map do |raw|
41
+ document = @project.parse_bson(raw)
42
+ module_name = ancestor_module(raw, units_by_id, module_by_id)
43
+ discover(document, raw, module_name)
44
+ end.sort_by(&:qualified_name).freeze
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Recursive BSON discovery is intentionally kept together so that its
51
+ # source-type guard and ownership context cannot diverge.
52
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
53
+ def discover(document, raw, module_name)
54
+ found = []
55
+ walk(document) do |node, path, ancestors|
56
+ source_type = node['$Type'].to_s
57
+ if source_type.match?(/Oql/i) && node['Query'].is_a?(String)
58
+ found << build_query(
59
+ node['Query'], raw, module_name, path + ['Query'], source_type, ancestors
60
+ )
61
+ end
62
+ node.each do |key, value|
63
+ next unless key.to_s.match?(/\AOqlQuery\z/i) && value.is_a?(String)
64
+
65
+ found << build_query(value, raw, module_name, path + [key], source_type, ancestors)
66
+ end
67
+ end
68
+ found.uniq { [_1.unit_id, _1.path] }
69
+ end
70
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
71
+
72
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
73
+ def walk(node, path = [], ancestors = [], &block)
74
+ return unless node.is_a?(Hash)
75
+
76
+ named = node['Name'] || node['name']
77
+ current_ancestors = named ? ancestors + [node] : ancestors
78
+ yield(node, path, current_ancestors)
79
+ node.each do |key, value|
80
+ case value
81
+ when Hash
82
+ walk(value, path + [key], current_ancestors, &block)
83
+ when Array
84
+ value.each_with_index do |child, index|
85
+ walk(child, path + [key, index], current_ancestors, &block)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
91
+
92
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
93
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
94
+ def build_query(oql, raw, module_name, path, source_type, ancestors)
95
+ owner = ancestors.last || {}
96
+ name = (owner['Name'] || owner['name'] || "OQL_#{raw['UnitID'].delete('-')[0, 8]}").to_s
97
+ unit_type = @project.parse_bson(raw)['$Type'].to_s
98
+ kind = if unit_type == 'DataSets$DataSet'
99
+ :dataset
100
+ elsif owner['$Type'].to_s.match?(/Entity/i)
101
+ :view_entity
102
+ else
103
+ :oql
104
+ end
105
+ id = Mxrb::IO::BsonCodec.extract_id(owner['$ID']) || raw['UnitID']
106
+ qualified_name = module_name ? "#{module_name}.#{name}" : name
107
+ parameters = Translator.parameters(oql)
108
+ Query.new(
109
+ id, qualified_name, name, kind, oql.freeze, raw['UnitID'],
110
+ path.map(&:to_s).freeze, source_type, parameters
111
+ )
112
+ end
113
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
114
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
115
+
116
+ def ancestor_module(raw, units_by_id, module_by_id)
117
+ current = raw
118
+ visited = {}
119
+ loop do
120
+ container = current['ContainerID']
121
+ return module_by_id[container] if module_by_id.key?(container)
122
+ return nil if container.nil? || visited[container]
123
+
124
+ visited[container] = true
125
+ current = units_by_id[container]
126
+ return nil unless current
127
+ end
128
+ end
129
+ end
130
+
131
+ # Converts the deliberately small, safe OQL read subset into inspectable SQL.
132
+ # rubocop:disable Metrics/ClassLength
133
+ class Translator
134
+ DIALECTS = %i[ansi postgresql sql_server].freeze
135
+ Token = Data.define(:type, :text)
136
+ SPACE = /\s/
137
+ WORD_START = /[A-Za-z_]/
138
+ WORD_BODY = /[A-Za-z0-9_$]/
139
+ TAIL_CLAUSES = %w[ORDER LIMIT OFFSET UNION].freeze
140
+ ALIAS_STOP_WORDS = %w[
141
+ FULL GROUP HAVING INNER JOIN LEFT LIMIT OFFSET ON ORDER OUTER RIGHT
142
+ SELECT UNION WHERE
143
+ ].freeze
144
+
145
+ def initialize(dialect: :postgresql)
146
+ @dialect = dialect.to_sym
147
+ raise ArgumentError, "unsupported SQL dialect #{@dialect.inspect}" unless DIALECTS.include?(@dialect)
148
+ end
149
+
150
+ def self.parameters(source)
151
+ new.send(:tokenize, source).filter_map do |token|
152
+ token.text.delete_prefix('$') if token.type == :parameter
153
+ end.uniq.freeze
154
+ end
155
+
156
+ def translate_source(source, name: 'AdHoc')
157
+ text = source.to_s
158
+ query = Query.new(
159
+ 'adhoc', name, name, :oql, text.freeze, nil, [].freeze,
160
+ 'Oql$AdHoc', self.class.parameters(text)
161
+ )
162
+ translate(query)
163
+ end
164
+
165
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
166
+ def translate(query)
167
+ tokens = tokenize(query.oql)
168
+ warning = validation_warning(tokens)
169
+ return unsupported(query, warning) if warning
170
+
171
+ tokens = reorder_from_first(tokens)
172
+ association = association_path(tokens)
173
+ return unsupported(query, association) if association
174
+
175
+ aliases = translate_entities!(tokens)
176
+ translate_attributes!(tokens, aliases)
177
+ translate_parameters!(tokens)
178
+ sql = tokens.map(&:text).join.strip
179
+ SqlView.new(
180
+ query, sql.freeze, @dialect, :logical,
181
+ [physical_mapping_warning].freeze, query.parameters
182
+ )
183
+ end
184
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
185
+
186
+ private
187
+
188
+ def tokenize(source)
189
+ tokens = []
190
+ index = 0
191
+ while index < source.length
192
+ type, text, index = next_token(source, index)
193
+ tokens << Token.new(type, text)
194
+ end
195
+ tokens
196
+ end
197
+
198
+ # Lexical branching is explicit to keep strings and comments opaque.
199
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
200
+ def next_token(source, index)
201
+ char = source[index]
202
+ return consume_while(source, index, :space) { SPACE.match?(_1) } if SPACE.match?(char)
203
+ return consume_quoted(source, index, "'", :string) if char == "'"
204
+ return consume_quoted(source, index, '"', :quoted_identifier) if char == '"'
205
+ return consume_line_comment(source, index) if source[index, 2] == '--'
206
+ return consume_block_comment(source, index) if source[index, 2] == '/*'
207
+ return consume_parameter(source, index) if char == '$' && source[index + 1]&.match?(WORD_START)
208
+ return consume_while(source, index, :word) { WORD_BODY.match?(_1) } if WORD_START.match?(char)
209
+
210
+ [:symbol, char, index + 1]
211
+ end
212
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
213
+
214
+ def consume_while(source, index, type)
215
+ finish = index
216
+ finish += 1 while finish < source.length && yield(source[finish])
217
+ [type, source[index...finish], finish]
218
+ end
219
+
220
+ # rubocop:disable Metrics/MethodLength
221
+ def consume_quoted(source, index, delimiter, type)
222
+ finish = index + 1
223
+ while finish < source.length
224
+ if source[finish] == delimiter
225
+ finish += 1
226
+ if source[finish] == delimiter
227
+ finish += 1
228
+ next
229
+ end
230
+ break
231
+ end
232
+ finish += 1
233
+ end
234
+ [type, source[index...finish], finish]
235
+ end
236
+ # rubocop:enable Metrics/MethodLength
237
+
238
+ def consume_line_comment(source, index)
239
+ finish = source.index("\n", index) || source.length
240
+ [:comment, source[index...finish], finish]
241
+ end
242
+
243
+ def consume_block_comment(source, index)
244
+ finish = source.index('*/', index + 2)
245
+ finish = finish ? finish + 2 : source.length
246
+ [:comment, source[index...finish], finish]
247
+ end
248
+
249
+ def consume_parameter(source, index)
250
+ _type, text, finish = consume_while(source, index + 1, :word) { WORD_BODY.match?(_1) }
251
+ [:parameter, "$#{text}", finish]
252
+ end
253
+
254
+ def validation_warning(tokens)
255
+ significant = tokens.reject { %i[space comment].include?(_1.type) }
256
+ first = significant.first&.text.to_s.upcase
257
+ return 'only read-only OQL SELECT queries can be visualized' unless %w[SELECT FROM].include?(first)
258
+ return 'multiple OQL statements are not accepted' if significant.any? { _1.text == ';' }
259
+
260
+ nil
261
+ end
262
+
263
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
264
+ def reorder_from_first(tokens)
265
+ first = significant_indices(tokens).first
266
+ return tokens unless tokens[first].text.casecmp?('FROM')
267
+
268
+ select_index = top_level_keyword(tokens, 'SELECT')
269
+ return tokens unless select_index
270
+
271
+ tail_index = TAIL_CLAUSES.filter_map do |keyword|
272
+ top_level_keyword(tokens, keyword, after: select_index + 1)
273
+ end.min || tokens.length
274
+ select_part = tokens[select_index...tail_index]
275
+ from_part = tokens[0...select_index]
276
+ tail_part = tokens[tail_index..]
277
+ select_part + [Token.new(:space, "\n")] + from_part + tail_part
278
+ end
279
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
280
+
281
+ def significant_indices(tokens)
282
+ tokens.each_index.reject { %i[space comment].include?(tokens[_1].type) }
283
+ end
284
+
285
+ # rubocop:disable Metrics/CyclomaticComplexity
286
+ def top_level_keyword(tokens, keyword, after: 0)
287
+ depth = 0
288
+ tokens.each_index do |index|
289
+ token = tokens[index]
290
+ depth += 1 if token.text == '('
291
+ depth -= 1 if token.text == ')'
292
+ next if index < after || depth != 0 || token.type != :word
293
+ return index if token.text.casecmp?(keyword)
294
+ end
295
+ nil
296
+ end
297
+ # rubocop:enable Metrics/CyclomaticComplexity
298
+
299
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
300
+ def association_path(tokens)
301
+ significant_indices(tokens).each do |index|
302
+ next unless tokens[index].type == :word && tokens[index].text.match?(/\A(?:FROM|JOIN)\z/i)
303
+
304
+ following = next_significant(tokens, index)
305
+ next if following.nil? || tokens[following].text == '('
306
+
307
+ separator = next_significant(tokens, following)
308
+ if separator && tokens[separator].text == '/'
309
+ return 'association-path JOINs require Mendix Runtime storage metadata'
310
+ end
311
+ end
312
+ nil
313
+ end
314
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
315
+
316
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
317
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
318
+ def translate_entities!(tokens)
319
+ aliases = {}
320
+ significant_indices(tokens).each do |index|
321
+ next unless tokens[index].type == :word && tokens[index].text.match?(/\A(?:FROM|JOIN)\z/i)
322
+
323
+ first = next_significant(tokens, index)
324
+ next if first.nil? || tokens[first].text == '('
325
+
326
+ dot = next_significant(tokens, first)
327
+ entity = dot && next_significant(tokens, dot)
328
+ next unless entity && tokens[dot].text == '.' && identifier?(tokens[first]) &&
329
+ identifier?(tokens[entity])
330
+
331
+ module_name = identifier_text(tokens[first])
332
+ entity_name = identifier_text(tokens[entity])
333
+ tokens[first] = Token.new(:quoted_identifier, quote_table(module_name, entity_name))
334
+ tokens[dot] = Token.new(:symbol, '')
335
+ tokens[entity] = Token.new(:word, '')
336
+ alias_index = next_significant(tokens, entity)
337
+ alias_index = next_significant(tokens, alias_index) \
338
+ if alias_index && tokens[alias_index].text.casecmp?('AS')
339
+ alias_name = if alias_index && identifier?(tokens[alias_index]) &&
340
+ !ALIAS_STOP_WORDS.include?(tokens[alias_index].text.upcase)
341
+ identifier_text(tokens[alias_index])
342
+ else
343
+ entity_name
344
+ end
345
+ aliases[alias_name.downcase] = true
346
+ end
347
+ aliases
348
+ end
349
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
350
+ # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
351
+
352
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
353
+ def translate_attributes!(tokens, aliases)
354
+ significant_indices(tokens).each do |index|
355
+ token = tokens[index]
356
+ next unless identifier?(token) && aliases[identifier_text(token).downcase]
357
+
358
+ separator = next_significant(tokens, index)
359
+ attribute = separator && next_significant(tokens, separator)
360
+ next unless attribute && %w[. /].include?(tokens[separator].text) &&
361
+ identifier?(tokens[attribute])
362
+
363
+ tokens[separator] = Token.new(:symbol, '.')
364
+ tokens[attribute] = Token.new(
365
+ :quoted_identifier, quote_attribute(identifier_text(tokens[attribute]))
366
+ )
367
+ end
368
+ end
369
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
370
+
371
+ def translate_parameters!(tokens)
372
+ tokens.map! do |token|
373
+ token.type == :parameter ? Token.new(:parameter, ":#{token.text.delete_prefix('$')}") : token
374
+ end
375
+ end
376
+
377
+ def identifier?(token)
378
+ %i[word quoted_identifier].include?(token.type)
379
+ end
380
+
381
+ def identifier_text(token)
382
+ return token.text unless token.type == :quoted_identifier
383
+
384
+ token.text[1...-1].gsub('""', '"')
385
+ end
386
+
387
+ def next_significant(tokens, index)
388
+ ((index + 1)...tokens.length).find do |candidate|
389
+ !%i[space comment].include?(tokens[candidate].type)
390
+ end
391
+ end
392
+
393
+ def quote_table(module_name, entity_name)
394
+ logical = if @dialect == :ansi
395
+ "#{module_name}.#{entity_name}"
396
+ else
397
+ "#{module_name.downcase}$#{entity_name.downcase}"
398
+ end
399
+ quote_identifier(logical)
400
+ end
401
+
402
+ def quote_identifier(value)
403
+ return "[#{value.gsub(']', ']]')}]" if @dialect == :sql_server
404
+
405
+ %("#{value.gsub('"', '""')}")
406
+ end
407
+
408
+ def quote_attribute(value)
409
+ quote_identifier(@dialect == :ansi ? value : value.downcase)
410
+ end
411
+
412
+ def physical_mapping_warning
413
+ 'logical table and column names are inferred; verify them against the Runtime database schema'
414
+ end
415
+
416
+ def unsupported(query, warning)
417
+ SqlView.new(query, nil, @dialect, :unsupported, [warning].freeze, query.parameters)
418
+ end
419
+ end
420
+ # rubocop:enable Metrics/ClassLength
421
+ end
422
+ end
423
+
424
+ require_relative 'oql/analyzer'
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tmpdir'
4
+
5
+ module Mxrb
6
+ UpgradeResult = Data.define(:path, :from, :to, :applied)
7
+ MigrationResult = Data.define(:current, :generated, :diff) do
8
+ def clean? = diff.added.empty? && diff.removed.empty? && diff.changed.empty?
9
+ end
10
+
11
+ # Inspects, previews upgrades, and compares generated definitions with an MPR.
12
+ class ProjectLifecycle
13
+ VERSION = /\A\d+\.\d+\.\d+\z/
14
+
15
+ def initialize(root = Dir.pwd)
16
+ @root = File.expand_path(root)
17
+ @project_file = File.join(@root, 'project.rb')
18
+ end
19
+
20
+ def inspect
21
+ {
22
+ root: @root, project_file: File.file?(@project_file),
23
+ declared_version: declared_version,
24
+ modules: Dir[File.join(@root, 'modules', '*', 'module.rb')].map { File.basename(File.dirname(_1)) },
25
+ mprs: Dir[File.join(@root, '*.mpr')].sort,
26
+ registered_scaffolds: Scaffold::Registry.new(@root).entries.keys.sort
27
+ }
28
+ end
29
+
30
+ def upgrade(version, apply: false) # rubocop:disable Metrics/MethodLength
31
+ target = version.to_s
32
+ raise ArgumentError, 'Mendix version must use MAJOR.MINOR.PATCH' unless VERSION.match?(target)
33
+
34
+ source = project_source
35
+ current = declared_version
36
+ raise ArgumentError, 'project.rb has no mendix_version declaration' unless current
37
+
38
+ if apply
39
+ updated = source.sub(/mendix_version\s+["'][^"']+["']/, %(mendix_version "#{target}"))
40
+ transaction = Scaffold::Transaction.new
41
+ transaction.write(@project_file, updated)
42
+ transaction.commit
43
+ end
44
+ UpgradeResult.new(@project_file, current, target, apply)
45
+ end
46
+
47
+ def migration_plan # rubocop:disable Metrics/MethodLength
48
+ current = Dir[File.join(@root, '*.mpr')].first
49
+ raise ArgumentError, 'no current MPR found' unless current
50
+
51
+ Dir.mktmpdir('mxrb-migration-') do |dir|
52
+ generated = File.join(dir, File.basename(current))
53
+ previous = ENV['MXRB_OUTPUT_PATH']
54
+ ENV['MXRB_OUTPUT_PATH'] = generated
55
+ load @project_file
56
+ MigrationResult.new(current, generated, Mxrb.diff(current, generated))
57
+ ensure
58
+ previous.nil? ? ENV.delete('MXRB_OUTPUT_PATH') : ENV['MXRB_OUTPUT_PATH'] = previous
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def project_source
65
+ raise ArgumentError, "#{@project_file}: project.rb not found" unless File.file?(@project_file)
66
+
67
+ File.read(@project_file)
68
+ end
69
+
70
+ def declared_version
71
+ return unless File.file?(@project_file)
72
+
73
+ File.read(@project_file)[/mendix_version\s+["']([^"']+)["']/, 1]
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ # Preview/apply orchestration for verified protocol connector packages.
5
+ module Protocols
6
+ Request = Data.define(:protocol, :version, :marketplace_id)
7
+ Adapter = Data.define(:installer, :api)
8
+
9
+ # Preview/apply boundary for a real official Marketplace connector. Planning
10
+ # never writes. Apply is available only when an authenticated Content API and
11
+ # an official installer were explicitly supplied by the caller.
12
+ class Plan
13
+ attr_reader :request, :entry, :mendix_version, :package
14
+
15
+ def initialize(request:, entry:, context:)
16
+ @request = request
17
+ @entry = entry
18
+ @mendix_version = context.fetch(:mendix_version).to_s
19
+ @adapter = context[:adapter]
20
+ @package = context[:package]
21
+ @applied = false
22
+ end
23
+
24
+ def changes
25
+ [{
26
+ action: :install, protocol: entry.protocol, name: entry.name,
27
+ marketplace_id: entry.marketplace_id,
28
+ version: package&.version || request.version || 'latest-compatible',
29
+ mendix_version:, source_url: entry.source_url
30
+ }.freeze].freeze
31
+ end
32
+
33
+ def blocked_reasons
34
+ [].tap do |reasons|
35
+ reasons << 'official Marketplace adapter was not provided' unless @adapter
36
+ reasons << 'official Marketplace version was not resolved' unless package
37
+ end.freeze
38
+ end
39
+
40
+ def safe? = blocked_reasons.empty?
41
+ def applied? = @applied
42
+
43
+ def apply!
44
+ raise MarketplaceError, "connector plan is blocked: #{blocked_reasons.join('; ')}" unless safe?
45
+ raise MarketplaceError, 'connector plan was already applied' if applied?
46
+
47
+ result = @adapter.installer.pull_official(
48
+ entry.marketplace_id, version: request.version, mendix_version:, api: @adapter.api
49
+ )
50
+ @applied = true
51
+ result
52
+ end
53
+ end
54
+
55
+ module_function
56
+
57
+ def request(protocol, version: nil, marketplace_id: nil)
58
+ Request.new(
59
+ protocol: normalize_protocol(protocol), version: version&.to_s,
60
+ marketplace_id: marketplace_id&.to_s
61
+ )
62
+ end
63
+
64
+ def adapter(installer:, api:) = Adapter.new(installer:, api:)
65
+
66
+ def plan(protocol, mendix_version:, version: nil, marketplace_id: nil, adapter: nil)
67
+ connector_request = request(protocol, version:, marketplace_id:)
68
+ entry = planning_entry(connector_request)
69
+ package = adapter&.api&.resolve(
70
+ entry.marketplace_id, version: connector_request.version, mendix_version:
71
+ )
72
+ Plan.new(
73
+ request: connector_request, entry:,
74
+ context: { mendix_version:, adapter:, package: }
75
+ )
76
+ end
77
+
78
+ def planning_entry(request)
79
+ candidates = find_by_protocol(request.protocol)
80
+ candidates = candidates.select { _1.marketplace_id == request.marketplace_id } if request.marketplace_id
81
+ raise MarketplaceError, "no verified Marketplace connector for #{request.protocol.inspect}" if candidates.empty?
82
+
83
+ candidates.find { _1.content_type == 'Module' } || candidates.first
84
+ end
85
+ end
86
+ end