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.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +455 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/oql-sql.md +41 -0
  17. data/docs/pt-BR/project-structure.md +197 -2
  18. data/docs/pt-BR/runtime-ruby.md +108 -0
  19. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  20. data/docs/pt-BR/validation-matrix.md +42 -5
  21. data/lib/mxrb/cli/help.rb +426 -0
  22. data/lib/mxrb/cli/release.rb +137 -0
  23. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  24. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  25. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  26. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  27. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  28. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  29. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  30. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  31. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  32. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  33. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  34. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  35. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  36. data/lib/mxrb/domain_diagram.rb +409 -0
  37. data/lib/mxrb/dsl/builder.rb +43 -9
  38. data/lib/mxrb/environment.rb +226 -0
  39. data/lib/mxrb/exporter.rb +89 -15
  40. data/lib/mxrb/http/server.rb +107 -0
  41. data/lib/mxrb/initializer.rb +80 -9
  42. data/lib/mxrb/integrity/validator.rb +14 -0
  43. data/lib/mxrb/io/bson_codec.rb +42 -1
  44. data/lib/mxrb/io/mpr_file.rb +146 -3
  45. data/lib/mxrb/model/attribute.rb +3 -0
  46. data/lib/mxrb/model/entity.rb +28 -2
  47. data/lib/mxrb/model/microflow.rb +3 -1
  48. data/lib/mxrb/model/page.rb +257 -17
  49. data/lib/mxrb/model/project.rb +3 -0
  50. data/lib/mxrb/modeler/catalog.rb +178 -0
  51. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  52. data/lib/mxrb/oql/server.rb +4 -8
  53. data/lib/mxrb/oql.rb +23 -1
  54. data/lib/mxrb/project_lifecycle.rb +5 -2
  55. data/lib/mxrb/ruby_app/exporter.rb +2508 -0
  56. data/lib/mxrb/ruby_app/preset.rb +506 -0
  57. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  58. data/lib/mxrb/ruby_app.rb +1369 -0
  59. data/lib/mxrb/runtime/access_control.rb +408 -0
  60. data/lib/mxrb/runtime/native.rb +1015 -93
  61. data/lib/mxrb/runtime/scheduler.rb +396 -0
  62. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  63. data/lib/mxrb/runtime/shared_store.rb +335 -0
  64. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  65. data/lib/mxrb/scaffold/generator.rb +11 -3
  66. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  67. data/lib/mxrb/uml/class_diagram.rb +118 -0
  68. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  69. data/lib/mxrb/uml/server.rb +146 -0
  70. data/lib/mxrb/uml/support.rb +35 -0
  71. data/lib/mxrb/version.rb +1 -1
  72. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  73. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  74. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  75. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  76. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  77. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  78. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  79. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  80. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  81. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  83. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  84. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  85. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  86. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  87. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  88. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  89. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  90. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  93. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  94. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  95. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  96. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  98. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  99. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  100. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  101. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  102. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  103. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  104. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  105. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  106. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  107. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  108. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  109. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  110. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  111. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  112. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  113. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  114. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  115. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  116. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  117. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  118. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  119. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  120. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  121. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  122. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  123. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  124. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  125. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  126. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  127. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  128. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  129. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  130. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  131. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  132. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  133. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  134. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  135. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  136. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  137. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  138. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  139. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  140. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  141. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  142. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  143. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  144. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  145. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  146. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  147. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  148. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  149. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  150. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  151. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  152. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  153. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  154. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  155. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  156. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  157. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  158. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  159. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  160. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  161. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  162. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  163. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  164. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  165. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  166. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  167. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  168. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  169. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  170. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  171. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  172. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  173. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  174. data/lib/mxrb/web_ui/domain.html +15 -0
  175. data/lib/mxrb/web_ui/modeler.html +16 -0
  176. data/lib/mxrb/web_ui/uml.html +31 -0
  177. data/lib/mxrb/web_ui.rb +89 -0
  178. data/lib/mxrb/widget_certification.rb +208 -0
  179. data/lib/mxrb/writer.rb +200 -24
  180. data/lib/mxrb.rb +26 -1
  181. 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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
- require 'webrick'
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 || WEBrick::Log.new(File::NULL)
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 = WEBrick::HTTPServer.new(
31
- BindAddress: @host, Port: @port, Logger: @logger, AccessLog: []
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
- new.send(:tokenize, source).filter_map do |token|
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(/mendix_version\s+["'][^"']+["']/, %(mendix_version "#{target}"))
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)[/mendix_version\s+["']([^"']+)["']/, 1]
76
+ File.read(@project_file).match(VERSION_DECLARATION)&.[](:version)
74
77
  end
75
78
  end
76
79
  end