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.
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # The printer laid out over lines: each clause starts a line, a clause too long for
5
+ # `max_line_length` puts its items one per line, indented by `indent_size`, and a long AND or OR
6
+ # chain in a condition puts each operand on a line of its own. Subqueries are indented inside
7
+ # their parentheses. Expressions otherwise print as `Printer` prints them, so what comes out
8
+ # parses into the same tree
9
+ class PrettyPrinter < Printer
10
+ def self.print(node, indent_size: 4, max_line_length: 80, commas_start_of_line: false)
11
+ new(indent_size:, max_line_length:, commas_start_of_line:).print(node)
12
+ rescue *STACK_EXHAUSTED
13
+ raise Error, "statement nested too deeply to print"
14
+ end
15
+
16
+ def initialize(indent_size:, max_line_length:, commas_start_of_line:)
17
+ super()
18
+ @pad = " " * indent_size
19
+ @max = max_line_length
20
+ @leading_commas = commas_start_of_line
21
+ end
22
+
23
+ private
24
+
25
+ def lines(*parts) = parts.flatten.reject { |p| !p || p.empty? }.join("\n")
26
+
27
+ # Only the line breaks between tokens: one inside a string or quoted name is part of its value
28
+ def indent(text)
29
+ src = text.b
30
+ breaks = []
31
+ lexer = Lexer.new(text)
32
+ while (token = lexer.next_token)
33
+ next unless token == :SPACE
34
+
35
+ ((lexer.anchor)...(lexer.cursor)).each { |i| breaks << i if src.getbyte(i) == 10 && i + 1 < src.bytesize && src.getbyte(i + 1) != 10 }
36
+ end
37
+
38
+ out = (src.empty? ? +"" : @pad.b.dup)
39
+ at = 0
40
+ breaks.each do |i|
41
+ out << src.byteslice(at, i + 1 - at) << @pad
42
+ at = i + 1
43
+ end
44
+ (out << src.byteslice(at, src.bytesize - at)).force_encoding(text.encoding)
45
+ end
46
+
47
+ # Lines after the first of a multi-line operand line up under the clause it sits in
48
+ def hanging(text) = text.include?("\n") ? indent(text).delete_prefix(@pad) : text
49
+
50
+ def fits?(text) = !text.include?("\n") && text.length <= @max
51
+
52
+ # `KEYWORD a, b, c` on one line if it fits, otherwise the keyword alone and an item per line
53
+ def clause(keyword, items)
54
+ items = Array(items).map { |item| (String === item) ? item : print(item) }
55
+ one_line = "#{keyword} #{items.join(", ")}"
56
+ return one_line if fits?(one_line)
57
+
58
+ body =
59
+ if @leading_commas
60
+ items.each_with_index.map { |item, i| i.zero? ? " #{item}" : ", #{item}" }.join("\n")
61
+ else
62
+ items.join(",\n")
63
+ end
64
+ "#{keyword}\n#{indent(body)}"
65
+ end
66
+
67
+ # A condition on one line if it fits, otherwise its top-level AND or OR chain an operand per line
68
+ def condition(keyword, node)
69
+ one_line = "#{keyword} #{print(node)}"
70
+ return one_line if fits?(one_line) || !(BinaryExpression === node && (node.operator in :ALL | :ANY))
71
+
72
+ chain = [node]
73
+ chain << chain.last.left while BinaryExpression === chain.last.left && chain.last.left.operator == node.operator
74
+ word = BINARY_OPERATORS.fetch(node.operator)
75
+ rest = chain.reverse_each.map { |link| "#{word} #{operand(link.right)}" }
76
+ lines("#{keyword} #{hanging(operand(chain.last.left))}", rest.map { |line| indent(line) })
77
+ end
78
+
79
+ def parens(inner) = inner.include?("\n") ? "(\n#{indent(inner)}\n)" : "(#{inner})"
80
+
81
+ def pretty_order_and_limit(node)
82
+ limit =
83
+ if node.offset && node.offset_tk_tok == :COMMA
84
+ "LIMIT #{print(node.offset)}, #{print(node.limit)}"
85
+ else
86
+ join(node.limit && "LIMIT #{print(node.limit)}", node.offset && "OFFSET #{print(node.offset)}")
87
+ end
88
+ [node.orderings && clause("ORDER BY", node.orderings), limit]
89
+ end
90
+
91
+ # -- statements
92
+
93
+ def print_select_statement(node) = lines(print(node.with_clause), print(node.body), pretty_order_and_limit(node))
94
+
95
+ def print_with_clause(node)
96
+ ctes = node.ctes.map { |cte| print(cte) }
97
+ head = join("WITH", node.recursive && "RECURSIVE")
98
+ "#{head} #{ctes.join(",\n")}"
99
+ end
100
+
101
+ def print_common_table_expression(node)
102
+ materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
103
+ name = identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : "")
104
+ join(name, "AS", materialized, parens(print(node.select)))
105
+ end
106
+
107
+ def print_compound_select(node) = lines(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
108
+
109
+ def print_select_core(node)
110
+ lines(
111
+ clause(node.distinct ? "SELECT DISTINCT" : "SELECT", node.columns),
112
+ node.source && from(node.source),
113
+ node.condition && condition("WHERE", node.condition),
114
+ node.groupings && clause("GROUP BY", node.groupings),
115
+ node.having && condition("HAVING", node.having),
116
+ node.windows && clause("WINDOW", node.windows),
117
+ )
118
+ end
119
+
120
+ def from(source) = (JoinClause === source) ? "FROM #{print(source)}" : clause("FROM", [source])
121
+
122
+ def print_values_clause(node) = clause("VALUES", node.rows)
123
+
124
+ def print_join_clause(node)
125
+ node.joins.reduce(print(node.left)) do |sql, each|
126
+ operator = print(each.operator)
127
+ right = join(print(each.source), print(each.constraint))
128
+ (operator == ",") ? "#{sql},\n#{right}" : "#{sql}\n#{operator} #{right}"
129
+ end
130
+ end
131
+
132
+ def print_subquery_source(node) = join(parens(print(node.select)), alias_of(node))
133
+ def print_subquery_expression(node) = parens(print(node.select))
134
+ def print_exists_expression(node) = "EXISTS #{parens(print(node.select))}"
135
+
136
+ def print_insert_statement(node)
137
+ verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
138
+ lines(
139
+ print(node.with_clause),
140
+ join(verb, "INTO", print(node.table), node.columns && "(#{list(node.columns)})"),
141
+ node.default_values ? "DEFAULT VALUES" : print(node.select),
142
+ node.upserts&.map { |u| print(u) },
143
+ print(node.returning),
144
+ )
145
+ end
146
+
147
+ def print_upsert_clause(node)
148
+ target = node.targets && "(#{list(node.targets)})"
149
+ head = join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}")
150
+ return join(head, "DO NOTHING") if node.action == :NOTHING
151
+
152
+ lines(head, indent(lines(clause("DO UPDATE SET", node.assignments), node.condition && condition("WHERE", node.condition))))
153
+ end
154
+
155
+ def print_update_statement(node)
156
+ lines(
157
+ print(node.with_clause),
158
+ join(node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table)),
159
+ clause("SET", node.assignments),
160
+ node.source && from(node.source),
161
+ node.condition && condition("WHERE", node.condition),
162
+ print(node.returning), pretty_order_and_limit(node),
163
+ )
164
+ end
165
+
166
+ def print_delete_statement(node)
167
+ lines(
168
+ print(node.with_clause), "DELETE FROM #{print(node.table)}",
169
+ node.condition && condition("WHERE", node.condition),
170
+ print(node.returning), pretty_order_and_limit(node),
171
+ )
172
+ end
173
+
174
+ def print_returning_clause(node) = clause("RETURNING", node.columns)
175
+
176
+ def print_create_table_statement(node)
177
+ head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
178
+ return lines("#{head} AS", print(node.select_statement)) if node.select_statement
179
+
180
+ definitions = [*node.columns, *node.constraints].map { |d| print(d) }
181
+ join("#{head} (\n#{indent(definitions.join(",\n"))}\n)", node.options && list(node.options))
182
+ end
183
+
184
+ def print_create_view_statement(node)
185
+ head = join(
186
+ "CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
187
+ node.columns && "(#{list(node.columns)})", "AS",
188
+ )
189
+ lines(head, print(node.select))
190
+ end
191
+
192
+ def print_create_trigger_statement(node)
193
+ event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
194
+ head = join(
195
+ "CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
196
+ node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
197
+ node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
198
+ )
199
+ lines(head, "BEGIN", node.statements.map { |s| indent("#{print(s)};") }, "END")
200
+ end
201
+ end
202
+ end
data/lib/plume/printer.rb CHANGED
@@ -4,8 +4,9 @@ module Plume
4
4
  # Turns an AST back into SQL. It works from the nodes' readers rather than from the tokens
