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