mxrb 0.1.3 → 0.1.5
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 +455 -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/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/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/page_bundle_compiler.rb +13 -4
- 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 +43 -9
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +89 -15
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +14 -0
- 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 +28 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/page.rb +257 -17
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +23 -1
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +2508 -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 +1369 -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/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 +200 -24
- data/lib/mxrb.rb +26 -1
- metadata +156 -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
|
@@ -154,11 +154,13 @@ module Mxrb
|
|
|
154
154
|
end
|
|
155
155
|
|
|
156
156
|
def self.parameters(source)
|
|
157
|
-
|
|
157
|
+
tokens(source).filter_map do |token|
|
|
158
158
|
token.text.delete_prefix('$') if token.type == :parameter
|
|
159
159
|
end.uniq.freeze
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
def self.tokens(source) = new.send(:tokenize, source.to_s)
|
|
163
|
+
|
|
162
164
|
def translate_source(source, name: 'AdHoc')
|
|
163
165
|
text = source.to_s
|
|
164
166
|
query = Query.new(
|
|
@@ -208,6 +210,7 @@ module Mxrb
|
|
|
208
210
|
return consume_while(source, index, :space) { SPACE.match?(_1) } if SPACE.match?(char)
|
|
209
211
|
return consume_quoted(source, index, "'", :string) if char == "'"
|
|
210
212
|
return consume_quoted(source, index, '"', :quoted_identifier) if char == '"'
|
|
213
|
+
return consume_bracket_quoted(source, index) if char == '['
|
|
211
214
|
return consume_line_comment(source, index) if source[index, 2] == '--'
|
|
212
215
|
return consume_block_comment(source, index) if source[index, 2] == '/*'
|
|
213
216
|
return consume_parameter(source, index) if char == '$' && source[index + 1]&.match?(WORD_START)
|
|
@@ -246,6 +249,22 @@ module Mxrb
|
|
|
246
249
|
[:comment, source[index...finish], finish]
|
|
247
250
|
end
|
|
248
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
|
+
|
|
249
268
|
def consume_block_comment(source, index)
|
|
250
269
|
finish = source.index('*/', index + 2)
|
|
251
270
|
finish = finish ? finish + 2 : source.length
|
|
@@ -387,6 +406,8 @@ module Mxrb
|
|
|
387
406
|
def identifier_text(token)
|
|
388
407
|
return token.text unless token.type == :quoted_identifier
|
|
389
408
|
|
|
409
|
+
return token.text[1...-1].gsub(']]', ']') if token.text.start_with?('[')
|
|
410
|
+
|
|
390
411
|
token.text[1...-1].gsub('""', '"')
|
|
391
412
|
end
|
|
392
413
|
|
|
@@ -428,3 +449,4 @@ module Mxrb
|
|
|
428
449
|
end
|
|
429
450
|
|
|
430
451
|
require_relative 'oql/analyzer'
|
|
452
|
+
require_relative 'oql/reverse_translator'
|
|
@@ -11,6 +11,7 @@ module Mxrb
|
|
|
11
11
|
# Inspects, previews upgrades, and compares generated definitions with an MPR.
|
|
12
12
|
class ProjectLifecycle
|
|
13
13
|
VERSION = /\A\d+\.\d+\.\d+\z/
|
|
14
|
+
VERSION_DECLARATION = /(?<prefix>mendix_version(?:\s+|\s*=\s*))["'](?<version>[^"']+)["']/
|
|
14
15
|
|
|
15
16
|
def initialize(root = Dir.pwd)
|
|
16
17
|
@root = File.expand_path(root)
|
|
@@ -36,7 +37,9 @@ module Mxrb
|
|
|
36
37
|
raise ArgumentError, 'project.rb has no mendix_version declaration' unless current
|
|
37
38
|
|
|
38
39
|
if apply
|
|
39
|
-
updated = source.sub(
|
|
40
|
+
updated = source.sub(VERSION_DECLARATION) do
|
|
41
|
+
"#{Regexp.last_match(:prefix)}\"#{target}\""
|
|
42
|
+
end
|
|
40
43
|
transaction = Scaffold::Transaction.new
|
|
41
44
|
transaction.write(@project_file, updated)
|
|
42
45
|
transaction.commit
|
|
@@ -70,7 +73,7 @@ module Mxrb
|
|
|
70
73
|
def declared_version
|
|
71
74
|
return unless File.file?(@project_file)
|
|
72
75
|
|
|
73
|
-
File.read(@project_file)[
|
|
76
|
+
File.read(@project_file).match(VERSION_DECLARATION)&.[](:version)
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
end
|