5
5
  # they were parsed from, so trees built by hand, or changed after parsing, print too. The
6
6
  # output is canonical rather than a copy of the input: keywords come out in upper case,
7
- # identifiers are quoted only when they have to be, and every compound operand gets
8
- # parentheses so precedence never has to be second-guessed.
7
+ # identifiers are quoted only when they have to be, and a compound operand gets parentheses
8
+ # unless it is the left operand of an operator at its own precedence, where SQLite's
9
+ # left-associativity already groups it.
9
10
  class Printer
10
11
  BARE_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
11
12
 
@@ -18,6 +19,21 @@ module Plume
18
19
  NOT_REGEXP: "NOT REGEXP", MATCH: "MATCH", NOT_MATCH: "NOT MATCH", IN: "IN", NOT_IN: "NOT IN",
19
20
  }.freeze
20
21
 
22
+ # Names that would read back as the start of an expression
23
+ EXPRESSION_KEYWORDS = (Parser::LITERAL_KEYWORDS + [:CAST, :RAISE]).freeze
24
+
25
+ # Levels Plume's parser and SQLite agree on: SQLite also puts = and IS with IN, LIKE and the rest
26
+ PRECEDENCE = {
27
+ ANY: 1, ALL: 2,
28
+ IN: 3, NOT_IN: 3, MATCH: 3, NOT_MATCH: 3, GLOB: 3, NOT_GLOB: 3, REGEXP: 3, NOT_REGEXP: 3,
29
+ EQUALS: 4, NOT_EQUALS: 4, IS: 4, IS_NOT: 4,
30
+ BELOW: 5, ABOVE: 5, ATMOST: 5, ATLEAST: 5,
31
+ BIT_AND: 6, BIT_OR: 6, BIT_LSHIFT: 6, BIT_RSHIFT: 6,
32
+ ADD: 7, SUB: 7,
33
+ MULTIPLY: 8, DIVIDE: 8, MODULO: 8,
34
+ CONCAT: 9, EXTRACT: 9, RETRIEVE: 9,
35
+ }.freeze
36
+
21
37
  COMPOUND_OPERATORS = { UNION: "UNION", UNION_ALL: "UNION ALL", INTERSECT: "INTERSECT", EXCEPT: "EXCEPT" }.freeze
