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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- 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
|