1 | # frozen_string_literal: true |
||
2 | |||
3 | 1 | require 'parslet' |
|
4 | |||
5 | # rubocop:disable Style/ClassAndModuleChildren |
||
6 | |||
7 | # Parslet DSL extension for capturing the input source |
||
8 | 1 | class CaptureSource < Parslet::Atoms::Capture |
|
9 | # Ugly hack to capture the source string that was parsed |
||
10 | 1 | def apply(source, context, consume_all) |
|
11 | 33 | before = source.instance_variable_get(:@str).rest |
|
12 | 33 | success, value = result = super(source, context, consume_all) |
|
13 | 33 | if success |
|
14 | # Save the portion of the source string |
||
15 | 33 | after = source.instance_variable_get(:@str).rest |
|
16 | 33 | source_str = before[0..(before.length - after.length - 1)] |
|
17 | 33 | value[(name.to_s + '_source').to_sym] = source_str |
|
0 ignored issues
–
show
introduced
by
![]() |
|||
18 | end |
||
19 | |||
20 | 33 | result |
|
21 | end |
||
22 | end |
||
23 | |||
24 | # Modify named captures to allow arrays |
||
25 | 1 | class Parslet::Atoms::Named < Parslet::Atoms::Base |
|
26 | 1 | def initialize(parslet, name, array = false) |
|
0 ignored issues
–
show
|
|||
27 | 3879 | super() |
|
28 | 3879 | @parslet = parslet |
|
29 | 3879 | @name = name |
|
30 | 3879 | @array = array |
|
31 | end |
||
32 | |||
33 | 1 | private |
|
34 | |||
35 | # Optionally wrap the produced single value in an array |
||
36 | 1 | def produce_return_value(val) |
|
37 | 4792 | flatval = flatten(val, true) |
|
38 | 4792 | flatval = [flatval] if @array && val.last == [:repetition] |
|
39 | 4792 | { name => flatval } |
|
40 | end |
||
41 | end |
||
42 | |||
43 | # Extend the DSL to with some additional ways to capture the output |
||
44 | 1 | module Parslet::Atoms::DSL |
|
45 | # Like #as, but ensures that the result is always an array |
||
46 | # @return [Array<Parslet::Atoms::Named>] |
||
47 | 1 | def as_array(name) |
|
48 | 1039 | Parslet::Atoms::Named.new(self, name, true) |
|
49 | end |
||
50 | |||
51 | # Capture some output along with the source string |
||
52 | # @return [CaptureSource] |
||
53 | 1 | def capture_source(name) |
|
54 | 33 | CaptureSource.new(self, name) |
|
55 | end |
||
56 | end |
||
57 | |||
58 | # rubocop:enable Style/ClassAndModuleChildren |
||
59 | |||
60 | 1 | module NoSE |
|
61 | # rubocop:disable Style/BlockEndNewline, Style/BlockDelimiters |
||
62 | # rubocop:disable Style/MultilineOperationIndentation |
||
63 | |||
64 | # Literals used in queries and updates |
||
65 | 1 | module Literals |
|
66 | 1 | include Parslet |
|
67 | |||
68 | 247 | rule(:integer) { match('[0-9]').repeat(1).as(:int) } |
|
69 | 239 | rule(:quote) { str('"') } |
|
70 | 28 | rule(:nonquote) { quote.absent? >> any } |
|
71 | 239 | rule(:string) { quote >> nonquote.repeat(1).as(:str) >> quote } |
|
72 | 247 | rule(:literal) { integer | string | str('?').as(:unknown) } |
|
73 | end |
||
74 | |||
75 | # Predicates used in queries and updates |
||
76 | 1 | module Predicates |
|
77 | 1 | include Parslet |
|
78 | |||
79 | 1 | rule(:operator) { |
|
80 | 216 | str('=') | str('!=') | str('<=') | str('>=') | str('<') | str('>') } |
|
81 | 1 | rule(:condition) { |
|
82 | 216 | field.as(:field) >> space? >> operator.as(:op) >> space? >> |
|
83 | literal.as(:value) } |
||
84 | 1 | rule(:expression) { |
|
85 | 216 | condition >> (space >> str('AND') >> space >> expression).repeat } |
|
86 | 1 | rule(:where) { |
|
87 | 216 | space >> str('WHERE') >> space >> expression.as_array(:expression) } |
|
88 | end |
||
89 | |||
90 | # Identifiers and combinations of them used in queries and updates |
||
91 | 1 | module Identifiers |
|
92 | 1 | include Parslet |
|
93 | |||
94 | 247 | rule(:identifier) { match('[A-z]').repeat(1).as(:identifier) } |
|
95 | 217 | rule(:field) { identifier >> (str('.') >> identifier).repeat(1) } |
|
96 | 42 | rule(:fields) { field >> (comma >> field).repeat } |
|
97 | 1 | rule(:select_field) { |
|
98 | 183 | field.as_array(:field) | (identifier >> str('.') >> |
|
99 | str('*').repeat(1, 2).as(:identifier2)) } |
||
100 | 184 | rule(:select_fields) { select_field >> (comma >> select_field).repeat } |
|
101 | 200 | rule(:path) { identifier >> (str('.') >> identifier).repeat } |
|
102 | end |
||
103 | |||
104 | # Field settings for update and insert statements |
||
105 | 1 | module UpdateSettings |
|
106 | 1 | include Parslet |
|
107 | |||
108 | 1 | rule(:setting) { |
|
109 | 41 | (identifier | str('**')).as(:field) >> space? >> str('=') >> space? >> |
|
110 | literal.as(:value) |
||
111 | } |
||
112 | 1 | rule(:settings) { |
|
113 | 41 | setting >> (space? >> str(',') >> space? >> setting).repeat |
|
114 | } |
||
115 | end |
||
116 | |||
117 | # Parser for a simple CQL-like grammar |
||
118 | 1 | class CQLP < Parslet::Parser |
|
119 | 1 | include Literals |
|
120 | 1 | include Identifiers |
|
121 | 1 | include Predicates |
|
122 | 1 | include UpdateSettings |
|
123 | |||
124 | 247 | rule(:space) { match('\s').repeat(1) } |
|
125 | 247 | rule(:space?) { space.maybe } |
|
126 | 184 | rule(:comma) { str(',') >> space? } |
|
127 | |||
128 | 184 | rule(:limit) { space >> str('LIMIT') >> space >> integer.as(:limit) } |
|
129 | 1 | rule(:order) { |
|
130 | 183 | space >> str('ORDER BY') >> space >> fields.as_array(:fields) } |
|
131 | |||
132 | 234 | rule(:comment) { str(' -- ') >> match('.').repeat } |
|
133 | |||
134 | 1 | rule(:query) { |
|
135 | 183 | str('SELECT') >> space >> select_fields.as_array(:select) >> |
|
136 | space >> str('FROM') >> space >> path.as_array(:path) >> |
||
137 | where.maybe.as(:where) >> order.maybe.as(:order) >> |
||
138 | limit.maybe.capture(:limit) >> comment.maybe.as(:comment) } |
||
139 | |||
140 | 1 | rule(:update) { |
|
141 | 24 | str('UPDATE') >> space >> identifier.as(:entity) >> space >> |
|
142 | 24 | (str('FROM') >> space >> path.as_array(:path) >> space).maybe >> |
|
143 | str('SET') >> space >> settings.as_array(:settings) >> |
||
144 | where.maybe.as(:where).capture_source(:where) >> |
||
145 | comment.maybe.as(:comment) |
||
146 | } |
||
147 | |||
148 | 1 | rule(:connect_item) { |
|
149 | 30 | identifier.as(:target) >> space? >> str('(') >> space? >> |
|
150 | literal.as(:target_pk) >> space? >> str(')') |
||
151 | } |
||
152 | |||
153 | 1 | rule(:connect_list) { |
|
154 | 17 | connect_item >> (space? >> str(',') >> space? >> connect_item).repeat |
|
155 | } |
||
156 | |||
157 | 1 | rule(:insert) { |
|
158 | 17 | str('INSERT INTO') >> space >> identifier.as(:entity) >> space >> |
|
159 | str('SET') >> space >> settings.as_array(:settings) >> |
||
160 | (space >> str('AND') >> space >> str('CONNECT') >> space >> |
||
161 | str('TO') >> space >> connect_list.as_array(:connections)).maybe >> |
||
162 | comment.maybe.as(:comment) |
||
163 | } |
||
164 | |||
165 | 1 | rule(:delete) { |
|
166 | 9 | str('DELETE') >> space >> identifier.as(:entity) >> |
|
167 | 9 | (space >> str('FROM') >> space >> path.as_array(:path)).maybe >> |
|
168 | where.maybe.as(:where).capture_source(:where) >> |
||
169 | comment.maybe.as(:comment) |
||
170 | } |
||
171 | |||
172 | 1 | rule(:connect) { |
|
173 | 13 | (str('CONNECT') | str('DISCONNECT')).capture(:type) >> space >> |
|
174 | identifier.as(:entity) >> space? >> str('(') >> space? >> |
||
175 | literal.as(:source_pk) >> space? >> str(')') >> space >> |
||
176 | dynamic do |_, context| |
||
177 | 13 | context.captures[:type] == 'CONNECT' ? str('TO') : str('FROM') |
|
178 | end >> space >> connect_item |
||
179 | } |
||
180 | |||
181 | 1 | rule(:statement) { |
|
182 | query | update | insert | delete | connect |
||
183 | } |
||
184 | |||
185 | 1 | root :statement |
|
186 | end |
||
187 | |||
188 | # Simple transformations to clean up the CQL parse tree |
||
189 | 1 | class CQLT < Parslet::Transform |
|
190 | 1520 | rule(identifier: simple(:identifier)) { identifier } |
|
191 | 1 | rule(identifier: simple(:identifier), identifier2: simple(:identifier2)) { |
|
192 | 40 | [identifier.to_s, identifier2.to_s] } |
|
193 | 203 | rule(field: sequence(:id)) { id.map(&:to_s) } |
|
194 | 1 | rule(path: sequence(:id)) { id.map(&:to_s) } |
|
195 | 57 | rule(str: simple(:string)) { string.to_s } |
|
196 | 1 | rule(statement: subtree(:stmt)) { stmt.first.last } |
|
197 | 37 | rule(int: simple(:integer)) { integer } |
|
198 | 364 | rule(unknown: simple(:val)) { nil } |
|
199 | end |
||
200 | |||
201 | # rubocop:enable all |
||
202 | end |
||
203 |