22
38
  JOIN_KINDS = { COMMA: ",", INNER: "JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN", FULL: "FULL JOIN", CROSS: "CROSS JOIN" }.freeze
23
39
  FOREIGN_KEY_ACTIONS = { CASCADE: "CASCADE", RESTRICT: "RESTRICT", SET_NULL: "SET NULL", SET_DEFAULT: "SET DEFAULT", NO_ACTION: "NO ACTION" }.freeze
@@ -25,7 +41,14 @@ module Plume
25
41
  # Operands of these need parentheses to keep their grouping when printed inside another expression
26
42
  COMPOUND_EXPRESSIONS = [BinaryExpression, UnaryExpression, TernaryExpression, LikeExpression, CollationExpression].freeze
27
43
 
28
- def self.print(node) = new.print(node)
44
+ Error = Class.new(StandardError)
45
+
46
+ def self.print(node)
47
+ new.print(node)
48
+ rescue *STACK_EXHAUSTED
49
+ # As with parsing, how deep is too deep depends on the stack the caller has left
50
+ raise Error, "statement nested too deeply to print"
51
+ end
29
52
 
30
53
  def print(node)
31
54
  case node
@@ -40,18 +63,22 @@ module Plume
40
63
 
41
64
  def identifier(name)
42
65
  name = name.to_s
