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
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Mxrb
|
|
6
|
+
module Runtime
|
|
7
|
+
module Native
|
|
8
|
+
# Graph interpretation intentionally keeps semantic dispatch together.
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
10
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
11
|
+
ObjectValue = Data.define(:entity, :id, :members)
|
|
12
|
+
|
|
13
|
+
# Transactional in-memory persistence used by the Ruby model Runtime.
|
|
14
|
+
class Store
|
|
15
|
+
def initialize
|
|
16
|
+
@records = Hash.new { |records, entity| records[entity] = [] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(entity)
|
|
20
|
+
ObjectValue.new(entity:, id: SecureRandom.uuid, members: {}).tap do |object|
|
|
21
|
+
@records[entity] << object
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def retrieve(entity) = @records[entity].dup
|
|
26
|
+
|
|
27
|
+
def delete(value)
|
|
28
|
+
Array(value).each { @records[_1.entity].delete(_1) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def count(entity, predicate = nil)
|
|
32
|
+
values = retrieve(entity)
|
|
33
|
+
predicate ? values.count { predicate.call(_1) } : values.size
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def snapshot
|
|
37
|
+
@records.to_h do |entity, values|
|
|
38
|
+
[entity, values.map { ObjectValue.new(entity: _1.entity, id: _1.id, members: _1.members.dup) }]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def restore(snapshot)
|
|
43
|
+
@records.clear
|
|
44
|
+
snapshot.each { |entity, values| @records[entity] = values }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Deliberately small Mendix-expression evaluator. Unsupported syntax is
|
|
49
|
+
# rejected rather than guessed, preserving deterministic test semantics.
|
|
50
|
+
class Expression
|
|
51
|
+
COMPARISONS = {
|
|
52
|
+
'=' => ->(left, right) { left == right },
|
|
53
|
+
'!=' => ->(left, right) { left != right },
|
|
54
|
+
'>' => ->(left, right) { left > right },
|
|
55
|
+
'<' => ->(left, right) { left < right },
|
|
56
|
+
'>=' => ->(left, right) { left >= right },
|
|
57
|
+
'<=' => ->(left, right) { left <= right }
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
def evaluate(source, variables, node: nil)
|
|
61
|
+
text = strip_outer_parentheses(unwrap(source).strip)
|
|
62
|
+
return nil if text.empty? || text == 'empty'
|
|
63
|
+
return true if text.casecmp?('true')
|
|
64
|
+
return false if text.casecmp?('false')
|
|
65
|
+
return text[1...-1].gsub("''", "'") if quoted?(text)
|
|
66
|
+
return Integer(text) if text.match?(/\A-?\d+\z/)
|
|
67
|
+
return Float(text) if text.match?(/\A-?\d+\.\d+\z/)
|
|
68
|
+
return variable(text, variables) if text.match?(%r{\A\$[A-Za-z_]\w*(?:/[A-Za-z_][\w.]*)?\z})
|
|
69
|
+
return node_member(text, node) if node && bare_reference?(text)
|
|
70
|
+
|
|
71
|
+
logical(text, variables) { |part| evaluate(part, variables, node:) }
|
|
72
|
+
rescue ArgumentError, TypeError
|
|
73
|
+
raise NativeRuntimeError, "unsupported Mendix expression: #{source.inspect}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def unwrap(source)
|
|
79
|
+
source.is_a?(Hash) ? (source['Value'] || source['Expression'] || '') : source.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def quoted?(text)
|
|
83
|
+
text.length >= 2 && text.start_with?("'") && text.end_with?("'")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def strip_outer_parentheses(text)
|
|
87
|
+
text = text[1...-1].strip while text.start_with?('(') && matching_parenthesis(text, 0) == text.length - 1
|
|
88
|
+
text
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def matching_parenthesis(text, opening)
|
|
92
|
+
depth = 0
|
|
93
|
+
quoted = false
|
|
94
|
+
text.each_char.with_index do |character, index|
|
|
95
|
+
next if index < opening
|
|
96
|
+
|
|
97
|
+
quoted = !quoted if character == "'"
|
|
98
|
+
next if quoted
|
|
99
|
+
|
|
100
|
+
depth += 1 if character == '('
|
|
101
|
+
depth -= 1 if character == ')'
|
|
102
|
+
return index if depth.zero?
|
|
103
|
+
end
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def variable(text, variables)
|
|
108
|
+
name, member = text.delete_prefix('$').split('/', 2)
|
|
109
|
+
value = variables.fetch(name) do
|
|
110
|
+
raise NativeRuntimeError, "unknown variable $#{name}"
|
|
111
|
+
end
|
|
112
|
+
return value unless member
|
|
113
|
+
|
|
114
|
+
raise NativeRuntimeError, "$#{name} is not an object" unless value.is_a?(ObjectValue)
|
|
115
|
+
|
|
116
|
+
value.members[member.split('.').last]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# An attribute reference inside an XPath predicate, e.g. `Name` or
|
|
120
|
+
# `Clinic.Animal/Name`, resolved against the current candidate object.
|
|
121
|
+
def bare_reference?(text)
|
|
122
|
+
text.match?(%r{\A[A-Za-z_]\w*(?:/[A-Za-z_][\w.]*)?\z})
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def node_member(text, node)
|
|
126
|
+
node.members[text.split('/').last.split('.').last]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def logical(text, variables, &block)
|
|
130
|
+
split = split_operator(text, /\s+or\s+/i)
|
|
131
|
+
return split.any? { yield(_1) } if split.size > 1
|
|
132
|
+
|
|
133
|
+
split = split_operator(text, /\s+and\s+/i)
|
|
134
|
+
return split.all? { yield(_1) } if split.size > 1
|
|
135
|
+
|
|
136
|
+
comparison(text, variables, &block)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def comparison(text, _variables)
|
|
140
|
+
match = text.match(/\A\(?(.+?)\)?\s*(=|!=|>=|<=|>|<)\s*\(?(.+?)\)?\z/)
|
|
141
|
+
raise ArgumentError unless match
|
|
142
|
+
|
|
143
|
+
left = yield(match[1])
|
|
144
|
+
right = yield(match[3])
|
|
145
|
+
COMPARISONS.fetch(match[2]).call(left, right)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def split_operator(text, operator)
|
|
149
|
+
text.split(operator)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Executes the graph and core data activities directly from the MPR.
|
|
154
|
+
class Interpreter
|
|
155
|
+
attr_reader :store, :log
|
|
156
|
+
|
|
157
|
+
def initialize(project, store: Store.new)
|
|
158
|
+
@project = project
|
|
159
|
+
@store = store
|
|
160
|
+
@expression = Expression.new
|
|
161
|
+
@log = []
|
|
162
|
+
@flows = project.modules.flat_map do |mod|
|
|
163
|
+
mod.microflows.map { ["#{mod.name}.#{_1.name}", _1] }
|
|
164
|
+
end.to_h
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def call(name, arguments = {})
|
|
168
|
+
flow = @flows[name]
|
|
169
|
+
raise NativeRuntimeError, "microflow #{name} not found" unless flow
|
|
170
|
+
|
|
171
|
+
snapshot = store.snapshot
|
|
172
|
+
execute(flow, normalize_arguments(flow, arguments))
|
|
173
|
+
rescue StandardError
|
|
174
|
+
store.restore(snapshot) if snapshot
|
|
175
|
+
raise
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Counts stored objects of an entity, optionally narrowed by an XPath
|
|
179
|
+
# constraint. Used by the functional Executor's count expectations.
|
|
180
|
+
def count(entity, xpath = nil)
|
|
181
|
+
filter_by_xpath(store.retrieve(entity.to_s), xpath.to_s, {}).size
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def execute(flow, variables)
|
|
187
|
+
objects = flow.objects.to_h { [identifier(_1), _1] }
|
|
188
|
+
outgoing = flow.flows.reject { _1['IsErrorHandler'] == true }
|
|
189
|
+
.group_by { identifier(_1['OriginPointer']) }
|
|
190
|
+
current = flow.objects.find { _1['$Type'] == 'Microflows$StartEvent' }
|
|
191
|
+
raise NativeRuntimeError, "microflow #{flow.name} has no start event" unless current
|
|
192
|
+
|
|
193
|
+
10_000.times do
|
|
194
|
+
type = current['$Type']
|
|
195
|
+
return @expression.evaluate(current['ReturnValue'], variables) if type == 'Microflows$EndEvent'
|
|
196
|
+
|
|
197
|
+
execute_action(current['Action'], variables) if type == 'Microflows$ActionActivity'
|
|
198
|
+
edge = select_edge(current, outgoing[identifier(current)] || [], variables)
|
|
199
|
+
raise NativeRuntimeError, "microflow #{flow.name} stops at #{type}" unless edge
|
|
200
|
+
|
|
201
|
+
current = objects[identifier(edge['DestinationPointer'])]
|
|
202
|
+
raise NativeRuntimeError, 'sequence flow points to a missing object' unless current
|
|
203
|
+
end
|
|
204
|
+
raise NativeRuntimeError, "microflow #{flow.name} exceeded 10000 steps"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def select_edge(object, edges, variables)
|
|
208
|
+
return edges.first unless %w[Microflows$ExclusiveSplit Microflows$InheritanceSplit].include?(object['$Type'])
|
|
209
|
+
|
|
210
|
+
value = split_value(object, variables)
|
|
211
|
+
edges.find { flow_case(_1) == value.to_s } || edges.find { flow_case(_1).empty? }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def split_value(object, variables)
|
|
215
|
+
if object['$Type'] == 'Microflows$InheritanceSplit'
|
|
216
|
+
value = variables.fetch(object['SplitVariableName'].to_s)
|
|
217
|
+
value.respond_to?(:entity) ? value.entity : value.class.name
|
|
218
|
+
else
|
|
219
|
+
@expression.evaluate(object.dig('SplitCondition', 'Expression'), variables)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def flow_case(edge)
|
|
224
|
+
values = items(edge['CaseValues'])
|
|
225
|
+
value = values.first || edge['NewCaseValue'] || {}
|
|
226
|
+
value['$Type'] == 'Microflows$NoCase' ? '' : value['Value'].to_s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def execute_action(action, variables)
|
|
230
|
+
raise NativeRuntimeError, 'action activity has no action' unless action.is_a?(Hash)
|
|
231
|
+
|
|
232
|
+
handler = action['$Type'].to_s.delete_prefix('Microflows$').delete_suffix('Action')
|
|
233
|
+
method = "action_#{underscore(handler)}"
|
|
234
|
+
raise NativeRuntimeError, "unsupported activity #{action['$Type']}" unless respond_to?(method, true)
|
|
235
|
+
|
|
236
|
+
send(method, action, variables)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def action_create_change(action, variables)
|
|
240
|
+
object = store.create(action['Entity'].to_s)
|
|
241
|
+
apply_changes(object, action['Items'], variables)
|
|
242
|
+
variables[action['VariableName'].to_s] = object
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def action_change(action, variables)
|
|
246
|
+
object = variables.fetch(action['ChangeVariableName'].to_s)
|
|
247
|
+
apply_changes(object, action['Items'], variables)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def action_create_variable(action, variables)
|
|
251
|
+
variables[action['VariableName'].to_s] = @expression.evaluate(action['InitialValue'], variables)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def action_change_variable(action, variables)
|
|
255
|
+
variables[action['ChangeVariableName'].to_s] = @expression.evaluate(action['Value'], variables)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def action_retrieve(action, variables)
|
|
259
|
+
source = action['RetrieveSource'] || {}
|
|
260
|
+
unless source['$Type'] == 'Microflows$DatabaseRetrieveSource'
|
|
261
|
+
raise NativeRuntimeError, "unsupported retrieve source #{source['$Type']}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
values = filter_by_xpath(store.retrieve(source['Entity'].to_s), source['XpathConstraint'].to_s, variables)
|
|
265
|
+
values = sort_values(values, items(source.dig('NewSortings', 'Sortings')))
|
|
266
|
+
range = source['Range'] || {}
|
|
267
|
+
limit = @expression.evaluate(range['LimitExpression'], variables)
|
|
268
|
+
values = values.first(limit) if limit.is_a?(Integer) && limit.positive?
|
|
269
|
+
variables[action['ResultVariableName'].to_s] = range['SingleObject'] == true ? values.first : values
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Narrows a value set by a Mendix XPath constraint. Only attribute
|
|
273
|
+
# predicates (comparisons, and/or, boolean shorthand) are understood;
|
|
274
|
+
# anything else is rejected by the expression evaluator rather than
|
|
275
|
+
# silently ignored.
|
|
276
|
+
def filter_by_xpath(values, xpath, variables)
|
|
277
|
+
predicate = xpath_predicate(xpath)
|
|
278
|
+
return values if predicate.empty?
|
|
279
|
+
|
|
280
|
+
values.select { @expression.evaluate(predicate, variables, node: _1) }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def xpath_predicate(xpath)
|
|
284
|
+
text = xpath.strip
|
|
285
|
+
return '' if text.empty?
|
|
286
|
+
|
|
287
|
+
groups = text.scan(/\[([^\[\]]*)\]/).flatten.map(&:strip).reject(&:empty?)
|
|
288
|
+
unless groups.any? && text.gsub(/\[[^\[\]]*\]/, '').strip.empty?
|
|
289
|
+
raise NativeRuntimeError, "unsupported native XPath constraint: #{xpath.inspect}"
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
groups.map { "(#{_1})" }.join(' and ')
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def sort_values(values, sortings)
|
|
296
|
+
return values if sortings.empty?
|
|
297
|
+
|
|
298
|
+
keys = sortings.map { [sort_attribute(_1), descending?(_1)] }
|
|
299
|
+
values.sort { |left, right| compare_by_keys(left, right, keys) }
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def compare_by_keys(left, right, keys)
|
|
303
|
+
keys.each do |attribute, descending|
|
|
304
|
+
comparison = compare_members(left.members[attribute], right.members[attribute])
|
|
305
|
+
comparison = -comparison if descending
|
|
306
|
+
return comparison unless comparison.zero?
|
|
307
|
+
end
|
|
308
|
+
0
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def compare_members(left, right)
|
|
312
|
+
return 0 if left.nil? && right.nil?
|
|
313
|
+
return 1 if left.nil?
|
|
314
|
+
return -1 if right.nil?
|
|
315
|
+
|
|
316
|
+
(left <=> right) || raise(NativeRuntimeError, "cannot sort #{left.inspect} and #{right.inspect}")
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def sort_attribute(sorting)
|
|
320
|
+
path = (sorting['AttributePath'] || sorting.dig('AttributeRef', 'Attribute')).to_s
|
|
321
|
+
raise NativeRuntimeError, 'native retrieve sorting requires an attribute' if path.empty?
|
|
322
|
+
|
|
323
|
+
path.split(%r{[./]}).last
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def descending?(sorting)
|
|
327
|
+
sorting['SortOrder'].to_s.casecmp?('Descending')
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def action_aggregate(action, variables)
|
|
331
|
+
values = Array(variables.fetch(action['AggregateVariableName'].to_s))
|
|
332
|
+
attribute = action['Attribute'].to_s.split('.').last.to_s
|
|
333
|
+
members = attribute.empty? ? values : values.map { _1.members[attribute] }
|
|
334
|
+
result = aggregate(action['AggregateFunction'], members)
|
|
335
|
+
variables[action['VariableName'].to_s] = result
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def action_microflow_call(action, variables)
|
|
339
|
+
call_doc = action['MicroflowCall'] || {}
|
|
340
|
+
arguments = items(call_doc['ParameterMappings']).to_h do |mapping|
|
|
341
|
+
[mapping['Parameter'].to_s.split('.').last, @expression.evaluate(mapping['Argument'], variables)]
|
|
342
|
+
end
|
|
343
|
+
result = call(call_doc['Microflow'].to_s, arguments)
|
|
344
|
+
variables[action['ResultVariableName'].to_s] = result if action['UseReturnVariable'] == true
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def action_commit(_action, _variables); end
|
|
348
|
+
|
|
349
|
+
def action_delete(action, variables)
|
|
350
|
+
store.delete(variables.fetch(action['DeleteVariableName'].to_s))
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def action_log_message(action, variables)
|
|
354
|
+
@log << render_template(action['MessageTemplate'], variables)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def action_cast(_action, _variables); end
|
|
358
|
+
|
|
359
|
+
def aggregate(function, values)
|
|
360
|
+
case function.to_s.downcase
|
|
361
|
+
when 'count' then values.size
|
|
362
|
+
when 'sum' then values.compact.sum
|
|
363
|
+
when 'minimum', 'min' then values.compact.min
|
|
364
|
+
when 'maximum', 'max' then values.compact.max
|
|
365
|
+
when 'average', 'avg' then values.empty? ? nil : values.compact.sum.to_f / values.compact.size
|
|
366
|
+
else raise NativeRuntimeError, "unsupported aggregate #{function}"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def apply_changes(object, values, variables)
|
|
371
|
+
items(values).each do |item|
|
|
372
|
+
member = (item['Attribute'].to_s.empty? ? item['Association'] : item['Attribute']).to_s
|
|
373
|
+
name = member.split(%r{[./]}).last
|
|
374
|
+
object.members[name] = @expression.evaluate(item['Value'], variables)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def render_template(template, variables)
|
|
379
|
+
text = template&.dig('Text').to_s
|
|
380
|
+
items(template&.dig('Parameters')).each_with_index do |parameter, index|
|
|
381
|
+
text = text.gsub("{#{index + 1}}", @expression.evaluate(parameter['Expression'], variables).to_s)
|
|
382
|
+
end
|
|
383
|
+
text
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def normalize_arguments(flow, arguments)
|
|
387
|
+
names = flow.parameters.filter_map { _1['Name'] if _1.is_a?(Hash) }
|
|
388
|
+
names.to_h do |name|
|
|
389
|
+
[name, arguments.fetch(name) { raise NativeRuntimeError, "missing argument #{name}" }]
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def items(value)
|
|
394
|
+
IO::BsonCodec.parse_array(value)[:items]
|
|
395
|
+
rescue StandardError
|
|
396
|
+
Array(value).drop(value.is_a?(Array) && value.first.is_a?(Integer) ? 1 : 0)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def identifier(value)
|
|
400
|
+
IO::BsonCodec.extract_id(value.is_a?(Hash) ? value['$ID'] : value).to_s
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def underscore(value)
|
|
404
|
+
value.gsub(/([a-z\d])([A-Z])/, '\\1_\\2').downcase
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Runs existing functional definitions against the Ruby interpreter.
|
|
409
|
+
class Executor
|
|
410
|
+
def initialize(project_path, definition, output: nil, clock: Process.method(:clock_gettime), **)
|
|
411
|
+
@project_path = File.expand_path(project_path)
|
|
412
|
+
@definition = definition
|
|
413
|
+
@output = output
|
|
414
|
+
@clock = clock
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def run
|
|
418
|
+
started = monotonic_time
|
|
419
|
+
project = Model::Project.open(@project_path)
|
|
420
|
+
interpreter = Interpreter.new(project)
|
|
421
|
+
tests = @definition.tests.map { run_test(interpreter, _1) }
|
|
422
|
+
transcript = tests.map { "[MXRB_TEST] #{_1.passed? ? 'PASS' : 'FAIL'} #{_1.name}" }.join("\n")
|
|
423
|
+
transcript = "#{transcript}\n[MXRB_TEST] DONE\n"
|
|
424
|
+
@output&.write(transcript)
|
|
425
|
+
Execution.new(
|
|
426
|
+
Functional::Result.new(tests.freeze, true), 'MXRB native validation passed',
|
|
427
|
+
'Ruby microflow interpreter', transcript, monotonic_time - started
|
|
428
|
+
)
|
|
429
|
+
ensure
|
|
430
|
+
project&.close
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
private
|
|
434
|
+
|
|
435
|
+
def run_test(interpreter, test)
|
|
436
|
+
interpreter.call(test.setup.target, evaluated_arguments(test.setup.arguments)) if test.setup
|
|
437
|
+
actual = interpreter.call(test.target, evaluated_arguments(test.arguments))
|
|
438
|
+
failures = expectations(interpreter, test, actual)
|
|
439
|
+
interpreter.call(test.cleanup.target, evaluated_arguments(test.cleanup.arguments)) if test.cleanup
|
|
440
|
+
Functional::TestResult.new(test.name, failures.empty?, failures.empty? ? 'passed' : failures.join('; '))
|
|
441
|
+
rescue StandardError => e
|
|
442
|
+
Functional::TestResult.new(test.name, false, e.message)
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def expectations(interpreter, test, actual)
|
|
446
|
+
failures = []
|
|
447
|
+
if test.expected_return
|
|
448
|
+
expected = Expression.new.evaluate(test.expected_return, {})
|
|
449
|
+
failures << "return #{actual.inspect}, expected #{expected.inspect}" unless actual == expected
|
|
450
|
+
end
|
|
451
|
+
test.counts.each do |expectation|
|
|
452
|
+
actual_count = interpreter.count(expectation.entity, expectation.xpath)
|
|
453
|
+
unless actual_count == expectation.equals
|
|
454
|
+
failures << "#{expectation.entity} count #{actual_count}, expected #{expectation.equals}"
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
failures
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def evaluated_arguments(arguments)
|
|
461
|
+
arguments.to_h.transform_values { Expression.new.evaluate(_1, {}) }
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def monotonic_time
|
|
465
|
+
@clock.call(Process::CLOCK_MONOTONIC)
|
|
466
|
+
rescue ArgumentError
|
|
467
|
+
@clock.call
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
471
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Mxrb
|
|
7
|
+
module Runtime
|
|
8
|
+
# Read-only query-plan connection to an explicitly configured SQL Server
|
|
9
|
+
# deployment. Credentials are passed through SQLCMDPASSWORD, never argv.
|
|
10
|
+
class SqlServerDatabase # rubocop:disable Metrics/ClassLength
|
|
11
|
+
MUTATING_SQL = /\b(?:ALTER|CREATE|DELETE|DROP|EXEC(?:UTE)?|INSERT|INTO|MERGE|TRUNCATE|UPDATE)\b/i
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Metrics/ParameterLists
|
|
14
|
+
def initialize(server:, database:, user:, password: ENV['MXRB_SQLSERVER_PASSWORD'],
|
|
15
|
+
sqlcmd: 'sqlcmd', runner: nil)
|
|
16
|
+
@server = required(server, 'SQL Server host')
|
|
17
|
+
@database = required(database, 'SQL Server database')
|
|
18
|
+
@user = required(user, 'SQL Server user')
|
|
19
|
+
@password = required(password, 'MXRB_SQLSERVER_PASSWORD')
|
|
20
|
+
@sqlcmd = sqlcmd
|
|
21
|
+
@runner = runner || method(:capture)
|
|
22
|
+
end
|
|
23
|
+
# rubocop:enable Metrics/ParameterLists
|
|
24
|
+
|
|
25
|
+
def explain(sql, analyze: false)
|
|
26
|
+
statement = read_only_statement(sql)
|
|
27
|
+
mode = analyze ? 'STATISTICS XML' : 'SHOWPLAN_XML'
|
|
28
|
+
output = run!("SET #{mode} ON; #{statement}; SET #{mode} OFF;")
|
|
29
|
+
Oql::SqlServerPlanAnalyzer.new.analyze(extract_showplan(output))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def workload(limit: 20)
|
|
33
|
+
maximum = Integer(limit)
|
|
34
|
+
raise ArgumentError, 'workload limit must be between 1 and 1000' unless (1..1000).cover?(maximum)
|
|
35
|
+
|
|
36
|
+
Oql::SqlServerWorkloadAnalyzer.new.analyze(
|
|
37
|
+
query_rows: query_workload(maximum), table_rows: table_workload,
|
|
38
|
+
index_rows: index_workload
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def query_workload(limit) # rubocop:disable Metrics/MethodLength
|
|
45
|
+
run_json(<<~SQL)
|
|
46
|
+
SELECT TOP (#{limit})
|
|
47
|
+
CONVERT(varchar(66), qs.query_hash, 1) AS query_hash,
|
|
48
|
+
SUBSTRING(st.text, (qs.statement_start_offset / 2) + 1,
|
|
49
|
+
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
|
|
50
|
+
ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) + 1) AS query_text,
|
|
51
|
+
qs.execution_count, qs.total_elapsed_time, qs.total_rows,
|
|
52
|
+
qs.total_logical_reads, qs.total_physical_reads
|
|
53
|
+
FROM sys.dm_exec_query_stats qs
|
|
54
|
+
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
|
|
55
|
+
ORDER BY qs.total_elapsed_time DESC FOR JSON PATH
|
|
56
|
+
SQL
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def table_workload
|
|
60
|
+
run_json(<<~SQL)
|
|
61
|
+
SELECT CONCAT(OBJECT_SCHEMA_NAME(i.object_id), '.', OBJECT_NAME(i.object_id)) AS relation,
|
|
62
|
+
SUM(COALESCE(s.user_scans, 0)) AS user_scans,
|
|
63
|
+
SUM(COALESCE(s.user_seeks, 0)) AS user_seeks
|
|
64
|
+
FROM sys.indexes i
|
|
65
|
+
LEFT JOIN sys.dm_db_index_usage_stats s
|
|
66
|
+
ON s.database_id = DB_ID() AND s.object_id = i.object_id AND s.index_id = i.index_id
|
|
67
|
+
WHERE i.object_id > 0
|
|
68
|
+
GROUP BY i.object_id FOR JSON PATH
|
|
69
|
+
SQL
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def index_workload # rubocop:disable Metrics/MethodLength
|
|
73
|
+
run_json(<<~SQL)
|
|
74
|
+
SELECT CONCAT(OBJECT_SCHEMA_NAME(i.object_id), '.', OBJECT_NAME(i.object_id)) AS relation,
|
|
75
|
+
i.name AS index_name, COALESCE(s.user_scans, 0) AS user_scans,
|
|
76
|
+
COALESCE(s.user_seeks, 0) AS user_seeks,
|
|
77
|
+
SUM(COALESCE(p.reserved_page_count, 0)) * 8192 AS index_bytes
|
|
78
|
+
FROM sys.indexes i
|
|
79
|
+
LEFT JOIN sys.dm_db_index_usage_stats s
|
|
80
|
+
ON s.database_id = DB_ID() AND s.object_id = i.object_id AND s.index_id = i.index_id
|
|
81
|
+
LEFT JOIN sys.dm_db_partition_stats p
|
|
82
|
+
ON p.object_id = i.object_id AND p.index_id = i.index_id
|
|
83
|
+
WHERE i.name IS NOT NULL
|
|
84
|
+
GROUP BY i.object_id, i.name, s.user_scans, s.user_seeks FOR JSON PATH
|
|
85
|
+
SQL
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def run_json(sql)
|
|
89
|
+
source = run!(sql).strip
|
|
90
|
+
JSON.parse(source.empty? ? '[]' : source)
|
|
91
|
+
rescue JSON::ParserError => e
|
|
92
|
+
raise ToolchainError, "sqlcmd returned invalid workload JSON: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run!(sql)
|
|
96
|
+
environment = { 'SQLCMDPASSWORD' => @password }
|
|
97
|
+
command = [
|
|
98
|
+
@sqlcmd, '-S', @server, '-d', @database, '-U', @user,
|
|
99
|
+
'-C', '-b', '-h', '-1', '-W', '-y', '0', '-Q', sql
|
|
100
|
+
]
|
|
101
|
+
output, error, status = @runner.call(environment, *command)
|
|
102
|
+
raise ToolchainError, "sqlcmd failed:\n#{output}#{error}" unless status.success?
|
|
103
|
+
|
|
104
|
+
output.to_s
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_showplan(output)
|
|
108
|
+
start = output.index('<ShowPlanXML')
|
|
109
|
+
finish = output.rindex('</ShowPlanXML>')
|
|
110
|
+
raise ToolchainError, 'sqlcmd returned no SHOWPLAN XML' unless start && finish
|
|
111
|
+
|
|
112
|
+
output[start..(finish + '</ShowPlanXML>'.length - 1)]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def read_only_statement(sql)
|
|
116
|
+
source = sql.to_s
|
|
117
|
+
raise ArgumentError, 'SQL contains a NUL byte' if source.include?("\0")
|
|
118
|
+
|
|
119
|
+
statement = source.strip
|
|
120
|
+
validate_read_only_statement!(statement)
|
|
121
|
+
statement
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_read_only_statement!(statement)
|
|
125
|
+
raise ArgumentError, 'SQL must not be empty' if statement.empty?
|
|
126
|
+
unless statement.match?(/\A(?:SELECT|WITH)\b/i) && !statement.include?(';')
|
|
127
|
+
raise ArgumentError, 'SQL Server plan analysis accepts one read-only SELECT or WITH statement'
|
|
128
|
+
end
|
|
129
|
+
return unless statement.match?(MUTATING_SQL)
|
|
130
|
+
|
|
131
|
+
raise ArgumentError, 'SQL Server plan analysis rejects mutating keywords, including writable CTEs'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def required(value, label)
|
|
135
|
+
text = value.to_s
|
|
136
|
+
raise ArgumentError, "#{label} is required" if text.empty? || text.include?("\0")
|
|
137
|
+
|
|
138
|
+
text
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def capture(environment, *command)
|
|
142
|
+
Open3.capture3(environment, *command)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|