-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathselect_parser.rb
More file actions
466 lines (356 loc) · 15.3 KB
/
Copy pathselect_parser.rb
File metadata and controls
466 lines (356 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
require_relative 'sql_constants'
require_relative 'expression_parser'
require_relative 'parsing_utils'
module SelectParser
include SqlConstants
include ParsingUtils
def parse_select(sql)
if sql.match(/\bFROM\b/i)
parse_select_from(sql)
else
parse_simple_select(sql)
end
end
private
def parse_select_from(sql)
# First extract the SELECT expression list and FROM table
# The table name may be followed by optional alias
select_match = sql.match(/\ASELECT#{PATTERNS[:whitespace]}(.*?)#{PATTERNS[:whitespace]}FROM#{PATTERNS[:whitespace]}(#{PATTERNS[:identifier]})/im)
return parse_error unless select_match
select_list = select_match[1].strip
table_name = select_match[2]
# Parse the rest of the statement starting after the table name
remainder = sql[select_match.end(0)..-1].strip
# Check for table alias (but not if it's a keyword like INNER, LEFT, etc.)
table_alias = nil
alias_match = remainder.match(/\A(#{PATTERNS[:identifier]})(?=#{PATTERNS[:whitespace]}|;|\z)/im)
if alias_match && !alias_match[1].match(/\A(INNER|LEFT|RIGHT|FULL|JOIN|WHERE|GROUP|ORDER|LIMIT|OFFSET)\z/i)
table_alias = alias_match[1]
remainder = remainder[alias_match.end(0)..-1].strip
end
# Parse the expression list
expressions = parse_expression_list(select_list)
return expressions if is_error?(expressions)
# Start building the result
result = { type: :select_from, table_name: table_name, expressions: expressions }
result[:table_alias] = table_alias if table_alias
# Parse JOIN clauses if present
join_result = parse_join_clauses(remainder)
return join_result if is_error?(join_result)
if join_result[:joins] && !join_result[:joins].empty?
result[:joins] = join_result[:joins]
remainder = join_result[:remainder]
end
# Parse WHERE clause if present
where_result = parse_where_clause(remainder)
return where_result if is_error?(where_result)
if where_result[:where]
result[:where] = where_result[:where]
remainder = where_result[:remainder]
end
# Parse GROUP BY clause if present
group_result = parse_group_by_clause(remainder)
return group_result if is_error?(group_result)
if group_result[:group_by]
result[:group_by] = group_result[:group_by]
remainder = group_result[:remainder]
end
# Parse ORDER BY clause if present
order_result = parse_order_by_clause(remainder)
return order_result if is_error?(order_result)
if order_result[:order_by]
result[:order_by] = order_result[:order_by]
remainder = order_result[:remainder]
end
# Parse LIMIT/OFFSET clause if present
limit_result = parse_limit_clause(remainder)
return limit_result if is_error?(limit_result)
if limit_result[:limit] || limit_result[:offset]
result[:limit] = limit_result[:limit] if limit_result[:limit]
result[:offset] = limit_result[:offset] if limit_result[:offset]
remainder = limit_result[:remainder]
end
# Check for any remaining content (should only be whitespace and optional semicolon)
remainder = remainder.strip
unless remainder.match(/\A#{PATTERNS[:optional_semicolon]}\z/)
return parse_error
end
result
end
def parse_simple_select(sql)
match = sql.match(/\ASELECT#{PATTERNS[:optional_whitespace]}(.*?)#{PATTERNS[:optional_semicolon]}/im)
return parse_error unless match
remainder = sql[match.end(0)..-1].strip
return parse_error unless remainder.empty?
select_list = match[1].strip
parsed_items = parse_expression_list(select_list)
return parsed_items if is_error?(parsed_items)
expressions = parsed_items.map { |item| item[:expression] }
columns = parsed_items.map { |item| item[:alias] }
{ type: :select, expressions: expressions, columns: columns }
end
def parse_expression_list(select_list)
return [] if select_list.empty?
parts = split_on_comma(select_list)
expressions = []
parts.each do |part|
expr_str, column_alias = extract_alias(part.strip)
parser = ExpressionParser.new
parsed_expr = parser.parse(expr_str)
return parsed_expr if is_error?(parsed_expr)
expressions << { expression: parsed_expr, alias: column_alias }
end
expressions
end
private
def extract_alias(expression)
alias_match = expression.match(/(.+?)\s+AS\s+(#{PATTERNS[:identifier]})\s*\z/i)
if alias_match
[alias_match[1].strip, alias_match[2]]
else
[expression.strip, nil]
end
end
def parse_where_clause(sql)
sql = sql.strip
return { where: nil, remainder: sql } unless sql.match(/\AWHERE\b/i)
# Match WHERE keyword and capture the rest
match = sql.match(/\AWHERE#{PATTERNS[:whitespace]}(.*)/im)
return parse_error unless match
remainder = match[1]
# Parse the WHERE expression - it ends at GROUP, ORDER, LIMIT, OFFSET, or semicolon/end
expr_match = remainder.match(/\A(.*?)(?:#{PATTERNS[:whitespace]}(?:GROUP|ORDER|LIMIT|OFFSET)\b|\s*;|\s*\z)/im)
if expr_match
where_expr_str = expr_match[1].strip
# Check for empty WHERE expression
return parse_error if where_expr_str.empty?
parser = ExpressionParser.new
where_expr = parser.parse(where_expr_str)
return where_expr if is_error?(where_expr)
# Calculate the remainder after the WHERE expression
remainder_start = expr_match.end(1)
new_remainder = remainder[remainder_start..-1]
{ where: where_expr, remainder: new_remainder }
else
# WHERE clause takes everything remaining
where_expr_str = remainder.strip
# Check for empty WHERE expression
return parse_error if where_expr_str.empty?
parser = ExpressionParser.new
where_expr = parser.parse(where_expr_str)
return where_expr if is_error?(where_expr)
{ where: where_expr, remainder: '' }
end
end
def parse_group_by_clause(sql)
sql = sql.strip
# Check if GROUP BY is present
group_match = sql.match(/\AGROUP#{PATTERNS[:whitespace]}BY\b/i)
return { group_by: nil, remainder: sql } unless group_match
# Get everything after GROUP BY
remainder = sql[group_match.end(0)..-1].strip
# Parse the GROUP BY expression
# It ends at ORDER, LIMIT, OFFSET, semicolon, or end of string
expr_match = remainder.match(/\A(.*?)(?:#{PATTERNS[:whitespace]}(?:ORDER|LIMIT|OFFSET)\b|\s*;|\s*\z)/im)
if expr_match
group_clause = expr_match[1].strip
# Check for empty GROUP BY expression
return parse_error if group_clause.empty?
# Parse the group expression
parser = ExpressionParser.new
group_expr = parser.parse(group_clause)
return group_expr if is_error?(group_expr)
# Calculate the remainder
remainder_start = expr_match.end(1)
new_remainder = remainder[remainder_start..-1]
{ group_by: group_expr, remainder: new_remainder }
else
{ group_by: nil, remainder: sql }
end
end
def parse_order_by_clause(sql)
sql = sql.strip
# Check if ORDER BY is present
order_match = sql.match(/\AORDER#{PATTERNS[:whitespace]}BY\b/i)
return { order_by: nil, remainder: sql } unless order_match
# Get everything after ORDER BY
remainder = sql[order_match.end(0)..-1].strip
# Parse the ORDER BY expression and optional direction
# It ends at LIMIT, OFFSET, semicolon, or end of string
expr_match = remainder.match(/\A(.*?)(?:#{PATTERNS[:whitespace]}(?:LIMIT|OFFSET)\b|\s*;|\s*\z)/im)
if expr_match
order_clause = expr_match[1].strip
# Check for empty ORDER BY expression
return parse_error if order_clause.empty?
# Parse direction (ASC/DESC) if present
dir_match = order_clause.match(/\A(.+?)#{PATTERNS[:whitespace]}(ASC|DESC)\s*\z/i)
if dir_match
order_expr_str = dir_match[1].strip
direction = dir_match[2].upcase
# Check for empty expression before direction
return parse_error if order_expr_str.empty?
# Check for invalid direction keywords
if order_expr_str.match(/\b(ASC|DESC|CLOCKWISE|COUNTERCLOCKWISE)\s*\z/i)
return parse_error
end
else
# Check for ASC/DESC at the beginning (no expression)
if order_clause.match(/\A(ASC|DESC)\s*\z/i)
return parse_error
end
# Check for invalid keywords at the end
if order_clause.match(/\b(CLOCKWISE|COUNTERCLOCKWISE)\s*\z/i)
return parse_error
end
order_expr_str = order_clause
direction = 'ASC' # Default direction
end
# Parse the order expression
parser = ExpressionParser.new
order_expr = parser.parse(order_expr_str)
return order_expr if is_error?(order_expr)
# Calculate the remainder
remainder_start = expr_match.end(1)
new_remainder = remainder[remainder_start..-1]
{ order_by: { expression: order_expr, direction: direction }, remainder: new_remainder }
else
{ order_by: nil, remainder: sql }
end
end
def parse_join_clauses(sql)
joins = []
remainder = sql
while remainder.match(/\A(INNER|LEFT\s+OUTER|RIGHT\s+OUTER|FULL\s+OUTER)#{PATTERNS[:whitespace]}JOIN\b/i)
# Match the JOIN type
join_match = remainder.match(/\A(INNER|LEFT\s+OUTER|RIGHT\s+OUTER|FULL\s+OUTER)#{PATTERNS[:whitespace]}JOIN#{PATTERNS[:whitespace]}(#{PATTERNS[:identifier]})/im)
unless join_match
# Try to match JOIN without type specification, should fail
if remainder.match(/\AJOIN\b/i)
return parse_error
end
break
end
join_type = join_match[1].gsub(/\s+/, '_').upcase
joined_table = join_match[2]
# Move past the JOIN clause
remainder = remainder[join_match.end(0)..-1].strip
# Check for table alias (but not if it's ON or another keyword)
joined_alias = nil
alias_match = remainder.match(/\A(#{PATTERNS[:identifier]})(?=#{PATTERNS[:whitespace]}|;|\z)/im)
if alias_match && !alias_match[1].match(/\A(ON|INNER|LEFT|RIGHT|FULL|JOIN|WHERE|GROUP|ORDER|LIMIT|OFFSET)\z/i)
joined_alias = alias_match[1]
remainder = remainder[alias_match.end(0)..-1].strip
end
# Check for ON clause (required for INNER JOIN)
if remainder.match(/\AON\b/i)
on_match = remainder.match(/\AON#{PATTERNS[:whitespace]}(.*)/im)
return parse_error unless on_match
on_remainder = on_match[1]
# Parse ON expression - ends at next JOIN, WHERE, GROUP, ORDER, LIMIT, OFFSET, semicolon, or end
on_expr_match = on_remainder.match(/\A(.*?)(?:#{PATTERNS[:whitespace]}(?:INNER|LEFT|RIGHT|FULL|JOIN|WHERE|GROUP|ORDER|LIMIT|OFFSET)\b|\s*;|\s*\z)/im)
if on_expr_match
on_expr_str = on_expr_match[1].strip
# Check for empty ON expression
return parse_error if on_expr_str.empty?
parser = ExpressionParser.new
on_expr = parser.parse(on_expr_str)
return on_expr if is_error?(on_expr)
join_info = {
type: join_type,
table: joined_table,
on: on_expr
}
join_info[:alias] = joined_alias if joined_alias
joins << join_info
# Update remainder
remainder_start = on_expr_match.end(1)
remainder = on_remainder[remainder_start..-1].strip
else
# ON clause takes everything remaining
on_expr_str = on_remainder.strip
# Check for empty ON expression
return parse_error if on_expr_str.empty?
parser = ExpressionParser.new
on_expr = parser.parse(on_expr_str)
return on_expr if is_error?(on_expr)
join_info = {
type: join_type,
table: joined_table,
on: on_expr
}
join_info[:alias] = joined_alias if joined_alias
joins << join_info
remainder = ''
end
else
# Missing ON clause for INNER JOIN
return parse_error
end
end
{ joins: joins, remainder: remainder }
end
def parse_limit_clause(sql)
sql = sql.strip
# Check for LIMIT
limit_match = sql.match(/\ALIMIT#{PATTERNS[:whitespace]}(.*)/im)
# Check for OFFSET without LIMIT
offset_only_match = sql.match(/\AOFFSET#{PATTERNS[:whitespace]}(.*)/im)
if limit_match
remainder = limit_match[1]
# Parse LIMIT expression - ends at OFFSET, semicolon, or end
limit_expr_match = remainder.match(/\A(.*?)(?:#{PATTERNS[:whitespace]}OFFSET\b|\s*;|\s*\z)/im)
if limit_expr_match
limit_expr_str = limit_expr_match[1].strip
# Check for empty LIMIT expression
return parse_error if limit_expr_str.empty?
parser = ExpressionParser.new
limit_expr = parser.parse(limit_expr_str)
return limit_expr if is_error?(limit_expr)
result = { limit: limit_expr }
# Check for OFFSET after LIMIT
remainder_after_limit = remainder[limit_expr_match.end(1)..-1].strip
offset_match = remainder_after_limit.match(/\AOFFSET#{PATTERNS[:whitespace]}(.*)/im)
if offset_match
offset_remainder = offset_match[1]
# Parse OFFSET expression - ends at semicolon or end
offset_expr_match = offset_remainder.match(/\A(.*?)(?:\s*;|\s*\z)/im)
if offset_expr_match
offset_expr_str = offset_expr_match[1].strip
# Check for empty OFFSET expression
return parse_error if offset_expr_str.empty?
parser = ExpressionParser.new
offset_expr = parser.parse(offset_expr_str)
return offset_expr if is_error?(offset_expr)
result[:offset] = offset_expr
result[:remainder] = offset_remainder[offset_expr_match.end(1)..-1]
else
result[:remainder] = remainder_after_limit
end
else
result[:remainder] = remainder_after_limit
end
result
else
{ limit: nil, offset: nil, remainder: sql }
end
elsif offset_only_match
# Handle OFFSET without LIMIT
remainder = offset_only_match[1]
# Parse OFFSET expression - ends at semicolon or end
offset_expr_match = remainder.match(/\A(.*?)(?:\s*;|\s*\z)/im)
if offset_expr_match
offset_expr_str = offset_expr_match[1].strip
# Check for empty OFFSET expression
return parse_error if offset_expr_str.empty?
parser = ExpressionParser.new
offset_expr = parser.parse(offset_expr_str)
return offset_expr if is_error?(offset_expr)
{ limit: nil, offset: offset_expr, remainder: remainder[offset_expr_match.end(1)..-1] }
else
{ limit: nil, offset: nil, remainder: sql }
end
else
{ limit: nil, offset: nil, remainder: sql }
end
end
end