43
- return name if name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
66
+ return name if name.ascii_only? && name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
44
67
 
45
- %("#{name.gsub('"', '""')}")
68
+ %("#{doubled(name, '"')}")
46
69
  end
47
70
 
48
- # Keywords SQLite falls back to treating as identifiers, like ROWID or KEY, need no quotes
49
- def bare_keyword_ok?(word) = !Lexer::KEYWORDS.include?(word) || Parser::TOKEN_FALLBACKS.include?(word)
71
+ # Keywords SQLite falls back to treating as identifiers, like ROWID or KEY, need no quotes -
72
+ # except the ones that would read back as the start of an expression
73
+ def bare_keyword_ok?(word) = !Lexer::KEYWORDS.include?(word) || (Parser::TOKEN_FALLBACKS.include?(word) && !EXPRESSION_KEYWORDS.include?(word))
50
74
 
51
- def string(value) = "'#{value.to_s.gsub("'", "''")}'"
75
+ def string(value) = "'#{doubled(value.to_s, "'")}'"
52
76
 
53
77
  private
54
78
 
79
+ # Done on the bytes, since TruffleRuby's gsub refuses a string that is not valid UTF-8
80
+ def doubled(text, quote) = text.b.gsub(quote, quote * 2).force_encoding(text.encoding)
81
+
55
82
  def underscore(name) = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
56
83
 
57
84
  def join(*parts) = parts.flatten.reject { |p| !p || (p.respond_to?(:empty?) && p.empty?) }.join(" ")
@@ -77,8 +104,12 @@ module Plume
77
104
  def order_and_limit(node)
78
105
  [
79
106
  node.orderings && "ORDER BY #{list(node.orderings)}",
80
- node.limit && "LIMIT #{print(node.limit)}",
81
- node.offset && "OFFSET #{print(node.offset)}",
107
+ # `LIMIT offset, limit` keeps its order: swapping the two would renumber their `?` parameters
108
+ if node.offset && node.offset_tk_tok == :COMMA
109
+ "LIMIT #{print(node.offset)}, #{print(node.limit)}"
110
+ else
111
+ [node.limit && "LIMIT #{print(node.limit)}", node.offset && "OFFSET #{print(node.offset)}"]
112
+ end,
82
113
  ]
83
114
  end
84
115
 
@@ -213,6 +244,10 @@ module Plume
213
244
  when :RENAME_COLUMN then "RENAME COLUMN #{identifier(node.column)} TO #{identifier(node.new_name)}"
214
245
  when :ADD_COLUMN then "ADD COLUMN #{print(node.column_definition)}"
215
246
  when :DROP_COLUMN then "DROP COLUMN #{identifier(node.column)}"
247
+ when :ADD_CONSTRAINT then "ADD #{print(node.constraint)}"
248
+ when :DROP_CONSTRAINT then "DROP CONSTRAINT #{identifier(node.constraint_name)}"
249
+ when :SET_NOT_NULL then join("ALTER COLUMN #{identifier(node.column)} SET NOT NULL", print(node.conflict_clause))
250
+ when :DROP_NOT_NULL then "ALTER COLUMN #{identifier(node.column)} DROP NOT NULL"
216
251
  end
217
252
  join("ALTER TABLE", print(node.table), action)
218
253
  end
@@ -298,8 +333,18 @@ module Plume
298
333
 
299
334
  def print_variable(node) = node.name
300
335
 
336
+ # Every binary operator associates to the left, so a left operand at the same precedence needs no
337
+ # parentheses: `a - b + c`, `a OR b OR c`. A long chain then prints flat rather than nesting a pair
338
+ # per term, and the left spine is walked in a loop, so its length costs no stack
301
339
  def print_binary_expression(node)
302
- "#{operand(node.left)} #{BINARY_OPERATORS.fetch(node.operator)} #{operand(node.right)}"
340
+ chain = [node]
341
+ while BinaryExpression === (left = chain.last.left) && PRECEDENCE[left.operator] == PRECEDENCE[node.operator]
342
+ chain << left
343
+ end
344
+
345
+ chain.reverse_each.reduce(+operand(chain.last.left)) do |sql, link|
346
+ sql << " " << BINARY_OPERATORS.fetch(link.operator) << " " << operand(link.right)
347
+ end
303
348
  end
304
349
 
305
350
  def print_unary_expression(node)
@@ -334,7 +379,7 @@ module Plume
334
379
 
335
380
  def print_parenthetical_expression(node) = "(#{print(node.contents)})"
336
381
 
337
- def print_cast_expression(node) = "CAST(#{print(node.expression)} AS #{print(node.as)})"
382
+ def print_cast_expression(node) = "CAST(#{join(print(node.expression), "AS", print(node.as))})"
338
383
 
339
384
  def print_case_expression(node)
340
385
  join("CASE", node.predicate && print(node.predicate), list_of(node.conditions), node.else_clause && "ELSE #{print(node.else_clause)}", "END")
@@ -354,7 +399,8 @@ module Plume
354
399
  end
355
400
 
356
401
  def print_function_arguments(node)
357
- join(node.distinct && "DISTINCT", list(node.expressions), node.order_by && "ORDER BY #{list(node.order_by)}")
402
+ quantifier = node.distinct ? "DISTINCT" : (node.distinct_kw_tok == :ALL && "ALL")
403
+ join(quantifier, list(node.expressions), node.order_by && "ORDER BY #{list(node.order_by)}")
358
404
  end
359
405
 
360
406
  def print_star_function_argument(_node) = "*"
@@ -442,19 +488,27 @@ module Plume
442
488
  end
443
489
 
444
490
  def print_unique_table_constraint(node) = join(constraint_name(node), "UNIQUE (#{list(node.columns)})", print(node.conflict_clause))
445
- def print_check_table_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})")
491
+ def print_check_table_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
446
492
  def print_no_op_table_constraint(node) = constraint_name(node)
