mxrb 0.1.2 → 0.1.4
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 +4 -4
- data/bin/mxrb +458 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1361 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module Oql
|
|
5
|
+
OqlProjection = Data.define(
|
|
6
|
+
:sql, :oql, :dialect, :confidence, :warnings, :parameters
|
|
7
|
+
) do
|
|
8
|
+
def supported? = !oql.nil?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Converts a conservative, read-only SQL subset back into logical OQL.
|
|
12
|
+
# Physical Runtime names are accepted as Module$Entity. Supplying a
|
|
13
|
+
# project restores canonical entity and attribute casing.
|
|
14
|
+
# Parser transformations are intentionally kept together so table, alias,
|
|
15
|
+
# attribute and parameter state cannot diverge.
|
|
16
|
+
# rubocop:disable Metrics
|
|
17
|
+
class ReverseTranslator
|
|
18
|
+
DIALECTS = Translator::DIALECTS
|
|
19
|
+
STOP_CLAUSES = %w[GROUP HAVING LIMIT OFFSET ORDER UNION WHERE].freeze
|
|
20
|
+
ALIAS_STOP_WORDS = (Translator::ALIAS_STOP_WORDS + %w[CROSS FETCH FOR]).freeze
|
|
21
|
+
REJECTED_WORDS = %w[
|
|
22
|
+
ALTER APPLY CALL COLLATE CREATE CROSS DELETE DROP EXEC EXECUTE FETCH
|
|
23
|
+
FILTER FOR GRANT INSERT INTO LATERAL MERGE NATURAL NULLS OVER QUALIFY
|
|
24
|
+
RETURNING REVOKE ROWS TOP TRUNCATE UNNEST UPDATE UPSERT VALUES
|
|
25
|
+
WINDOW WITH
|
|
26
|
+
].freeze
|
|
27
|
+
FUNCTIONS = %w[
|
|
28
|
+
AVG CAST COALESCE COUNT DATEADD DATEDIFF DATEFORMAT DATEPARSE DATEPART
|
|
29
|
+
DATETRUNC LENGTH LOCATE LOWER LPAD LTRIM MAX MIN RANGEBEGIN RANGEEND
|
|
30
|
+
REPLACE ROUND RPAD RTRIM STRING_AGG SUBSTRING SUM TRIM UPPER
|
|
31
|
+
].freeze
|
|
32
|
+
FUNCTION_ALIASES = { 'CHAR_LENGTH' => 'LENGTH', 'LEN' => 'LENGTH' }.freeze
|
|
33
|
+
FUNCTION_LIKE_OPERATORS = %w[
|
|
34
|
+
AS BOOLEAN CASE DATETIME DECIMAL EXISTS FLOAT FROM HAVING IN INTEGER
|
|
35
|
+
JOIN LONG ON SELECT STRING WHEN WHERE
|
|
36
|
+
].freeze
|
|
37
|
+
RESERVED = %w[
|
|
38
|
+
ALL AND AS ASC AVG BOOLEAN BY CASE CAST COUNT DATETIME DAY DECIMAL
|
|
39
|
+
DESC DISTINCT ELSE END EXISTS FALSE FLOAT FROM FULL GROUP HAVING HOUR
|
|
40
|
+
IN INNER INTEGER IS JOIN KEY LEFT LIKE LIMIT LONG MATCHED MAX MERGE
|
|
41
|
+
MILLISECOND MIN MINUTE MONTH NOT NULL OFFSET ON OR ORDER OUTER QUARTER
|
|
42
|
+
REPLACE RIGHT SECOND SELECT SET SOURCE STRING STRING_AGG SUM TARGET
|
|
43
|
+
THEN TRUE UNION UPDATE UPSERT USING VALUES WEEK WEEKDAY WHEN WHERE WITH
|
|
44
|
+
YEAR
|
|
45
|
+
].freeze
|
|
46
|
+
SCHEMAS = %w[dbo public].freeze
|
|
47
|
+
|
|
48
|
+
def initialize(dialect: :postgresql, project: nil)
|
|
49
|
+
@dialect = dialect.to_sym
|
|
50
|
+
raise ArgumentError, "unsupported SQL dialect #{@dialect.inspect}" unless DIALECTS.include?(@dialect)
|
|
51
|
+
|
|
52
|
+
@catalog = entity_catalog(project)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def translate(source)
|
|
56
|
+
sql = source.to_s
|
|
57
|
+
tokens = Translator.tokens(sql)
|
|
58
|
+
translate_function_aliases!(tokens)
|
|
59
|
+
warning = validation_warning(tokens)
|
|
60
|
+
return unsupported(sql, warning) if warning
|
|
61
|
+
|
|
62
|
+
warnings = []
|
|
63
|
+
aliases = translate_tables!(tokens, warnings)
|
|
64
|
+
return unsupported(sql, aliases) if aliases.is_a?(String)
|
|
65
|
+
|
|
66
|
+
translate_attributes!(tokens, aliases)
|
|
67
|
+
parameter_result = translate_parameters!(tokens)
|
|
68
|
+
return unsupported(sql, parameter_result) if parameter_result.is_a?(String)
|
|
69
|
+
|
|
70
|
+
translate_operators!(tokens)
|
|
71
|
+
|
|
72
|
+
oql = tokens.map(&:text).join.strip
|
|
73
|
+
confidence = warnings.empty? ? :logical : :inferred
|
|
74
|
+
OqlProjection.new(
|
|
75
|
+
sql.freeze, oql.freeze, @dialect, confidence,
|
|
76
|
+
warnings.uniq.freeze, parameter_result.freeze
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def validation_warning(tokens)
|
|
83
|
+
significant = significant_indices(tokens)
|
|
84
|
+
first = significant.first && tokens[significant.first].text.upcase
|
|
85
|
+
return 'SQL keyword WITH has no safe OQL conversion' if first == 'WITH'
|
|
86
|
+
return 'only read-only SQL SELECT queries can be converted to OQL' unless first == 'SELECT'
|
|
87
|
+
return 'multiple SQL statements are not accepted' if significant.any? { tokens[_1].text == ';' }
|
|
88
|
+
|
|
89
|
+
rejected = significant.find do |index|
|
|
90
|
+
token = tokens[index]
|
|
91
|
+
token.type == :word && REJECTED_WORDS.include?(token.text.upcase)
|
|
92
|
+
end
|
|
93
|
+
return "SQL keyword #{tokens[rejected].text.upcase} has no safe OQL conversion" if rejected
|
|
94
|
+
return 'PostgreSQL casts using :: are not supported; use CAST(expression AS type)' \
|
|
95
|
+
if adjacent_symbols?(tokens, ':', ':')
|
|
96
|
+
return 'SQL ILIKE has no database-independent OQL equivalent' if keyword?(tokens, 'ILIKE')
|
|
97
|
+
return 'PostgreSQL DISTINCT ON has no OQL equivalent' if adjacent_words?(tokens, 'DISTINCT', 'ON')
|
|
98
|
+
return 'SQL table subqueries are outside the safe OQL conversion subset' if table_subquery?(tokens)
|
|
99
|
+
|
|
100
|
+
function = unsupported_function(tokens)
|
|
101
|
+
return "SQL function #{function} has no supported OQL equivalent" if function
|
|
102
|
+
return 'SQL concatenation and bitwise operators have no safe OQL conversion' \
|
|
103
|
+
if unsupported_operator?(tokens)
|
|
104
|
+
return 'positional SQL parameters are not supported; use named parameters' \
|
|
105
|
+
if significant.any? { tokens[_1].text == '?' } || postgres_positional_parameter?(tokens)
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def translate_tables!(tokens, warnings)
|
|
111
|
+
aliases = {}
|
|
112
|
+
table_start_indices(tokens).each do |start|
|
|
113
|
+
parsed = parse_table(tokens, start)
|
|
114
|
+
return "SQL table #{tokens[start].text.inspect} cannot be mapped to Module.Entity" unless parsed
|
|
115
|
+
|
|
116
|
+
info, finish, original, table_warnings = parsed
|
|
117
|
+
warnings.concat(table_warnings)
|
|
118
|
+
tokens[start] = Translator::Token.new(:word, render_qualified(info.fetch(:qualified_name)))
|
|
119
|
+
((start + 1)..finish).each { tokens[_1] = Translator::Token.new(:word, '') }
|
|
120
|
+
|
|
121
|
+
alias_index = next_significant(tokens, finish)
|
|
122
|
+
alias_index = next_significant(tokens, alias_index) \
|
|
123
|
+
if alias_index && tokens[alias_index].text.casecmp?('AS')
|
|
124
|
+
explicit_alias = alias_index && identifier?(tokens[alias_index]) &&
|
|
125
|
+
!ALIAS_STOP_WORDS.include?(tokens[alias_index].text.upcase)
|
|
126
|
+
alias_name = explicit_alias ? identifier_text(tokens[alias_index]) : info.fetch(:entity_name)
|
|
127
|
+
aliases[alias_name.downcase] = info
|
|
128
|
+
aliases[original.downcase] = info
|
|
129
|
+
aliases[info.fetch(:qualified_name).downcase] = info
|
|
130
|
+
end
|
|
131
|
+
return 'SQL query does not reference a convertible Module.Entity source' if aliases.empty?
|
|
132
|
+
|
|
133
|
+
aliases
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def table_start_indices(tokens)
|
|
137
|
+
starts = []
|
|
138
|
+
active_from = {}
|
|
139
|
+
join_constraint = {}
|
|
140
|
+
depth = 0
|
|
141
|
+
significant_indices(tokens).each do |index|
|
|
142
|
+
token = tokens[index]
|
|
143
|
+
if token.text == ')'
|
|
144
|
+
active_from.delete(depth)
|
|
145
|
+
depth -= 1
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
if token.type == :word
|
|
149
|
+
word = token.text.upcase
|
|
150
|
+
if word == 'FROM'
|
|
151
|
+
active_from[depth] = true
|
|
152
|
+
join_constraint[depth] = false
|
|
153
|
+
starts << next_significant(tokens, index)
|
|
154
|
+
elsif word == 'JOIN'
|
|
155
|
+
join_constraint[depth] = false
|
|
156
|
+
starts << next_significant(tokens, index)
|
|
157
|
+
elsif word == 'ON'
|
|
158
|
+
join_constraint[depth] = true
|
|
159
|
+
elsif STOP_CLAUSES.include?(word)
|
|
160
|
+
active_from.delete(depth)
|
|
161
|
+
join_constraint.delete(depth)
|
|
162
|
+
end
|
|
163
|
+
elsif token.text == ',' && active_from[depth] && !join_constraint[depth]
|
|
164
|
+
starts << next_significant(tokens, index)
|
|
165
|
+
end
|
|
166
|
+
depth += 1 if token.text == '('
|
|
167
|
+
end
|
|
168
|
+
starts.compact.uniq
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def parse_table(tokens, start)
|
|
172
|
+
first = tokens[start]
|
|
173
|
+
return unless identifier?(first)
|
|
174
|
+
|
|
175
|
+
first_text = identifier_text(first)
|
|
176
|
+
dot = next_significant(tokens, start)
|
|
177
|
+
second = dot && next_significant(tokens, dot)
|
|
178
|
+
if second && tokens[dot].text == '.' && identifier?(tokens[second])
|
|
179
|
+
second_text = identifier_text(tokens[second])
|
|
180
|
+
if physical_name?(second_text) && (SCHEMAS.include?(first_text.downcase) || catalog_entry(second_text))
|
|
181
|
+
info, warnings = resolve_table(second_text)
|
|
182
|
+
return unless info
|
|
183
|
+
|
|
184
|
+
warnings << "SQL schema #{first_text} was removed from the logical OQL entity name"
|
|
185
|
+
return [info, second, second_text, warnings]
|
|
186
|
+
end
|
|
187
|
+
info, warnings = resolve_table("#{first_text}.#{second_text}")
|
|
188
|
+
return info && [info, second, "#{first_text}.#{second_text}", warnings]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
info, warnings = resolve_table(first_text)
|
|
192
|
+
info && [info, start, first_text, warnings]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def resolve_table(value)
|
|
196
|
+
canonical = catalog_entry(value)
|
|
197
|
+
return [canonical, []] if canonical
|
|
198
|
+
|
|
199
|
+
parts = if value.include?('$')
|
|
200
|
+
value.split('$', 2)
|
|
201
|
+
elsif value.include?('.')
|
|
202
|
+
value.split('.', 2)
|
|
203
|
+
end
|
|
204
|
+
return [nil, []] unless parts&.size == 2 && parts.none?(&:empty?)
|
|
205
|
+
|
|
206
|
+
qualified = parts.join('.')
|
|
207
|
+
info = entity_info(qualified)
|
|
208
|
+
warning = if value.include?('$')
|
|
209
|
+
"logical entity casing was inferred from physical table #{value}; use --project for canonical names"
|
|
210
|
+
end
|
|
211
|
+
[info, Array(warning)]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def translate_attributes!(tokens, aliases)
|
|
215
|
+
significant_indices(tokens).each do |index|
|
|
216
|
+
token = tokens[index]
|
|
217
|
+
next unless identifier?(token)
|
|
218
|
+
|
|
219
|
+
info = aliases[identifier_text(token).downcase]
|
|
220
|
+
next unless info
|
|
221
|
+
|
|
222
|
+
separator = next_significant(tokens, index)
|
|
223
|
+
attribute = separator && next_significant(tokens, separator)
|
|
224
|
+
next unless attribute && tokens[separator].text == '.' &&
|
|
225
|
+
(identifier?(tokens[attribute]) || tokens[attribute].text == '*')
|
|
226
|
+
|
|
227
|
+
name = tokens[attribute].text == '*' ? '*' : canonical_attribute(info, identifier_text(tokens[attribute]))
|
|
228
|
+
tokens[separator] = Translator::Token.new(:path_separator, '/')
|
|
229
|
+
tokens[attribute] = Translator::Token.new(:word, name == '*' ? name : quote_identifier(name))
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def translate_function_aliases!(tokens)
|
|
234
|
+
tokens.each_index do |index|
|
|
235
|
+
token = tokens[index]
|
|
236
|
+
next unless token.type == :word
|
|
237
|
+
|
|
238
|
+
replacement = FUNCTION_ALIASES[token.text.upcase]
|
|
239
|
+
tokens[index] = Translator::Token.new(:word, replacement) if replacement
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def translate_operators!(tokens)
|
|
244
|
+
tokens.each_index do |index|
|
|
245
|
+
token = tokens[index]
|
|
246
|
+
tokens[index] = Translator::Token.new(:symbol, ':') \
|
|
247
|
+
if token.type == :symbol && token.text == '/'
|
|
248
|
+
end
|
|
249
|
+
significant_indices(tokens).each_cons(2) do |left, right|
|
|
250
|
+
next unless tokens[left].text == '<' && tokens[right].text == '>'
|
|
251
|
+
|
|
252
|
+
tokens[left] = Translator::Token.new(:symbol, '!')
|
|
253
|
+
tokens[right] = Translator::Token.new(:symbol, '=')
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def translate_parameters!(tokens)
|
|
258
|
+
parameters = []
|
|
259
|
+
tokens.each_index do |index|
|
|
260
|
+
token = tokens[index]
|
|
261
|
+
if token.type == :parameter
|
|
262
|
+
parameters << token.text.delete_prefix('$')
|
|
263
|
+
next
|
|
264
|
+
end
|
|
265
|
+
next unless %w[: @].include?(token.text)
|
|
266
|
+
|
|
267
|
+
name_index = next_significant(tokens, index)
|
|
268
|
+
return 'named SQL parameter is missing its name' unless name_index && tokens[name_index].type == :word
|
|
269
|
+
|
|
270
|
+
name = tokens[name_index].text
|
|
271
|
+
tokens[index] = Translator::Token.new(:parameter, "$#{name}")
|
|
272
|
+
tokens[name_index] = Translator::Token.new(:word, '')
|
|
273
|
+
parameters << name
|
|
274
|
+
end
|
|
275
|
+
parameters.uniq
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def entity_catalog(project)
|
|
279
|
+
return {} unless project
|
|
280
|
+
|
|
281
|
+
project.modules.each_with_object({}) do |mod, catalog|
|
|
282
|
+
mod.entities.each do |entity|
|
|
283
|
+
qualified = entity.qualified_name || "#{mod.name}.#{entity.name}"
|
|
284
|
+
info = entity_info(qualified, entity.attributes)
|
|
285
|
+
catalog[qualified.downcase] = info
|
|
286
|
+
catalog[qualified.tr('.', '$').downcase] = info
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def entity_info(qualified, attributes = [])
|
|
292
|
+
module_name, entity_name = qualified.split('.', 2)
|
|
293
|
+
{
|
|
294
|
+
qualified_name: "#{module_name}.#{entity_name}", module_name:, entity_name:,
|
|
295
|
+
attributes: attributes.to_h { [_1.name.to_s.downcase, _1.name.to_s] }
|
|
296
|
+
}
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def catalog_entry(value) = @catalog[value.to_s.downcase]
|
|
300
|
+
def physical_name?(value) = value.include?('$')
|
|
301
|
+
|
|
302
|
+
def canonical_attribute(info, name)
|
|
303
|
+
info.fetch(:attributes).fetch(name.downcase, name)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def render_qualified(value)
|
|
307
|
+
value.split('.', 2).map { quote_identifier(_1) }.join('.')
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def quote_identifier(value)
|
|
311
|
+
return value if value.match?(/\A[A-Za-z_][A-Za-z0-9_$]*\z/) && !RESERVED.include?(value.upcase)
|
|
312
|
+
|
|
313
|
+
%("#{value.gsub('"', '""')}")
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def identifier?(token) = token && %i[word quoted_identifier].include?(token.type)
|
|
317
|
+
|
|
318
|
+
def identifier_text(token)
|
|
319
|
+
text = token.text
|
|
320
|
+
return text[1...-1].gsub(']]', ']') if text.start_with?('[') && text.end_with?(']')
|
|
321
|
+
return text[1...-1].gsub('""', '"') if text.start_with?('"') && text.end_with?('"')
|
|
322
|
+
|
|
323
|
+
text
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def significant_indices(tokens)
|
|
327
|
+
tokens.each_index.reject { %i[space comment].include?(tokens[_1].type) || tokens[_1].text.empty? }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def next_significant(tokens, index)
|
|
331
|
+
((index + 1)...tokens.length).find do |candidate|
|
|
332
|
+
!%i[space comment].include?(tokens[candidate].type) && !tokens[candidate].text.empty?
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def adjacent_symbols?(tokens, first, second)
|
|
337
|
+
significant = significant_indices(tokens)
|
|
338
|
+
significant.each_cons(2).any? do |left, right|
|
|
339
|
+
tokens[left].text == first && tokens[right].text == second
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def adjacent_words?(tokens, first, second)
|
|
344
|
+
significant = significant_indices(tokens)
|
|
345
|
+
significant.each_cons(2).any? do |left, right|
|
|
346
|
+
tokens[left].type == :word && tokens[right].type == :word &&
|
|
347
|
+
tokens[left].text.casecmp?(first) && tokens[right].text.casecmp?(second)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def table_subquery?(tokens)
|
|
352
|
+
significant = significant_indices(tokens)
|
|
353
|
+
significant.each_cons(2).any? do |left, right|
|
|
354
|
+
tokens[left].type == :word && %w[FROM JOIN].include?(tokens[left].text.upcase) &&
|
|
355
|
+
tokens[right].text == '('
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def unsupported_function(tokens)
|
|
360
|
+
significant = significant_indices(tokens)
|
|
361
|
+
significant.each_cons(2).filter_map do |name, opening|
|
|
362
|
+
next unless identifier?(tokens[name]) && tokens[opening].text == '('
|
|
363
|
+
|
|
364
|
+
function = identifier_text(tokens[name]).upcase
|
|
365
|
+
function unless FUNCTIONS.include?(function) || FUNCTION_LIKE_OPERATORS.include?(function)
|
|
366
|
+
end.first
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def unsupported_operator?(tokens)
|
|
370
|
+
[%w[| |], %w[- >], %w[# >]].any? do |first, second|
|
|
371
|
+
adjacent_symbols?(tokens, first, second)
|
|
372
|
+
end || significant_indices(tokens).any? { %w[& ^].include?(tokens[_1].text) }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def keyword?(tokens, word)
|
|
376
|
+
significant_indices(tokens).any? do |index|
|
|
377
|
+
tokens[index].type == :word && tokens[index].text.casecmp?(word)
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def postgres_positional_parameter?(tokens)
|
|
382
|
+
significant = significant_indices(tokens)
|
|
383
|
+
significant.each_cons(2).any? do |left, right|
|
|
384
|
+
tokens[left].text == '$' && tokens[right].text.match?(/\A\d\z/)
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def unsupported(sql, warning)
|
|
389
|
+
OqlProjection.new(sql.freeze, nil, @dialect, :unsupported, [warning].freeze, [].freeze)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
# rubocop:enable Metrics
|
|
393
|
+
end
|
|
394
|
+
end
|
data/lib/mxrb/oql/server.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
|
-
|
|
4
|
+
require_relative '../http/server'
|
|
5
5
|
|
|
6
6
|
module Mxrb
|
|
7
7
|
module Oql
|
|
@@ -21,18 +21,14 @@ module Mxrb
|
|
|
21
21
|
|
|
22
22
|
@port = Integer(port)
|
|
23
23
|
@workspace = workspace || Runtime::DatabaseWorkspace.new(project_path, port: database_port)
|
|
24
|
-
@logger = logger
|
|
24
|
+
@logger = logger
|
|
25
25
|
end
|
|
26
26
|
# rubocop:enable Metrics/ParameterLists
|
|
27
27
|
|
|
28
28
|
def start(prepare: true)
|
|
29
29
|
@workspace.up if prepare
|
|
30
|
-
@server =
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
@server.mount_proc('/query') { |request, response| dispatch(request, response) }
|
|
34
|
-
yield(@server) if block_given?
|
|
35
|
-
@server.start
|
|
30
|
+
@server = Http::Server.new(host:, port:, logger: @logger) { dispatch(_1, _2) }
|
|
31
|
+
@server.start { yield(_1) if block_given? }
|
|
36
32
|
end
|
|
37
33
|
|
|
38
34
|
def shutdown = @server&.shutdown
|
data/lib/mxrb/oql.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Mxrb
|
|
|
49
49
|
|
|
50
50
|
# Recursive BSON discovery is intentionally kept together so that its
|
|
51
51
|
# source-type guard and ownership context cannot diverge.
|
|
52
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
52
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
53
53
|
def discover(document, raw, module_name)
|
|
54
54
|
found = []
|
|
55
55
|
walk(document) do |node, path, ancestors|
|
|
@@ -59,6 +59,11 @@ module Mxrb
|
|
|
59
59
|
node['Query'], raw, module_name, path + ['Query'], source_type, ancestors
|
|
60
60
|
)
|
|
61
61
|
end
|
|
62
|
+
if source_type == 'DomainModels$ViewEntitySourceDocument' && node['Oql'].is_a?(String)
|
|
63
|
+
found << build_query(
|
|
64
|
+
node['Oql'], raw, module_name, path + ['Oql'], source_type, ancestors
|
|
65
|
+
)
|
|
66
|
+
end
|
|
62
67
|
node.each do |key, value|
|
|
63
68
|
next unless key.to_s.match?(/\AOqlQuery\z/i) && value.is_a?(String)
|
|
64
69
|
|
|
@@ -67,7 +72,7 @@ module Mxrb
|
|
|
67
72
|
end
|
|
68
73
|
found.uniq { [_1.unit_id, _1.path] }
|
|
69
74
|
end
|
|
70
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
75
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
71
76
|
|
|
72
77
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
73
78
|
def walk(node, path = [], ancestors = [], &block)
|
|
@@ -97,7 +102,8 @@ module Mxrb
|
|
|
97
102
|
unit_type = @project.parse_bson(raw)['$Type'].to_s
|
|
98
103
|
kind = if unit_type == 'DataSets$DataSet'
|
|
99
104
|
:dataset
|
|
100
|
-
elsif
|
|
105
|
+
elsif unit_type == 'DomainModels$ViewEntitySourceDocument' ||
|
|
106
|
+
owner['$Type'].to_s.match?(/Entity/i)
|
|
101
107
|
:view_entity
|
|
102
108
|
else
|
|
103
109
|
:oql
|
|
@@ -148,11 +154,13 @@ module Mxrb
|
|
|
148
154
|
end
|
|
149
155
|
|
|
150
156
|
def self.parameters(source)
|
|
151
|
-
|
|
157
|
+
tokens(source).filter_map do |token|
|
|
152
158
|
token.text.delete_prefix('$') if token.type == :parameter
|
|
153
159
|
end.uniq.freeze
|
|
154
160
|
end
|
|
155
161
|
|
|
162
|
+
def self.tokens(source) = new.send(:tokenize, source.to_s)
|
|
163
|
+
|
|
156
164
|
def translate_source(source, name: 'AdHoc')
|
|
157
165
|
text = source.to_s
|
|
158
166
|
query = Query.new(
|
|
@@ -202,6 +210,7 @@ module Mxrb
|
|
|
202
210
|
return consume_while(source, index, :space) { SPACE.match?(_1) } if SPACE.match?(char)
|
|
203
211
|
return consume_quoted(source, index, "'", :string) if char == "'"
|
|
204
212
|
return consume_quoted(source, index, '"', :quoted_identifier) if char == '"'
|
|
213
|
+
return consume_bracket_quoted(source, index) if char == '['
|
|
205
214
|
return consume_line_comment(source, index) if source[index, 2] == '--'
|
|
206
215
|
return consume_block_comment(source, index) if source[index, 2] == '/*'
|
|
207
216
|
return consume_parameter(source, index) if char == '$' && source[index + 1]&.match?(WORD_START)
|
|
@@ -240,6 +249,22 @@ module Mxrb
|
|
|
240
249
|
[:comment, source[index...finish], finish]
|
|
241
250
|
end
|
|
242
251
|
|
|
252
|
+
def consume_bracket_quoted(source, index) # rubocop:disable Metrics/MethodLength
|
|
253
|
+
finish = index + 1
|
|
254
|
+
while finish < source.length
|
|
255
|
+
if source[finish] == ']'
|
|
256
|
+
finish += 1
|
|
257
|
+
if source[finish] == ']'
|
|
258
|
+
finish += 1
|
|
259
|
+
next
|
|
260
|
+
end
|
|
261
|
+
break
|
|
262
|
+
end
|
|
263
|
+
finish += 1
|
|
264
|
+
end
|
|
265
|
+
[:quoted_identifier, source[index...finish], finish]
|
|
266
|
+
end
|
|
267
|
+
|
|
243
268
|
def consume_block_comment(source, index)
|
|
244
269
|
finish = source.index('*/', index + 2)
|
|
245
270
|
finish = finish ? finish + 2 : source.length
|
|
@@ -381,6 +406,8 @@ module Mxrb
|
|
|
381
406
|
def identifier_text(token)
|
|
382
407
|
return token.text unless token.type == :quoted_identifier
|
|
383
408
|
|
|
409
|
+
return token.text[1...-1].gsub(']]', ']') if token.text.start_with?('[')
|
|
410
|
+
|
|
384
411
|
token.text[1...-1].gsub('""', '"')
|
|
385
412
|
end
|
|
386
413
|
|
|
@@ -422,3 +449,4 @@ module Mxrb
|
|
|
422
449
|
end
|
|
423
450
|
|
|
424
451
|
require_relative 'oql/analyzer'
|
|
452
|
+
require_relative 'oql/reverse_translator'
|