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.
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
- line = @sql.byteslice(0, @beg).count("\n") + 1
15
- where = @sql.strip.include?("\n") ? " line #{line}" : ""
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 SyntaxError.new(result.msg)
164
+ raise result.to_error
151
165
  else
152
166
  result
153
167
  end
154
- rescue SystemStackError
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: @lexer.sql,
684
- alter_table_kw: Token::Keyword(alter_table_kw),
685
- table: table,
686
- action: action,
687
- action_kw: Token::Keyword(action_kw),
688
- column_tk: Token::Identifier(column_tk),
689
- to_kw: Token::Keyword(to_kw),
690
- new_name_tk: Token::Identifier(new_name_tk),
691
- column_definition: 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
- columns = require_some(trailing_sep: :COMMA) { column_def }
878
- # inlined `table_constraint`
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 :UNIQUE)
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 (check_kw = maybe :CHECK)
931
- check_lp = require :LP
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
- operator_precedence = OPERATOR_PRECEDENCE[operator] || -1
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 (ref = maybe { identifier })
1501
- if maybe :DOT
1502
- name = identifier
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
- if (arguments_lp = maybe :LP)
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
- function_tk: Token::Identifier(ref),
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
- table_tk: Token::Identifier(table),
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, :ID)
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 { identifier(except: [:PARTITION, :ORDER, :RANGE, :ROWS, :GROUPS]) }
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 = identifier
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
- foreign_key_actions = maybe_some(trailing_sep: nil) do
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
- match_clauses = maybe_some(trailing_sep: nil) do
2260
- match_kw = require :MATCH
2261
- type_tk = name(except: [:ON, :MATCH, :DEFERRABLE, :NOT])
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
- elsif !(current_token == :STRING && peek == :DOT) && (literal_tk = maybe { literal_value })
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
- if (distinct_kw = maybe_one_of :DISTINCT, :ALL)
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
- FunctionArguments.concrete(
2706
- full_source: @lexer.sql,
2707
- expressions: expressions,
2708
- order_by_kw: Token::Keyword(order_by_kw),
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 = maybe :PRECEDING
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 = maybe :FOLLOWING
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 = maybe :PRECEDING
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
- def indexed_column
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: Token::Keyword(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 = require current_token
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
- def collect(into: [], trailing_sep: nil, leading_sep: nil, &block)
3879
- break_next = false
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 break_next
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
- buf << [lexer.next_token, lexer.anchor, lexer.cursor].freeze
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)