447
493
 
494
+ def print_deferrable_column_constraint(node) = join(constraint_name(node), deferrable(node) || "DEFERRABLE")
495
+
496
+ # The words as written, or else what `deferred` says
497
+ def deferrable(node)
498
+ return Array(node.deferrable_span_tok).join(" ") if node.deferrable_span?
499
+
500
+ "DEFERRABLE INITIALLY DEFERRED" if node.deferred
501
+ end
502
+
448
503
  def print_foreign_key_table_constraint(node)
449
504
  join(constraint_name(node), "FOREIGN KEY (#{list(node.columns)})", print(node.foreign_key_clause))
450
505
  end
451
506
 
452
507
  def print_foreign_key_clause(node)
453
- deferrable = node.deferrable_span? ? node.deferrable_span_tok : (node.deferred && "DEFERRABLE INITIALLY DEFERRED")
454
508
  join(
455
509
  "REFERENCES", identifier(node.table), node.columns && "(#{list(node.columns)})",
456
510
  list_of(Array(node.actions)), list_of(Array(node.match_clauses)),
457
- deferrable && Array(deferrable).join(" "),
511
+ deferrable(node),
458
512
  )
459
513
  end
460
514
 
@@ -462,7 +516,7 @@ module Plume
462
516
  def print_match_clause(node) = "MATCH #{node.type}"
463
517
 
464
518
  def print_indexed_column(node)
465
- join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s)
519
+ join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s, node.nulls && "NULLS #{node.nulls.to_s.upcase}")
466
520
  end
467
521
 
