plume 0.2.0 → 0.3.0
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/README.md +244 -19
- data/lib/plume/analysis/resolver.rb +255 -32
- data/lib/plume/analysis.rb +93 -17
- data/lib/plume/ast/alter_table_statement.rb +14 -3
- data/lib/plume/ast/check_table_constraint.rb +1 -0
- data/lib/plume/ast/collate_column_constraint.rb +1 -1
- data/lib/plume/ast/collation_expression.rb +1 -1
- data/lib/plume/ast/column_type.rb +2 -1
- data/lib/plume/ast/deferrable_column_constraint.rb +20 -0
- data/lib/plume/ast/function_arguments.rb +2 -1
- data/lib/plume/ast/indexed_column.rb +4 -1
- data/lib/plume/ast/node.rb +135 -12
- data/lib/plume/ast/ordering_term.rb +1 -1
- data/lib/plume/ast/token.rb +7 -4
- data/lib/plume/catalog.rb +149 -53
- data/lib/plume/comment.rb +73 -0
- data/lib/plume/dump.rb +143 -0
- data/lib/plume/fingerprint.rb +86 -0
- data/lib/plume/lexer.rb +8 -7
- data/lib/plume/literal_patches.rb +76 -0
- data/lib/plume/normalize.rb +65 -0
- data/lib/plume/parameters.rb +52 -0
- data/lib/plume/parser.rb +221 -172
- data/lib/plume/pretty_printer.rb +202 -0
- data/lib/plume/printer.rb +71 -17
- data/lib/plume/truncate.rb +67 -0
- data/lib/plume/version.rb +1 -1
- data/lib/plume.rb +134 -3
- metadata +11 -2
data/lib/plume/parser.rb
CHANGED
|
@@ -10,16 +10,29 @@ module Plume
|
|
|
10
10
|
@sql, @token, @beg, @fin, @expected = sql, token, beg, fin, expected
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def to_error
|
|
14
|
+
# At the end of input the span sits one byte past the SQL
|
|
15
|
+
location = [@beg, [@fin, @sql.bytesize].min].freeze
|
|
16
|
+
SyntaxError.new(msg, location:, token: @token, line:, expected: @expected.freeze)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def line = @sql.b.byteslice(0, @beg).count("\n") + 1
|
|
20
|
+
|
|
13
21
|
def msg
|
|
14
|
-
|
|
15
|
-
where =
|
|
22
|
+
src = @sql.b # the SQL can be invalid UTF-8, which String#strip will not take
|
|
23
|
+
where = src.strip.include?("\n") ? " line #{line}" : ""
|
|
16
24
|
what = @token ? "token #{@token}[#{@sql.byteslice(@beg, @fin - @beg).inspect}]" : "end of input"
|
|
17
25
|
"Unexpected #{what} at#{where}:\n#{Plume.excerpt(@sql, @beg, @fin)}\n Expected one of: #{@expected.join(", ")}\n"
|
|
18
26
|
end
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
# see: https://github.com/sqlite/sqlite/blob/master/src/parse.y#L255-L283
|
|
30
|
+
# Literals in an expression, names anywhere else: SQLite has no TRUE or FALSE keyword, and lets
|
|
31
|
+
# the CURRENT_* ones (CTIME_KW) fall back to names
|
|
32
|
+
LITERAL_KEYWORDS = Set[:TRUE, :FALSE, :CURRENT_DATE, :CURRENT_TIME, :CURRENT_TIMESTAMP].freeze
|
|
33
|
+
|
|
22
34
|
TOKEN_FALLBACKS = Set[
|
|
35
|
+
*LITERAL_KEYWORDS,
|
|
23
36
|
:EXPLAIN,
|
|
24
37
|
:QUERY,
|
|
25
38
|
:PLAN,
|
|
@@ -57,6 +70,7 @@ module Plume
|
|
|
57
70
|
:GLOB,
|
|
58
71
|
:LIKE,
|
|
59
72
|
:ROWID,
|
|
73
|
+
:STORED,
|
|
60
74
|
:STRICT,
|
|
61
75
|
:COLUMN,
|
|
62
76
|
:DO,
|
|
@@ -147,11 +161,11 @@ module Plume
|
|
|
147
161
|
result = catch(:ERROR) { sql_stmt_list }
|
|
148
162
|
|
|
149
163
|
if ErrorMessage === result
|
|
150
|
-
raise
|
|
164
|
+
raise result.to_error
|
|
151
165
|
else
|
|
152
166
|
result
|
|
153
167
|
end
|
|
154
|
-
rescue
|
|
168
|
+
rescue *STACK_EXHAUSTED
|
|
155
169
|
# How deep is too deep depends on the stack the caller has left, so no fixture relies on it
|
|
156
170
|
raise SyntaxError.new("statement nested too deeply")
|
|
157
171
|
end
|
|
@@ -651,6 +665,11 @@ module Plume
|
|
|
651
665
|
# cmd ::= ALTER TABLE add_column_fullname ADD kwcolumn_opt columnname carglist
|
|
652
666
|
# cmd ::= ALTER TABLE fullname DROP kwcolumn_opt nm
|
|
653
667
|
# cmd ::= ALTER TABLE fullname RENAME kwcolumn_opt nm TO nm
|
|
668
|
+
# cmd ::= ALTER TABLE fullname DROP CONSTRAINT nm
|
|
669
|
+
# cmd ::= ALTER TABLE fullname ALTER kwcolumn_opt nm DROP NOT NULL
|
|
670
|
+
# cmd ::= ALTER TABLE fullname ALTER kwcolumn_opt nm SET NOT NULL onconf
|
|
671
|
+
# cmd ::= ALTER TABLE fullname ADD CONSTRAINT nm CHECK LP expr RP onconf
|
|
672
|
+
# cmd ::= ALTER TABLE fullname ADD CHECK LP expr RP onconf
|
|
654
673
|
# kwcolumn_opt ::= . | COLUMNKW
|
|
655
674
|
#
|
|
656
675
|
# Relevant SQLite documentation:
|
|
@@ -669,26 +688,69 @@ module Plume
|
|
|
669
688
|
column_tk = name
|
|
670
689
|
to_kw = require :TO
|
|
671
690
|
new_name_tk = name
|
|
691
|
+
elsif current_token == :ADD && (peek in :CONSTRAINT | :CHECK)
|
|
692
|
+
action = :ADD_CONSTRAINT
|
|
693
|
+
action_kw = require :ADD
|
|
694
|
+
constraint_name = name if (constraint_kw = maybe :CONSTRAINT)
|
|
695
|
+
constraint = check_table_constraint(constraint_kw, constraint_name)
|
|
672
696
|
elsif (action_kw = maybe_one_of [:ADD, :COLUMN].freeze, :ADD)
|
|
673
697
|
action = :ADD_COLUMN
|
|
674
698
|
column_definition = column_def
|
|
699
|
+
elsif current_token == :DROP && peek == :CONSTRAINT
|
|
700
|
+
action = :DROP_CONSTRAINT
|
|
701
|
+
action_kw = require_all_of :DROP, :CONSTRAINT
|
|
702
|
+
constraint_name_tk = name
|
|
675
703
|
elsif (action_kw = maybe_one_of [:DROP, :COLUMN].freeze, :DROP)
|
|
676
704
|
action = :DROP_COLUMN
|
|
677
705
|
column_tk = name
|
|
706
|
+
elsif (action_kw = maybe_one_of [:ALTER, :COLUMN].freeze, :ALTER)
|
|
707
|
+
column_tk = name
|
|
708
|
+
if current_token == :DROP
|
|
709
|
+
action = :DROP_NOT_NULL
|
|
710
|
+
not_null_kw = require_all_of :DROP, :NOT, :NULL
|
|
711
|
+
else
|
|
712
|
+
action = :SET_NOT_NULL
|
|
713
|
+
not_null_kw = require_all_of :SET, :NOT, :NULL
|
|
714
|
+
on_conflict = maybe { conflict_clause }
|
|
715
|
+
end
|
|
678
716
|
else
|
|
679
|
-
expected!(:RENAME, :ADD, :DROP)
|
|
717
|
+
expected!(:RENAME, :ADD, :DROP, :ALTER)
|
|
680
718
|
end
|
|
681
719
|
|
|
682
720
|
AlterTableStatement.concrete(
|
|
683
|
-
full_source:
|
|
684
|
-
alter_table_kw:
|
|
685
|
-
table:
|
|
686
|
-
action:
|
|
687
|
-
action_kw:
|
|
688
|
-
column_tk:
|
|
689
|
-
to_kw:
|
|
690
|
-
new_name_tk:
|
|
691
|
-
column_definition:
|
|
721
|
+
full_source: @lexer.sql,
|
|
722
|
+
alter_table_kw: Token::Keyword(alter_table_kw),
|
|
723
|
+
table: table,
|
|
724
|
+
action: action,
|
|
725
|
+
action_kw: Token::Keyword(action_kw),
|
|
726
|
+
column_tk: Token::Identifier(column_tk),
|
|
727
|
+
to_kw: Token::Keyword(to_kw),
|
|
728
|
+
new_name_tk: Token::Identifier(new_name_tk),
|
|
729
|
+
column_definition: column_definition,
|
|
730
|
+
constraint: constraint,
|
|
731
|
+
constraint_name_tk: Token::Identifier(constraint_name_tk),
|
|
732
|
+
not_null_kw: Token::Keyword(not_null_kw),
|
|
733
|
+
conflict_clause: on_conflict,
|
|
734
|
+
)
|
|
735
|
+
end
|
|
736
|
+
|
|
737
|
+
# tcons ::= CHECK LP expr RP onconf, after the `CONSTRAINT nm` the caller may have read
|
|
738
|
+
def check_table_constraint(constraint_kw = nil, constraint_name = nil)
|
|
739
|
+
check_kw = require :CHECK
|
|
740
|
+
check_lp = require :LP
|
|
741
|
+
check = expression
|
|
742
|
+
check_rp = require :RP
|
|
743
|
+
on_conflict = maybe { conflict_clause }
|
|
744
|
+
|
|
745
|
+
CheckTableConstraint.concrete(
|
|
746
|
+
full_source: @lexer.sql,
|
|
747
|
+
constraint_kw: Token::Keyword(constraint_kw),
|
|
748
|
+
name_tk: Token::Identifier(constraint_name),
|
|
749
|
+
check_kw: Token::Keyword(check_kw),
|
|
750
|
+
check_lp: Token::Punctuation(check_lp),
|
|
751
|
+
expression: check,
|
|
752
|
+
check_rp: Token::Punctuation(check_rp),
|
|
753
|
+
conflict_clause: on_conflict,
|
|
692
754
|
)
|
|
693
755
|
end
|
|
694
756
|
|
|
@@ -874,8 +936,11 @@ module Plume
|
|
|
874
936
|
select_statement: as_select
|
|
875
937
|
)
|
|
876
938
|
elsif (columns_lp = maybe :LP)
|
|
877
|
-
|
|
878
|
-
|
|
939
|
+
# The comma after the last column leads into the table constraints: `columnlist COMMA conslist`
|
|
940
|
+
columns = require_some(given: column_def, trailing_sep: :COMMA) do
|
|
941
|
+
column_def unless current_token in :CONSTRAINT | :PRIMARY | :UNIQUE | :CHECK | :FOREIGN
|
|
942
|
+
end
|
|
943
|
+
# inlined `table_constraint`, but for CHECK, which ALTER TABLE shares through check_table_constraint
|
|
879
944
|
constraints = maybe_some(trailing_sep: [:COMMA, nil].freeze) do
|
|
880
945
|
#
|
|
881
946
|
# Relevant `parse.y` grammar rules:
|
|
@@ -895,7 +960,7 @@ module Plume
|
|
|
895
960
|
# │ └───────{ , }◀──────┘└────────▶─────────┘ │
|
|
896
961
|
# ├─▶{ UNIQUE }──▶{ ( }┬▶[ indexed-column ]┬▶{ ) }─▶[ conflict-clause ]────────────────────────────────▶─┤
|
|
897
962
|
# │ └───────{ , }◀──────┘ │
|
|
898
|
-
# ├─▶{ CHECK }─▶{ ( }─▶[ expr ]─▶{ ) }
|
|
963
|
+
# ├─▶{ CHECK }─▶{ ( }─▶[ expr ]─▶{ ) }─▶[ conflict-clause ]────────────────────────────────────────────▶─┤
|
|
899
964
|
# └─▶{ FOREIGN }─▶{ KEY }─▶{ ( }┬▶[ indexed-column ]┬▶{ ) }─▶[ foreign-key-clause ]────────────────────▶─┘
|
|
900
965
|
# └──────{ , }◀───────┘
|
|
901
966
|
#
|
|
@@ -911,9 +976,9 @@ module Plume
|
|
|
911
976
|
constraint_name = name
|
|
912
977
|
end
|
|
913
978
|
|
|
914
|
-
if (unique_kw = maybe
|
|
979
|
+
if (unique_kw = maybe :UNIQUE)
|
|
915
980
|
unique_lp = require :LP
|
|
916
|
-
unique_columns = require_some(trailing_sep: :COMMA) { indexed_column }
|
|
981
|
+
unique_columns = require_some(trailing_sep: :COMMA) { indexed_column(nulls: true) }
|
|
917
982
|
unique_rp = require :RP
|
|
918
983
|
on_conflict = maybe { conflict_clause }
|
|
919
984
|
|
|
@@ -927,23 +992,11 @@ module Plume
|
|
|
927
992
|
unique_rp: Token::Punctuation(unique_rp),
|
|
928
993
|
conflict_clause: on_conflict,
|
|
929
994
|
)
|
|
930
|
-
elsif
|
|
931
|
-
|
|
932
|
-
check = expression
|
|
933
|
-
check_rp = require :RP
|
|
934
|
-
|
|
935
|
-
CheckTableConstraint.concrete(
|
|
936
|
-
full_source: @lexer.sql,
|
|
937
|
-
constraint_kw: Token::Keyword(constraint_kw),
|
|
938
|
-
name_tk: Token::Identifier(constraint_name),
|
|
939
|
-
check_kw: Token::Keyword(check_kw),
|
|
940
|
-
check_lp: Token::Punctuation(check_lp),
|
|
941
|
-
expression: check,
|
|
942
|
-
check_rp: Token::Punctuation(check_rp),
|
|
943
|
-
)
|
|
995
|
+
elsif current_token == :CHECK
|
|
996
|
+
check_table_constraint(constraint_kw, constraint_name)
|
|
944
997
|
elsif (primary_key_kw = maybe_all_of :PRIMARY, :KEY)
|
|
945
998
|
primary_key_lp = require :LP
|
|
946
|
-
primary_key_columns = require_some(trailing_sep: :COMMA) { indexed_column }
|
|
999
|
+
primary_key_columns = require_some(trailing_sep: :COMMA) { indexed_column(nulls: true) }
|
|
947
1000
|
autoincrement_kw = maybe :AUTOINCREMENT
|
|
948
1001
|
primary_key_rp = require :RP
|
|
949
1002
|
on_conflict = maybe { conflict_clause }
|
|
@@ -1457,7 +1510,8 @@ module Plume
|
|
|
1457
1510
|
left = basic_expression
|
|
1458
1511
|
|
|
1459
1512
|
while (operator = current_token)
|
|
1460
|
-
|
|
1513
|
+
# ~ has a precedence for its operand's sake, but is no binary operator
|
|
1514
|
+
operator_precedence = ((operator != :BITNOT) && OPERATOR_PRECEDENCE[operator]) || -1
|
|
1461
1515
|
break if operator_precedence < min_precedence
|
|
1462
1516
|
next_precedence = operator_precedence + 1
|
|
1463
1517
|
|
|
@@ -1497,39 +1551,20 @@ module Plume
|
|
|
1497
1551
|
else
|
|
1498
1552
|
expected!(:RP, "select-stmt", "expr")
|
|
1499
1553
|
end
|
|
1500
|
-
elsif (
|
|
1501
|
-
|
|
1502
|
-
|
|
1554
|
+
elsif (first_tk = maybe { name })
|
|
1555
|
+
# nm dbnm paren_exprlist: a table, or a table-valued function's call
|
|
1556
|
+
dot_tk = maybe :DOT
|
|
1557
|
+
schema_tk, object_tk = dot_tk ? [first_tk, name] : [nil, first_tk]
|
|
1503
1558
|
|
|
1504
|
-
|
|
1505
|
-
arguments = maybe_some(trailing_sep: :COMMA) { expression }
|
|
1506
|
-
arguments_rp = require :RP
|
|
1507
|
-
|
|
1508
|
-
right = FunctionReference.concrete(
|
|
1509
|
-
full_source: @lexer.sql,
|
|
1510
|
-
schema_tk: Token::Identifier(ref),
|
|
1511
|
-
function_tk: Token::Identifier(name),
|
|
1512
|
-
arguments_lp: Token::Punctuation(arguments_lp),
|
|
1513
|
-
arguments: FunctionArguments.concrete(
|
|
1514
|
-
full_source: @lexer.sql,
|
|
1515
|
-
expressions: arguments
|
|
1516
|
-
),
|
|
1517
|
-
arguments_rp: Token::Punctuation(arguments_rp),
|
|
1518
|
-
)
|
|
1519
|
-
else
|
|
1520
|
-
right = TableName.concrete(
|
|
1521
|
-
full_source: @lexer.sql,
|
|
1522
|
-
schema_tk: Token::Identifier(ref),
|
|
1523
|
-
table_tk: Token::Identifier(name),
|
|
1524
|
-
)
|
|
1525
|
-
end
|
|
1526
|
-
elsif (arguments_lp = maybe :LP)
|
|
1559
|
+
if (arguments_lp = maybe :LP)
|
|
1527
1560
|
arguments = maybe_some(trailing_sep: :COMMA) { expression }
|
|
1528
1561
|
arguments_rp = require :RP
|
|
1529
1562
|
|
|
1530
1563
|
right = FunctionReference.concrete(
|
|
1531
1564
|
full_source: @lexer.sql,
|
|
1532
|
-
|
|
1565
|
+
schema_tk: Token::Identifier(schema_tk),
|
|
1566
|
+
dot_tk: Token::Punctuation(dot_tk),
|
|
1567
|
+
function_tk: Token::Identifier(object_tk),
|
|
1533
1568
|
arguments_lp: Token::Punctuation(arguments_lp),
|
|
1534
1569
|
arguments: FunctionArguments.concrete(
|
|
1535
1570
|
full_source: @lexer.sql,
|
|
@@ -1538,15 +1573,15 @@ module Plume
|
|
|
1538
1573
|
arguments_rp: Token::Punctuation(arguments_rp),
|
|
1539
1574
|
)
|
|
1540
1575
|
else
|
|
1541
|
-
table = ref
|
|
1542
|
-
|
|
1543
1576
|
right = TableName.concrete(
|
|
1544
1577
|
full_source: @lexer.sql,
|
|
1545
|
-
|
|
1578
|
+
schema_tk: Token::Identifier(schema_tk),
|
|
1579
|
+
dot_tk: Token::Punctuation(dot_tk),
|
|
1580
|
+
table_tk: Token::Identifier(object_tk),
|
|
1546
1581
|
)
|
|
1547
1582
|
end
|
|
1548
1583
|
else
|
|
1549
|
-
expected!(:LP, :
|
|
1584
|
+
expected!(:LP, :STRING, "idj")
|
|
1550
1585
|
end
|
|
1551
1586
|
|
|
1552
1587
|
left = BinaryExpression.concrete(
|
|
@@ -1801,6 +1836,13 @@ module Plume
|
|
|
1801
1836
|
not_null_kw: Token::Keyword(not_null_kw),
|
|
1802
1837
|
conflict_clause: on_conflict,
|
|
1803
1838
|
)
|
|
1839
|
+
elsif current_token == :DEFERRABLE || (current_token == :NOT && peek == :DEFERRABLE)
|
|
1840
|
+
DeferrableColumnConstraint.concrete(
|
|
1841
|
+
full_source: @lexer.sql,
|
|
1842
|
+
constraint_kw: Token::Keyword(constraint_kw),
|
|
1843
|
+
name_tk: Token::Identifier(constraint_name),
|
|
1844
|
+
deferrable_span: Token::Keyword(defer_subclause),
|
|
1845
|
+
)
|
|
1804
1846
|
elsif (null_kw = maybe :NULL)
|
|
1805
1847
|
on_conflict = maybe { conflict_clause }
|
|
1806
1848
|
|
|
@@ -2083,7 +2125,7 @@ module Plume
|
|
|
2083
2125
|
|
|
2084
2126
|
# The inside of a window-defn parenthesis, shared by OVER (…) and WINDOW name AS (…)
|
|
2085
2127
|
def window_body
|
|
2086
|
-
base_window_name_tk = maybe {
|
|
2128
|
+
base_window_name_tk = maybe { name(except: [:PARTITION, :ORDER, :RANGE, :ROWS, :GROUPS]) }
|
|
2087
2129
|
if (partition_by_kw = maybe_all_of(:PARTITION, :BY))
|
|
2088
2130
|
partitions = require_some(trailing_sep: :COMMA) { expression }
|
|
2089
2131
|
end
|
|
@@ -2184,7 +2226,7 @@ module Plume
|
|
|
2184
2226
|
window_rp: Token::Punctuation(window_rp),
|
|
2185
2227
|
)
|
|
2186
2228
|
else
|
|
2187
|
-
window_name_tk =
|
|
2229
|
+
window_name_tk = name
|
|
2188
2230
|
|
|
2189
2231
|
OverClause.concrete(
|
|
2190
2232
|
full_source: @lexer.sql,
|
|
@@ -2245,9 +2287,12 @@ module Plume
|
|
|
2245
2287
|
columns = require_some(trailing_sep: :COMMA) { indexed_column }
|
|
2246
2288
|
columns_rp = require :RP
|
|
2247
2289
|
end
|
|
2248
|
-
|
|
2290
|
+
# refargs ::= refargs refarg, so ON and MATCH clauses come in any order
|
|
2291
|
+
refargs = maybe_some(trailing_sep: nil) do
|
|
2292
|
+
next match_clause if current_token == :MATCH
|
|
2293
|
+
|
|
2249
2294
|
on_kw = require :ON
|
|
2250
|
-
trigger_kw = require_one_of :DELETE, :UPDATE
|
|
2295
|
+
trigger_kw = require_one_of :DELETE, :UPDATE, :INSERT
|
|
2251
2296
|
action_kw = require_one_of :CASCADE, :RESTRICT, [:SET, :NULL].freeze, [:SET, :DEFAULT].freeze, [:NO, :ACTION].freeze
|
|
2252
2297
|
ForeignKeyAction.concrete(
|
|
2253
2298
|
full_source: @lexer.sql,
|
|
@@ -2256,24 +2301,9 @@ module Plume
|
|
|
2256
2301
|
action_kw: Token::Keyword(action_kw),
|
|
2257
2302
|
)
|
|
2258
2303
|
end
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
MatchClause.concrete(
|
|
2263
|
-
full_source: @lexer.sql,
|
|
2264
|
-
match_kw: Token::Keyword(match_kw),
|
|
2265
|
-
type_tk: Token::Identifier(type_tk),
|
|
2266
|
-
)
|
|
2267
|
-
end
|
|
2268
|
-
|
|
2269
|
-
deferrable_span = maybe_one_of(
|
|
2270
|
-
[:NOT, :DEFERRABLE, :INITIALLY, :IMMEDIATE].freeze,
|
|
2271
|
-
[:NOT, :DEFERRABLE, :INITIALLY, :DEFERRED].freeze,
|
|
2272
|
-
[:DEFERRABLE, :INITIALLY, :IMMEDIATE].freeze,
|
|
2273
|
-
[:DEFERRABLE, :INITIALLY, :DEFERRED].freeze,
|
|
2274
|
-
[:NOT, :DEFERRABLE].freeze,
|
|
2275
|
-
:DEFERRABLE,
|
|
2276
|
-
)
|
|
2304
|
+
foreign_key_actions = refargs.grep(ForeignKeyAction)
|
|
2305
|
+
match_clauses = refargs.grep(MatchClause)
|
|
2306
|
+
deferrable_span = defer_subclause
|
|
2277
2307
|
|
|
2278
2308
|
ForeignKeyClause.concrete(
|
|
2279
2309
|
full_source: @lexer.sql,
|
|
@@ -2288,6 +2318,28 @@ module Plume
|
|
|
2288
2318
|
)
|
|
2289
2319
|
end
|
|
2290
2320
|
|
|
2321
|
+
def match_clause
|
|
2322
|
+
match_kw = require :MATCH
|
|
2323
|
+
type_tk = name(except: [:ON, :MATCH, :DEFERRABLE, :NOT])
|
|
2324
|
+
MatchClause.concrete(
|
|
2325
|
+
full_source: @lexer.sql,
|
|
2326
|
+
match_kw: Token::Keyword(match_kw),
|
|
2327
|
+
type_tk: Token::Identifier(type_tk),
|
|
2328
|
+
)
|
|
2329
|
+
end
|
|
2330
|
+
|
|
2331
|
+
# defer_subclause ::= NOT DEFERRABLE init_deferred_pred_opt | DEFERRABLE init_deferred_pred_opt
|
|
2332
|
+
def defer_subclause
|
|
2333
|
+
maybe_one_of(
|
|
2334
|
+
[:NOT, :DEFERRABLE, :INITIALLY, :IMMEDIATE].freeze,
|
|
2335
|
+
[:NOT, :DEFERRABLE, :INITIALLY, :DEFERRED].freeze,
|
|
2336
|
+
[:DEFERRABLE, :INITIALLY, :IMMEDIATE].freeze,
|
|
2337
|
+
[:DEFERRABLE, :INITIALLY, :DEFERRED].freeze,
|
|
2338
|
+
[:NOT, :DEFERRABLE].freeze,
|
|
2339
|
+
:DEFERRABLE,
|
|
2340
|
+
)
|
|
2341
|
+
end
|
|
2342
|
+
|
|
2291
2343
|
def conflict_clause
|
|
2292
2344
|
#
|
|
2293
2345
|
# Used by:
|
|
@@ -2403,7 +2455,7 @@ module Plume
|
|
|
2403
2455
|
expression_lp = require :LP
|
|
2404
2456
|
e = expression
|
|
2405
2457
|
as_kw = require :AS
|
|
2406
|
-
t = type_name
|
|
2458
|
+
t = type_name unless current_token == :RP # typetoken ::= .
|
|
2407
2459
|
expression_rp = require :RP
|
|
2408
2460
|
|
|
2409
2461
|
CastExpression.concrete(
|
|
@@ -2491,7 +2543,10 @@ module Plume
|
|
|
2491
2543
|
select_rp: Token::Punctuation(select_rp),
|
|
2492
2544
|
)
|
|
2493
2545
|
# A string followed by a dot is a quoted table name, as in `'t'.col`
|
|
2494
|
-
|
|
2546
|
+
# A string or TRUE/FALSE before a dot names a table: `'t'.x`, `false.false`. SQLite has no TRUE
|
|
2547
|
+
# or FALSE keyword at all, while CURRENT_* is always a literal in an expression
|
|
2548
|
+
elsif !((current_token in :STRING | :TRUE | :FALSE) && peek == :DOT) &&
|
|
2549
|
+
(literal_tk = maybe { literal_value })
|
|
2495
2550
|
LiteralExpression.concrete(
|
|
2496
2551
|
full_source: @lexer.sql,
|
|
2497
2552
|
value_tk: literal_tk,
|
|
@@ -2561,7 +2616,12 @@ module Plume
|
|
|
2561
2616
|
raise_function
|
|
2562
2617
|
# SQLite lexes "#9" as a variable too, but "#" and a digit only ever names a register
|
|
2563
2618
|
# inside its own nested parses, so as a parameter it is a syntax error
|
|
2564
|
-
elsif :VARIABLE == current_token && !current_value.match?(/\A#\d/)
|
|
2619
|
+
elsif :VARIABLE == current_token && !current_value.b.match?(/\A#\d/)
|
|
2620
|
+
# SQLite numbers them from ?1 up to its default SQLITE_MAX_VARIABLE_NUMBER
|
|
2621
|
+
if (number = current_value.b[/\A\?(\d+)\z/, 1]) && !number.to_i.between?(1, 32_766)
|
|
2622
|
+
expected!("?1 to ?32766")
|
|
2623
|
+
end
|
|
2624
|
+
|
|
2565
2625
|
Variable.concrete(
|
|
2566
2626
|
full_source: @lexer.sql,
|
|
2567
2627
|
name_tk: Token::Variable(require(:VARIABLE)),
|
|
@@ -2681,38 +2741,22 @@ module Plume
|
|
|
2681
2741
|
#
|
|
2682
2742
|
# [description]
|
|
2683
2743
|
#
|
|
2684
|
-
|
|
2685
|
-
expressions = require_some(trailing_sep: :COMMA) { expression }
|
|
2686
|
-
if (order_by_kw = maybe_all_of :ORDER, :BY)
|
|
2687
|
-
order_by = require_some(trailing_sep: :COMMA) { ordering_term }
|
|
2688
|
-
end
|
|
2689
|
-
|
|
2690
|
-
FunctionArguments.concrete(
|
|
2691
|
-
full_source: @lexer.sql,
|
|
2692
|
-
distinct_kw: Token::Keyword(distinct_kw),
|
|
2693
|
-
expressions: expressions,
|
|
2694
|
-
order_by_kw: Token::Keyword(order_by_kw),
|
|
2695
|
-
order_by: order_by,
|
|
2696
|
-
)
|
|
2697
|
-
elsif maybe :STAR
|
|
2698
|
-
StarFunctionArgument.new
|
|
2699
|
-
elsif (e = maybe { expression })
|
|
2700
|
-
expressions = require_some(given: e, trailing_sep: :COMMA) { expression }
|
|
2701
|
-
if (order_by_kw = maybe_all_of :ORDER, :BY)
|
|
2702
|
-
order_by = require_some(trailing_sep: :COMMA) { ordering_term }
|
|
2703
|
-
end
|
|
2744
|
+
return StarFunctionArgument.new if maybe :STAR
|
|
2704
2745
|
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
order_by: order_by,
|
|
2710
|
-
)
|
|
2711
|
-
elsif :RP == current_token
|
|
2712
|
-
EmptyFunctionArgument.new
|
|
2713
|
-
else
|
|
2714
|
-
expected!(:DISTINCT, :STAR, :RP, "expr")
|
|
2746
|
+
distinct_kw = maybe_one_of :DISTINCT, :ALL
|
|
2747
|
+
expressions = maybe_some(trailing_sep: :COMMA) { expression }
|
|
2748
|
+
if (order_by_kw = maybe_all_of :ORDER, :BY)
|
|
2749
|
+
order_by = require_some(trailing_sep: :COMMA) { ordering_term }
|
|
2715
2750
|
end
|
|
2751
|
+
return EmptyFunctionArgument.new unless distinct_kw || expressions.any? || order_by_kw
|
|
2752
|
+
|
|
2753
|
+
FunctionArguments.concrete(
|
|
2754
|
+
full_source: @lexer.sql,
|
|
2755
|
+
distinct_kw: Token::Keyword(distinct_kw),
|
|
2756
|
+
expressions: expressions,
|
|
2757
|
+
order_by_kw: Token::Keyword(order_by_kw),
|
|
2758
|
+
order_by: order_by,
|
|
2759
|
+
)
|
|
2716
2760
|
end
|
|
2717
2761
|
|
|
2718
2762
|
def frame_spec
|
|
@@ -2745,7 +2789,7 @@ module Plume
|
|
|
2745
2789
|
if (between_kw = maybe :BETWEEN)
|
|
2746
2790
|
if (unbounded_kw = maybe :UNBOUNDED)
|
|
2747
2791
|
precedence = 1
|
|
2748
|
-
preceding_kw =
|
|
2792
|
+
preceding_kw = require :PRECEDING
|
|
2749
2793
|
starting_boundary = FrameBoundary.concrete(
|
|
2750
2794
|
full_source: @lexer.sql,
|
|
2751
2795
|
type_tk: Token::Keyword(preceding_kw),
|
|
@@ -2786,7 +2830,7 @@ module Plume
|
|
|
2786
2830
|
type_tk: Token::Keyword(current_row_kw),
|
|
2787
2831
|
)
|
|
2788
2832
|
elsif (unbounded_kw = maybe :UNBOUNDED)
|
|
2789
|
-
following_kw =
|
|
2833
|
+
following_kw = require :FOLLOWING
|
|
2790
2834
|
ending_boundary = FrameBoundary.concrete(
|
|
2791
2835
|
full_source: @lexer.sql,
|
|
2792
2836
|
type_tk: Token::Keyword(following_kw),
|
|
@@ -2814,7 +2858,7 @@ module Plume
|
|
|
2814
2858
|
expected!("CURRENT ROW", "UNBOUNDED FOLLOWING", "expr")
|
|
2815
2859
|
end
|
|
2816
2860
|
elsif (unbounded_kw = maybe :UNBOUNDED)
|
|
2817
|
-
preceding_kw =
|
|
2861
|
+
preceding_kw = require :PRECEDING
|
|
2818
2862
|
starting_boundary = FrameBoundary.concrete(
|
|
2819
2863
|
full_source: @lexer.sql,
|
|
2820
2864
|
type_tk: Token::Keyword(preceding_kw),
|
|
@@ -3474,7 +3518,9 @@ module Plume
|
|
|
3474
3518
|
end
|
|
3475
3519
|
end
|
|
3476
3520
|
|
|
3477
|
-
|
|
3521
|
+
# `nulls`: whether NULLS FIRST/LAST may follow, as in the sortlist of a PRIMARY KEY or UNIQUE -
|
|
3522
|
+
# not in the eidlist of a foreign key, a view or a CTE
|
|
3523
|
+
def indexed_column(nulls: false)
|
|
3478
3524
|
# Used by:
|
|
3479
3525
|
# - ccons, tcons, creat-view, cte
|
|
3480
3526
|
#
|
|
@@ -3483,6 +3529,7 @@ module Plume
|
|
|
3483
3529
|
# collate ::= . | COLLATE ids.
|
|
3484
3530
|
# sortorder ::= . | ASC | DESC
|
|
3485
3531
|
# nm ::= idj | STRING
|
|
3532
|
+
# (PRIMARY KEY and UNIQUE take a sortlist, whose items may also end NULLS FIRST or NULLS LAST)
|
|
3486
3533
|
#
|
|
3487
3534
|
# Syntax diagram:
|
|
3488
3535
|
# ◯──▶{ column-name }─┬─▶───────────────────────────────┬┬────────▶───┬──▶◯
|
|
@@ -3490,7 +3537,7 @@ module Plume
|
|
|
3490
3537
|
# └─▶{ DESC }──┘
|
|
3491
3538
|
#
|
|
3492
3539
|
# Simplified grammar:
|
|
3493
|
-
# name [COLLATE identifier] [ASC | DESC]
|
|
3540
|
+
# name [COLLATE identifier] [ASC | DESC] [NULLS FIRST | NULLS LAST]
|
|
3494
3541
|
#
|
|
3495
3542
|
# Relevant SQLite documentation:
|
|
3496
3543
|
# - https://www.sqlite.org/syntax/indexed-column.html
|
|
@@ -3512,49 +3559,15 @@ module Plume
|
|
|
3512
3559
|
collation_tk = identifier_or_string(except: [:ASC, :DESC].freeze)
|
|
3513
3560
|
end
|
|
3514
3561
|
direction_tk = maybe_one_of :ASC, :DESC
|
|
3562
|
+
nulls_tk = maybe_one_of [:NULLS, :FIRST].freeze, [:NULLS, :LAST].freeze if nulls
|
|
3515
3563
|
|
|
3516
3564
|
IndexedColumn.concrete(
|
|
3517
3565
|
full_source: @lexer.sql,
|
|
3518
3566
|
name_tk: Token::Identifier(column_name),
|
|
3519
3567
|
collate_kw: Token::Keyword(collate_kw),
|
|
3520
3568
|
collation_tk: Token::Keyword(collation_tk),
|
|
3521
|
-
direction_tk: Token::Keyword(direction_tk)
|
|
3522
|
-
)
|
|
3523
|
-
end
|
|
3524
|
-
|
|
3525
|
-
def indexed_expression
|
|
3526
|
-
# Used by:
|
|
3527
|
-
# - tcons, upsert, expr (ORDER BY), create-index, window
|
|
3528
|
-
#
|
|
3529
|
-
# Relevant `parse.y` grammar rules:
|
|
3530
|
-
# sortlist ::= sortlist COMMA expr sortorder nulls | expr sortorder nulls
|
|
3531
|
-
# sortorder ::= . | ASC | DESC
|
|
3532
|
-
# nulls ::= . | NULLS FIRST | NULLS LAST
|
|
3533
|
-
#
|
|
3534
|
-
# Syntax diagram:
|
|
3535
|
-
# ◯─▶[ expr ]─┬────────▶───┬─┬────────────▶────────────┬───▶◯
|
|
3536
|
-
# ├─▶{ ASC }─▶─┤ ├─▶{ NULLS }─▶{ FIRST }─▶─┤
|
|
3537
|
-
# └─▶{ DESC }──┘ └─▶{ NULLS }─▶{ LAST }────┘
|
|
3538
|
-
#
|
|
3539
|
-
# Simplified grammar:
|
|
3540
|
-
# expression [ASC | DESC] [NULLS FIRST | NULLS LAST]
|
|
3541
|
-
#
|
|
3542
|
-
# Relevant SQLite documentation:
|
|
3543
|
-
# - https://www.sqlite.org/syntax/indexed-column.html
|
|
3544
|
-
# - https://www.sqlite.org/expridx.html
|
|
3545
|
-
#
|
|
3546
|
-
# the sortlist non-terminal stores a list of expression where each
|
|
3547
|
-
# expression is optionally followed by ASC or DESC to indicate the
|
|
3548
|
-
# sort order.
|
|
3549
|
-
#
|
|
3550
|
-
expr = expression
|
|
3551
|
-
direction_tk = maybe_one_of :ASC, :DESC
|
|
3552
|
-
nulls_tk = maybe_one_of [:NULLS, :FIRST].freeze, [:NULLS, :LAST].freeze
|
|
3553
|
-
|
|
3554
|
-
IndexedExpression.new(
|
|
3555
|
-
expression: expr,
|
|
3556
3569
|
direction_tk: Token::Keyword(direction_tk),
|
|
3557
|
-
nulls_tk:
|
|
3570
|
+
nulls_tk: Token::Keyword(nulls_tk),
|
|
3558
3571
|
)
|
|
3559
3572
|
end
|
|
3560
3573
|
|
|
@@ -3590,7 +3603,7 @@ module Plume
|
|
|
3590
3603
|
direction = require current_token
|
|
3591
3604
|
end
|
|
3592
3605
|
if (nulls_kw = maybe(:NULLS))
|
|
3593
|
-
nulls =
|
|
3606
|
+
nulls = require_one_of(:FIRST, :LAST)
|
|
3594
3607
|
end
|
|
3595
3608
|
|
|
3596
3609
|
OrderingTerm.concrete(
|
|
@@ -3716,6 +3729,8 @@ module Plume
|
|
|
3716
3729
|
start_buffer = @peek_buffer.dup
|
|
3717
3730
|
start_lexer_cursor = @lexer.cursor
|
|
3718
3731
|
start_lexer_pos = @lexer.anchor
|
|
3732
|
+
start_last_consumed = @last_consumed_token
|
|
3733
|
+
start_last_consumed_fin = @last_consumed_fin
|
|
3719
3734
|
|
|
3720
3735
|
result = catch(:ERROR) { yield }
|
|
3721
3736
|
|
|
@@ -3723,6 +3738,8 @@ module Plume
|
|
|
3723
3738
|
@peek_buffer = start_buffer
|
|
3724
3739
|
@lexer.cursor = start_lexer_cursor
|
|
3725
3740
|
@lexer.anchor = start_lexer_pos
|
|
3741
|
+
@last_consumed_token = start_last_consumed
|
|
3742
|
+
@last_consumed_fin = start_last_consumed_fin
|
|
3726
3743
|
nil
|
|
3727
3744
|
else
|
|
3728
3745
|
result
|
|
@@ -3826,7 +3843,7 @@ module Plume
|
|
|
3826
3843
|
end
|
|
3827
3844
|
a << insert
|
|
3828
3845
|
|
|
3829
|
-
collect(into: a, trailing_sep: trailing_sep, leading_sep: leading_sep, &)
|
|
3846
|
+
collect(into: a, trailing_sep: trailing_sep, leading_sep: leading_sep, last_sep: sep_tk, &)
|
|
3830
3847
|
|
|
3831
3848
|
return token_span_from(*a) if a.all?(Array)
|
|
3832
3849
|
|
|
@@ -3865,7 +3882,7 @@ module Plume
|
|
|
3865
3882
|
if insert
|
|
3866
3883
|
a << insert
|
|
3867
3884
|
|
|
3868
|
-
collect(into: a, trailing_sep: trailing_sep, leading_sep: leading_sep, &)
|
|
3885
|
+
collect(into: a, trailing_sep: trailing_sep, leading_sep: leading_sep, last_sep: sep_tk, &)
|
|
3869
3886
|
|
|
3870
3887
|
return token_span_from(*a) if a.all?(Array)
|
|
3871
3888
|
|
|
@@ -3875,10 +3892,11 @@ module Plume
|
|
|
3875
3892
|
end
|
|
3876
3893
|
end
|
|
3877
3894
|
|
|
3878
|
-
|
|
3879
|
-
|
|
3895
|
+
# `last_sep` is what followed the last item in `into`: nil when no separator did, so the list has
|
|
3896
|
+
# ended; a separator token, so another item must follow; false when an optional one was left out
|
|
3897
|
+
def collect(into: [], trailing_sep: nil, leading_sep: nil, last_sep: false, &block)
|
|
3880
3898
|
while true
|
|
3881
|
-
break if
|
|
3899
|
+
break if trailing_sep && last_sep.nil?
|
|
3882
3900
|
|
|
3883
3901
|
if leading_sep
|
|
3884
3902
|
sep_tk = (Array === leading_sep) ? maybe_one_of(*leading_sep) : maybe(leading_sep)
|
|
@@ -3894,11 +3912,10 @@ module Plume
|
|
|
3894
3912
|
end
|
|
3895
3913
|
|
|
3896
3914
|
elsif trailing_sep
|
|
3897
|
-
val = maybe { yield }
|
|
3915
|
+
val = last_sep ? yield : maybe { yield }
|
|
3898
3916
|
break if val.nil?
|
|
3899
3917
|
|
|
3900
|
-
sep_tk = (Array === trailing_sep) ? maybe_one_of(*trailing_sep) : maybe(trailing_sep)
|
|
3901
|
-
break_next = sep_tk.nil?
|
|
3918
|
+
last_sep = sep_tk = (Array === trailing_sep) ? maybe_one_of(*trailing_sep) : maybe(trailing_sep)
|
|
3902
3919
|
|
|
3903
3920
|
insert = if Node === val
|
|
3904
3921
|
val.trailing = Token::Punctuation(sep_tk) if sep_tk
|
|
@@ -3993,6 +4010,7 @@ module Plume
|
|
|
3993
4010
|
ensure_buffer
|
|
3994
4011
|
token = @peek_buffer.shift
|
|
3995
4012
|
@last_consumed_fin = token[2]
|
|
4013
|
+
@last_consumed_token = token[0]
|
|
3996
4014
|
token
|
|
3997
4015
|
end
|
|
3998
4016
|
|
|
@@ -4002,11 +4020,42 @@ module Plume
|
|
|
4002
4020
|
lexer = @lexer
|
|
4003
4021
|
@peek_buffer.tap do |buf|
|
|
4004
4022
|
while buf.size < size
|
|
4005
|
-
|
|
4023
|
+
token = lexer.next_token
|
|
4024
|
+
if token == :WINDOW || token == :OVER || token == :FILTER
|
|
4025
|
+
token = contextual_keyword(token, buf.empty? ? @last_consumed_token : buf.last[0])
|
|
4026
|
+
end
|
|
4027
|
+
buf << [token, lexer.anchor, lexer.cursor].freeze
|
|
4006
4028
|
end
|
|
4007
4029
|
end
|
|
4008
4030
|
end
|
|
4009
4031
|
|
|
4032
|
+
# Like SQLite's tokenize.c, decide from the tokens around them whether WINDOW, OVER and FILTER are
|
|
4033
|
+
# keywords here or names: WINDOW only before `name AS`, OVER only after `)` and before `(` or a
|
|
4034
|
+
# name, FILTER only after `)` and before `(`
|
|
4035
|
+
def contextual_keyword(token, previous)
|
|
4036
|
+
following, after = lex_ahead
|
|
4037
|
+
keyword =
|
|
4038
|
+
case token
|
|
4039
|
+
when :WINDOW then name_token?(following) && after == :AS
|
|
4040
|
+
when :OVER then previous == :RP && (following == :LP || name_token?(following))
|
|
4041
|
+
when :FILTER then previous == :RP && following == :LP
|
|
4042
|
+
end
|
|
4043
|
+
keyword ? token : :ID
|
|
4044
|
+
end
|
|
4045
|
+
|
|
4046
|
+
# The types of the two tokens after the one just lexed, leaving the lexer where it was
|
|
4047
|
+
def lex_ahead
|
|
4048
|
+
cursor, anchor = @lexer.cursor, @lexer.anchor
|
|
4049
|
+
[@lexer.next_token, @lexer.next_token]
|
|
4050
|
+
ensure
|
|
4051
|
+
@lexer.cursor, @lexer.anchor = cursor, anchor
|
|
4052
|
+
end
|
|
4053
|
+
|
|
4054
|
+
# What SQLite's tokenize.c counts as a name when looking around WINDOW and OVER
|
|
4055
|
+
def name_token?(token)
|
|
4056
|
+
(token in :ID | :STRING | :WINDOW | :OVER) || JOIN_KEYWORDS.include?(token) || TOKEN_FALLBACKS.include?(token)
|
|
4057
|
+
end
|
|
4058
|
+
|
|
4010
4059
|
def expected!(*expected)
|
|
4011
4060
|
token, beg, fin = ensure_buffer[0]
|
|
4012
4061
|
throw :ERROR, ErrorMessage.new(@lexer.sql, token, beg, fin, expected)
|