468
522
  def print_strict_table_option(_node) = "STRICT"
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plume
4
+ # SQL shortened to fit a length, for logs and error trackers, by putting `...` where the parts
5
+ # least needed to recognise the query were: result columns, WHERE conditions, VALUES rows, CTE
6
+ # bodies, INSERT column lists and UPDATE assignments - the most deeply nested first, the longest
7
+ # first among those. Everything else stays as written. When that is not enough, the end is cut off
8
+ module Truncate
9
+ extend self
10
+
11
+ ELLIPSIS = "..."
12
+
13
+ def call(sql, max_length)
14
+ return sql if sql.length <= max_length
15
+
16
+ src = sql.b
17
+ chosen = []
18
+ result = sql
19
+ candidates(Parser.new(sql).parse).each do |beg, fin, _depth|
20
+ next if fin - beg <= ELLIPSIS.bytesize || chosen.any? { |b, f| b <= beg && fin <= f }
21
+
22
+ chosen.reject! { |b, f| beg <= b && f <= fin }
23
+ chosen << [beg, fin]
24
+ result = render(src, chosen, sql.encoding)
25
+ return result if result.length <= max_length
26
+ end
27
+ "#{result[0, [max_length - ELLIPSIS.length, 0].max]}#{ELLIPSIS}"[0, max_length]
28
+ end
29
+
30
+ # Byte ranges that may be cut, with how deeply they are nested, in the order to cut them
31
+ def candidates(statements)
32
+ found = []
33
+ stack = statements.reverse.map { |s| [s, 0] }
34
+ while (node, depth = stack.pop)
35
+ span =
36
+ case node
37
+ when SelectCore then [list_span(node.columns), node.condition&.location]
38
+ when ValuesClause then [list_span(node.rows)]
39
+ when CommonTableExpression then [node.select&.location]
40
+ when InsertStatement then [list_span(node.columns)]
41
+ when UpdateStatement then [list_span(node.assignments)]
42
+ else []
43
+ end
44
+ span.compact.each { |beg, fin| found << [beg, fin, depth] }
45
+ node.children.reverse_each { |child| stack << [child, depth + 1] }
46
+ end
47
+ found.sort_by { |beg, fin, depth| [-depth, beg - fin] }
48
+ end
49
+
50
+ private
51
+
52
+ def list_span(nodes)
53
+ first, last = nodes&.first&.location, nodes&.last&.location
54
+ [first[0], last[1]] if first && last
55
+ end
56
+
57
+ def render(src, spans, encoding)
58
+ out = +""
59
+ at = 0
60
+ spans.sort.each do |beg, fin|
61
+ out << src.byteslice(at, beg - at) << ELLIPSIS
62
+ at = fin
63
+ end
64
+ (out << src.byteslice(at, src.bytesize - at)).force_encoding(encoding)
65
+ end
66
+ end
67
+ end
data/lib/plume/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Plume
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/plume.rb CHANGED
@@ -3,10 +3,15 @@
3
3
  require "plume/version"
4
4
  require "literal"
5
5
  require "plume/union"
6
+ require "plume/literal_patches"
6
7
 
7
8
  module Plume
8
9
  extend Literal::Types
9
10
  extend Union::Types
11
+ extend LiteralPatches::Types if LiteralPatches::DEFERRED_WRITES_ON_CHECK
12
+
13
+ # JRuby and TruffleRuby have no Ractors, so there is nothing to make shareable for
14
+ def self.make_shareable(obj) = defined?(Ractor) ? Ractor.make_shareable(obj) : obj
10
15
 
11
16
  autoload :Lexer, "plume/lexer"
12
17
  autoload :Parser, "plume/parser"
@@ -14,7 +19,15 @@ module Plume
14
19
  autoload :SQLiteGrammar, "plume/sqlite_grammar"
15
20
  autoload :Catalog, "plume/catalog"
16
21
  autoload :Printer, "plume/printer"
22
+ autoload :PrettyPrinter, "plume/pretty_printer"
17
23
  autoload :Analysis, "plume/analysis"
24
+ autoload :Parameters, "plume/parameters"
25
+ autoload :Dump, "plume/dump"
26
+ autoload :Truncate, "plume/truncate"
27
+ autoload :Comment, "plume/comment"
28
+ autoload :Comments, "plume/comment"
29
+ autoload :Normalize, "plume/normalize"
30
+ autoload :Fingerprint, "plume/fingerprint"
18
31
 
19
32
  autoload :BinaryExpression, "plume/ast/binary_expression"
20
33
  autoload :ParentheticalExpression, "plume/ast/parenthetical_expression"
@@ -33,6 +46,7 @@ module Plume
33
46
  autoload :CreateTableStatement, "plume/ast/create_table_statement"
34
47
  autoload :CreateViewStatement, "plume/ast/create_view_statement"
35
48
  autoload :DefaultColumnConstraint, "plume/ast/default_column_constraint"
49
+ autoload :DeferrableColumnConstraint, "plume/ast/deferrable_column_constraint"
36
50
  autoload :EmptyFunctionArgument, "plume/ast/empty_function_argument"
37
51
  autoload :Expression, "plume/ast/expression"
38
52
  autoload :ForeignKeyAction, "plume/ast/foreign_key_action"
@@ -118,17 +132,113 @@ module Plume
118
132
  autoload :WindowDefinition, "plume/ast/window_definition"
119
133
  autoload :WithClause, "plume/ast/with_clause"
120
134
 
121
- SyntaxError = Class.new(StandardError)
135
+ # Where parsing gave up, for editors and linters that want more than the message:
136
+ # `location` is the byte range `[beg, fin]` of the offending token, as `Node#location` gives it,
137
+ # `token` its type (nil at the end of input), `line` counts from 1
138
+ class SyntaxError < StandardError
139
+ attr_reader :location, :token, :line, :expected
140
+
141
+ def initialize(message = nil, location: nil, token: nil, line: nil, expected: [])
142
+ @location, @token, @line, @expected = location, token, line, expected
143
+ super(message)
144
+ end
145
+
146
+ def position = location&.first
147
+ end
148
+
149
+ # What running out of stack raises. JRuby lets a Java StackOverflowError through
150
+ # when the stack runs out inside its own Java code rather than in Ruby
151
+ STACK_EXHAUSTED =
152
+ if defined?(JRUBY_VERSION)
153
+ require "java"
154
+ [SystemStackError, Java::JavaLang::StackOverflowError].freeze
155
+ else
156
+ [SystemStackError].freeze
157
+ end
122
158
 
123
- def self.tokenize(str, with_values: false, skip_spaces: true)
159
+ # `with_positions` adds each token's byte range `beg, fin`, after the value if there is one
160
+ def self.tokenize(str, with_values: false, with_positions: false, skip_spaces: true)
124
161
  lex = Lexer.new(str, skip_spaces: skip_spaces)
125
162
  [].tap do |tokens|
126
163
  while (token = lex.next_token)
127
- tokens << (with_values ? [token, lex.value] : token)
164
+ tokens <<
165
+ if with_positions
166
+ with_values ? [token, lex.value, lex.anchor, lex.cursor] : [token, lex.anchor, lex.cursor]
167
+ else
168
+ with_values ? [token, lex.value] : token
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Loads everything that is otherwise autoloaded and freezes what the node classes know
175
+ # about their props, which is what lets Plume run inside Ractors. Call it from the main
176
+ # Ractor before starting any. Node classes cannot get new props afterwards
177
+ def self.eager_load!
178
+ # SQLiteGrammar is a development tool which pulls in net/http
179
+ (constants - [:SQLiteGrammar]).each { |name| const_get(name) if autoload?(name) }
180
+ Analysis::Resolver
181
+ # Node constructors and type errors reach for these, and Ruby 3.3 cannot autoload outside the main Ractor.
182
+ # Not every Literal version has all of them
183
+ %i[Null TypeError ArgumentError].each { |name| Literal.const_get(name) if Literal.const_defined?(name) }
184
+ # Literal versions that cache a `_Deferred` do so on first use, which a frozen one cannot do.
185
+ # Each node's schema then shares Expression with the rest of its prop types
186
+ Expression === nil
187
+ make_shareable(Token::Tuple)
188
+ constants.each do |name|
189
+ klass = const_get(name)
190
+ klass.make_shareable! if Class === klass && klass <= Node
191
+ end
192
+ true
193
+ end
194
+
195
+ # The statements in `sql`, found by their tokens alone, so SQL that does not parse splits too.
196
+ # Each is its text from its first token to its last, without the semicolon; `with_positions`
197
+ # gives `[text, beg, fin]` with the byte range. A trigger's body stays in one piece
198
+ def self.split(sql, with_positions: false)
199
+ lexer = Lexer.new(sql, skip_spaces: true)
200
+ statements = []
201
+ beg = fin = nil
202
+ leading = [] # the first tokens of the statement, enough to tell a CREATE TRIGGER
203
+ body = cases = 0
204
+
205
+ while (token = lexer.next_token)
206
+ if token == :SEMI && body.zero?
207
+ statements << [beg, fin] if beg
208
+ beg = nil
209
+ leading.clear
210
+ cases = 0
211
+ next
128
212
  end
213
+
214
+ beg ||= lexer.anchor
215
+ fin = lexer.cursor
216
+ leading << token if leading.size < 3
217
+ trigger = leading[0] == :CREATE && (leading[1] == :TRIGGER || ((leading[1] in :TEMP | :TEMPORARY) && leading[2] == :TRIGGER))
218
+ next unless trigger
219
+
220
+ # Only END closes the body, and a CASE inside it has an END of its own
221
+ case token
222
+ when :BEGIN then body += 1 if body.zero?
223
+ when :CASE then cases += 1 if body.positive?
224
+ when :END
225
+ if cases.positive? then cases -= 1
226
+ elsif body.positive? then body -= 1
227
+ end
228
+ end
229
+ end
230
+ statements << [beg, fin] if beg
231
+
232
+ src = sql.b
233
+ statements.map do |b, f|
234
+ text = src.byteslice(b, f - b).force_encoding(sql.encoding)
235
+ with_positions ? [text, b, f] : text
129
236
  end
130
237
  end
131
238
 
239
+ # Every comment in `sql`, in order
240
+ def self.comments(sql) = Comments.in(sql)
241
+
132
242
  def self.parse(sql)
133
243
  Parser.new(sql).parse
134
244
  end
@@ -143,6 +253,27 @@ module Plume
143
253
  " #{line}\n #{caret}"
144
254
  end
145
255
 
256
+ # The bind parameters of one statement, given as SQL or already parsed
257
+ def self.parameters(sql)
258
+ statements = (Node === sql) ? [sql] : Array(Parser.new(sql).parse)
259
+ raise ArgumentError, "expected exactly one statement, got #{statements.size}" unless statements.size == 1
260
+
261
+ Parameters.new(statements.first)
262
+ end
263
+
264
+ # A tree from what `Node#dump` gave, or a list of them
265
+ def self.load(dump) = Dump.load(dump)
266
+ # At most `max_length` characters of `sql`, with the least telling parts left out first,
267
+ # see Plume::Truncate. A parsed tree is printed first
268
+ def self.truncate(sql, max_length) = Truncate.call((Node === sql) ? sql.to_sql : sql, max_length)
269
+ # The SQL with its constants turned into `?`, see Plume::Normalize. A parsed tree gives a tree back
270
+ def self.normalize(sql) = (Node === sql) ? Normalize.tree(sql) : Normalize.text(sql)
271
+
272
+ # The same for the same query whatever its values and spelling, see Plume::Fingerprint
273
+ def self.fingerprint(sql) = Fingerprint.of((Node === sql || Array === sql) ? sql : Parser.new(sql).parse)
274
+
146
275
  def self.reads(...) = Analysis.reads(...)
276
+ def self.writes(...) = Analysis.writes(...)
147
277
  def self.access(...) = Analysis.access(...)
278
+ def self.summary(...) = Analysis.summary(...)
148
279
  end