plume 0.1.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 +506 -11
- data/lib/plume/analysis/resolver.rb +797 -0
- data/lib/plume/analysis.rb +211 -0
- data/lib/plume/ast/alter_table_statement.rb +40 -0
- data/lib/plume/ast/analyze_statement.rb +15 -0
- data/lib/plume/ast/assignment.rb +18 -0
- data/lib/plume/ast/attach_statement.rb +17 -0
- data/lib/plume/ast/begin_statement.rb +17 -0
- data/lib/plume/ast/binary_expression.rb +71 -0
- data/lib/plume/ast/case_condition.rb +29 -0
- data/lib/plume/ast/case_expression.rb +22 -0
- data/lib/plume/ast/cast_expression.rb +16 -0
- data/lib/plume/ast/check_column_constraint.rb +20 -0
- data/lib/plume/ast/check_table_constraint.rb +20 -0
- data/lib/plume/ast/collate_column_constraint.rb +19 -0
- data/lib/plume/ast/collation_expression.rb +26 -0
- data/lib/plume/ast/column_constraint.rb +7 -0
- data/lib/plume/ast/column_definition.rb +16 -0
- data/lib/plume/ast/column_name.rb +22 -0
- data/lib/plume/ast/column_type.rb +26 -0
- data/lib/plume/ast/commit_statement.rb +13 -0
- data/lib/plume/ast/common_table_expression.rb +31 -0
- data/lib/plume/ast/compound_select.rb +30 -0
- data/lib/plume/ast/conflict_clause.rb +22 -0
- data/lib/plume/ast/conflict_resolution.rb +20 -0
- data/lib/plume/ast/create_index_statement.rb +29 -0
- data/lib/plume/ast/create_table_statement.rb +47 -0
- data/lib/plume/ast/create_trigger_statement.rb +43 -0
- data/lib/plume/ast/create_view_statement.rb +30 -0
- data/lib/plume/ast/create_virtual_table_statement.rb +28 -0
- data/lib/plume/ast/default_column_constraint.rb +29 -0
- data/lib/plume/ast/deferrable_column_constraint.rb +20 -0
- data/lib/plume/ast/delete_statement.rb +28 -0
- data/lib/plume/ast/detach_statement.rb +13 -0
- data/lib/plume/ast/drop_statement.rb +21 -0
- data/lib/plume/ast/empty_function_argument.rb +6 -0
- data/lib/plume/ast/exists_expression.rb +15 -0
- data/lib/plume/ast/explain_statement.rb +15 -0
- data/lib/plume/ast/expression.rb +30 -0
- data/lib/plume/ast/foreign_key_action.rb +26 -0
- data/lib/plume/ast/foreign_key_clause.rb +50 -0
- data/lib/plume/ast/foreign_key_column_constraint.rb +16 -0
- data/lib/plume/ast/foreign_key_table_constraint.rb +20 -0
- data/lib/plume/ast/frame_boundary.rb +35 -0
- data/lib/plume/ast/frame_spec.rb +34 -0
- data/lib/plume/ast/function_arguments.rb +18 -0
- data/lib/plume/ast/function_reference.rb +29 -0
- data/lib/plume/ast/generated_as_column_constraint.rb +24 -0
- data/lib/plume/ast/indexed_column.rb +24 -0
- data/lib/plume/ast/insert_statement.rb +35 -0
- data/lib/plume/ast/join.rb +12 -0
- data/lib/plume/ast/join_clause.rb +15 -0
- data/lib/plume/ast/join_on_constraint.rb +12 -0
- data/lib/plume/ast/join_operator.rb +31 -0
- data/lib/plume/ast/join_using_constraint.rb +16 -0
- data/lib/plume/ast/like_expression.rb +19 -0
- data/lib/plume/ast/literal_expression.rb +29 -0
- data/lib/plume/ast/literal_false.rb +7 -0
- data/lib/plume/ast/literal_nil.rb +7 -0
- data/lib/plume/ast/literal_value.rb +18 -0
- data/lib/plume/ast/match_clause.rb +15 -0
- data/lib/plume/ast/node.rb +330 -0
- data/lib/plume/ast/noop_column_constraint.rb +15 -0
- data/lib/plume/ast/noop_table_constraint.rb +15 -0
- data/lib/plume/ast/not_like_expression.rb +7 -0
- data/lib/plume/ast/not_null_column_constraint.rb +17 -0
- data/lib/plume/ast/null_column_constraint.rb +17 -0
- data/lib/plume/ast/ordering_term.rb +32 -0
- data/lib/plume/ast/over_clause.rb +25 -0
- data/lib/plume/ast/parenthetical_expression.rb +37 -0
- data/lib/plume/ast/parenthetical_source.rb +19 -0
- data/lib/plume/ast/pragma_statement.rb +26 -0
- data/lib/plume/ast/primary_key_column_constraint.rb +23 -0
- data/lib/plume/ast/primary_key_table_constraint.rb +23 -0
- data/lib/plume/ast/raise_expression.rb +23 -0
- data/lib/plume/ast/reindex_statement.rb +15 -0
- data/lib/plume/ast/release_statement.rb +16 -0
- data/lib/plume/ast/result_column.rb +18 -0
- data/lib/plume/ast/returning_clause.rb +12 -0
- data/lib/plume/ast/rollback_statement.rb +19 -0
- data/lib/plume/ast/savepoint_statement.rb +16 -0
- data/lib/plume/ast/select_core.rb +27 -0
- data/lib/plume/ast/select_statement.rb +34 -0
- data/lib/plume/ast/star_function_argument.rb +6 -0
- data/lib/plume/ast/star_result_column.rb +16 -0
- data/lib/plume/ast/strict_table_option.rb +6 -0
- data/lib/plume/ast/subquery_expression.rb +14 -0
- data/lib/plume/ast/subquery_source.rb +19 -0
- data/lib/plume/ast/table_constraint.rb +7 -0
- data/lib/plume/ast/table_function.rb +28 -0
- data/lib/plume/ast/table_name.rb +28 -0
- data/lib/plume/ast/table_option.rb +7 -0
- data/lib/plume/ast/ternary_expression.rb +26 -0
- data/lib/plume/ast/token.rb +183 -0
- data/lib/plume/ast/unary_expression.rb +36 -0
- data/lib/plume/ast/unique_column_constraint.rb +17 -0
- data/lib/plume/ast/unique_table_constraint.rb +20 -0
- data/lib/plume/ast/update_statement.rb +35 -0
- data/lib/plume/ast/upsert_clause.rb +26 -0
- data/lib/plume/ast/vacuum_statement.rb +18 -0
- data/lib/plume/ast/values_clause.rb +12 -0
- data/lib/plume/ast/values_row.rb +13 -0
- data/lib/plume/ast/variable.rb +15 -0
- data/lib/plume/ast/window_definition.rb +26 -0
- data/lib/plume/ast/with_clause.rb +15 -0
- data/lib/plume/ast/without_rowid_table_option.rb +6 -0
- data/lib/plume/catalog.rb +276 -0
- 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 +640 -0
- 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 +4064 -0
- data/lib/plume/pretty_printer.rb +202 -0
- data/lib/plume/printer.rb +525 -0
- data/lib/plume/sqlite_grammar.rb +86 -0
- data/lib/plume/truncate.rb +67 -0
- data/lib/plume/union.rb +28 -0
- data/lib/plume/version.rb +1 -1
- data/lib/plume.rb +274 -3
- metadata +176 -12
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# Turns an AST back into SQL. It works from the nodes' readers rather than from the tokens
|
|
5
|
+
# they were parsed from, so trees built by hand, or changed after parsing, print too. The
|
|
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 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.
|
|
10
|
+
class Printer
|
|
11
|
+
BARE_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
12
|
+
|
|
13
|
+
BINARY_OPERATORS = {
|
|
14
|
+
ALL: "AND", ANY: "OR", CONCAT: "||", EXTRACT: "->", RETRIEVE: "->>",
|
|
15
|
+
MULTIPLY: "*", DIVIDE: "/", MODULO: "%", ADD: "+", SUB: "-",
|
|
16
|
+
BIT_AND: "&", BIT_OR: "|", BIT_LSHIFT: "<<", BIT_RSHIFT: ">>",
|
|
17
|
+
BELOW: "<", ABOVE: ">", ATMOST: "<=", ATLEAST: ">=", EQUALS: "=", NOT_EQUALS: "!=",
|
|
18
|
+
IS: "IS", IS_NOT: "IS NOT", GLOB: "GLOB", NOT_GLOB: "NOT GLOB", REGEXP: "REGEXP",
|
|
19
|
+
NOT_REGEXP: "NOT REGEXP", MATCH: "MATCH", NOT_MATCH: "NOT MATCH", IN: "IN", NOT_IN: "NOT IN",
|
|
20
|
+
}.freeze
|
|
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
|
+
|
|
37
|
+
COMPOUND_OPERATORS = { UNION: "UNION", UNION_ALL: "UNION ALL", INTERSECT: "INTERSECT", EXCEPT: "EXCEPT" }.freeze
|
|
38
|
+
JOIN_KINDS = { COMMA: ",", INNER: "JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN", FULL: "FULL JOIN", CROSS: "CROSS JOIN" }.freeze
|
|
39
|
+
FOREIGN_KEY_ACTIONS = { CASCADE: "CASCADE", RESTRICT: "RESTRICT", SET_NULL: "SET NULL", SET_DEFAULT: "SET DEFAULT", NO_ACTION: "NO ACTION" }.freeze
|
|
40
|
+
|
|
41
|
+
# Operands of these need parentheses to keep their grouping when printed inside another expression
|
|
42
|
+
COMPOUND_EXPRESSIONS = [BinaryExpression, UnaryExpression, TernaryExpression, LikeExpression, CollationExpression].freeze
|
|
43
|
+
|
|
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
|
|
52
|
+
|
|
53
|
+
def print(node)
|
|
54
|
+
case node
|
|
55
|
+
when nil then nil
|
|
56
|
+
when Array then node.map { |n| print(n) }.join(", ")
|
|
57
|
+
when Node then __send__(:"print_#{underscore(node.class.name.split("::").last)}", node)
|
|
58
|
+
# Expressions built by hand may hold plain values, which stand for the literal they spell
|
|
59
|
+
when LiteralValue then print(LiteralExpression.new(value: node))
|
|
60
|
+
else raise ArgumentError, "cannot print #{node.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def identifier(name)
|
|
65
|
+
name = name.to_s
|
|
66
|
+
return name if name.ascii_only? && name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
|
|
67
|
+
|
|
68
|
+
%("#{doubled(name, '"')}")
|
|
69
|
+
end
|
|
70
|
+
|
|
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))
|
|
74
|
+
|
|
75
|
+
def string(value) = "'#{doubled(value.to_s, "'")}'"
|
|
76
|
+
|
|
77
|
+
private
|
|
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
|
+
|
|
82
|
+
def underscore(name) = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
83
|
+
|
|
84
|
+
def join(*parts) = parts.flatten.reject { |p| !p || (p.respond_to?(:empty?) && p.empty?) }.join(" ")
|
|
85
|
+
|
|
86
|
+
def qualified(*names) = names.compact.map { |n| identifier(n) }.join(".")
|
|
87
|
+
|
|
88
|
+
def operand(node) = (COMPOUND_EXPRESSIONS.any? { |klass| klass === node }) ? "(#{print(node)})" : print(node)
|
|
89
|
+
|
|
90
|
+
def alias_of(node) = node.alias && "AS #{identifier(node.alias)}"
|
|
91
|
+
|
|
92
|
+
def list(items) = Array(items).map { |item| print(item) }.join(", ")
|
|
93
|
+
|
|
94
|
+
def parenthesized_names(names) = names && "(#{names.map { |n| identifier(n) }.join(", ")})"
|
|
95
|
+
|
|
96
|
+
# -- statements
|
|
97
|
+
|
|
98
|
+
def print_explain_statement(node) = join(node.query_plan ? "EXPLAIN QUERY PLAN" : "EXPLAIN", print(node.statement))
|
|
99
|
+
|
|
100
|
+
def print_select_statement(node)
|
|
101
|
+
join(print(node.with_clause), print(node.body), order_and_limit(node))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def order_and_limit(node)
|
|
105
|
+
[
|
|
106
|
+
node.orderings && "ORDER BY #{list(node.orderings)}",
|
|
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,
|
|
113
|
+
]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def print_with_clause(node) = join("WITH", node.recursive && "RECURSIVE", list(node.ctes))
|
|
117
|
+
|
|
118
|
+
def print_common_table_expression(node)
|
|
119
|
+
materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
|
|
120
|
+
join(identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : ""), "AS", materialized, "(#{print(node.select)})")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def print_compound_select(node)
|
|
124
|
+
join(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def print_select_core(node)
|
|
128
|
+
join(
|
|
129
|
+
"SELECT", node.distinct && "DISTINCT", list(node.columns),
|
|
130
|
+
node.source && "FROM #{print(node.source)}",
|
|
131
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
132
|
+
node.groupings && "GROUP BY #{list(node.groupings)}",
|
|
133
|
+
node.having && "HAVING #{print(node.having)}",
|
|
134
|
+
node.windows && "WINDOW #{list(node.windows)}",
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def print_values_clause(node) = "VALUES #{list(node.rows)}"
|
|
139
|
+
def print_values_row(node) = "(#{list(node.values)})"
|
|
140
|
+
|
|
141
|
+
def print_result_column(node) = join(print(node.result), alias_of(node))
|
|
142
|
+
def print_star_result_column(node) = node.table ? "#{identifier(node.table)}.*" : "*"
|
|
143
|
+
|
|
144
|
+
def print_window_definition(node) = "#{identifier(node.name)} AS (#{window_body(node)})"
|
|
145
|
+
|
|
146
|
+
def window_body(node)
|
|
147
|
+
join(
|
|
148
|
+
node.base_window_name && identifier(node.base_window_name),
|
|
149
|
+
node.partitions && "PARTITION BY #{list(node.partitions)}",
|
|
150
|
+
node.orderings && "ORDER BY #{list(node.orderings)}",
|
|
151
|
+
print(node.frame),
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def print_insert_statement(node)
|
|
156
|
+
verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
|
|
157
|
+
join(
|
|
158
|
+
print(node.with_clause), verb, "INTO", print(node.table),
|
|
159
|
+
node.columns && "(#{list(node.columns)})",
|
|
160
|
+
node.default_values ? "DEFAULT VALUES" : print(node.select),
|
|
161
|
+
node.upserts&.map { |u| print(u) },
|
|
162
|
+
print(node.returning),
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def print_upsert_clause(node)
|
|
167
|
+
target = node.targets && "(#{list(node.targets)})"
|
|
168
|
+
action =
|
|
169
|
+
if node.action == :NOTHING
|
|
170
|
+
"DO NOTHING"
|
|
171
|
+
else
|
|
172
|
+
join("DO UPDATE SET", list(node.assignments), node.condition && "WHERE #{print(node.condition)}")
|
|
173
|
+
end
|
|
174
|
+
join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}", action)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def print_update_statement(node)
|
|
178
|
+
join(
|
|
179
|
+
print(node.with_clause), node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table),
|
|
180
|
+
"SET", list(node.assignments),
|
|
181
|
+
node.source && "FROM #{print(node.source)}",
|
|
182
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
183
|
+
print(node.returning), order_and_limit(node),
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def print_delete_statement(node)
|
|
188
|
+
join(
|
|
189
|
+
print(node.with_clause), "DELETE FROM", print(node.table),
|
|
190
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
191
|
+
print(node.returning), order_and_limit(node),
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def print_assignment(node)
|
|
196
|
+
target = (node.columns.size == 1 && !node.columns_lp?) ? print(node.columns[0]) : "(#{list(node.columns)})"
|
|
197
|
+
"#{target} = #{print(node.value)}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def print_returning_clause(node) = "RETURNING #{list(node.columns)}"
|
|
201
|
+
|
|
202
|
+
def print_create_table_statement(node)
|
|
203
|
+
head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
|
|
204
|
+
return join(head, "AS", print(node.select_statement)) if node.select_statement
|
|
205
|
+
|
|
206
|
+
definitions = list([*node.columns, *node.constraints])
|
|
207
|
+
join(head, "(#{definitions})", node.options && list(node.options))
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def print_create_view_statement(node)
|
|
211
|
+
join(
|
|
212
|
+
"CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
|
|
213
|
+
node.columns && "(#{list(node.columns)})", "AS", print(node.select),
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def print_create_index_statement(node)
|
|
218
|
+
join(
|
|
219
|
+
"CREATE", node.unique && "UNIQUE", "INDEX", node.if_not_exists && "IF NOT EXISTS", print(node.index),
|
|
220
|
+
"ON", identifier(node.table), "(#{list(node.columns)})",
|
|
221
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def print_create_virtual_table_statement(node)
|
|
226
|
+
arguments = node.arguments_lp? ? "(#{node.arguments.join(", ")})" : nil
|
|
227
|
+
join("CREATE VIRTUAL TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table), "USING", identifier(node.module_name) + arguments.to_s)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def print_create_trigger_statement(node)
|
|
231
|
+
event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
|
|
232
|
+
join(
|
|
233
|
+
"CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
|
|
234
|
+
node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
|
|
235
|
+
node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
|
|
236
|
+
"BEGIN", node.statements.map { |s| "#{print(s)};" }, "END",
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def print_alter_table_statement(node)
|
|
241
|
+
action =
|
|
242
|
+
case node.action
|
|
243
|
+
when :RENAME_TABLE then "RENAME TO #{identifier(node.new_name)}"
|
|
244
|
+
when :RENAME_COLUMN then "RENAME COLUMN #{identifier(node.column)} TO #{identifier(node.new_name)}"
|
|
245
|
+
when :ADD_COLUMN then "ADD COLUMN #{print(node.column_definition)}"
|
|
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"
|
|
251
|
+
end
|
|
252
|
+
join("ALTER TABLE", print(node.table), action)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def print_drop_statement(node) = join("DROP", node.type.to_s, node.if_exists && "IF EXISTS", print(node.target))
|
|
256
|
+
|
|
257
|
+
def print_begin_statement(node) = join("BEGIN", node.type&.to_s)
|
|
258
|
+
def print_commit_statement(_node) = "COMMIT"
|
|
259
|
+
def print_rollback_statement(node) = join("ROLLBACK", node.savepoint && "TO SAVEPOINT #{identifier(node.savepoint)}")
|
|
260
|
+
def print_savepoint_statement(node) = "SAVEPOINT #{identifier(node.name)}"
|
|
261
|
+
def print_release_statement(node) = "RELEASE SAVEPOINT #{identifier(node.name)}"
|
|
262
|
+
|
|
263
|
+
def print_pragma_statement(node)
|
|
264
|
+
name = qualified(node.schema, node.name)
|
|
265
|
+
return "PRAGMA #{name}" if node.value.nil?
|
|
266
|
+
|
|
267
|
+
value =
|
|
268
|
+
case node.value
|
|
269
|
+
when Numeric then node.value.to_s
|
|
270
|
+
else (node.value_tk_tok == :STRING) ? string(node.value) : identifier(node.value)
|
|
271
|
+
end
|
|
272
|
+
(node.eq_tk_tok == :LP) ? "PRAGMA #{name}(#{value})" : "PRAGMA #{name} = #{value}"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def print_attach_statement(node)
|
|
276
|
+
join("ATTACH DATABASE", print(node.file), "AS", print(node.schema), node.key && "KEY #{print(node.key)}")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def print_detach_statement(node) = "DETACH DATABASE #{print(node.schema)}"
|
|
280
|
+
def print_analyze_statement(node) = join("ANALYZE", print(node.target))
|
|
281
|
+
def print_reindex_statement(node) = join("REINDEX", print(node.target))
|
|
282
|
+
def print_vacuum_statement(node) = join("VACUUM", node.schema && identifier(node.schema), node.into && "INTO #{print(node.into)}")
|
|
283
|
+
|
|
284
|
+
# -- FROM
|
|
285
|
+
|
|
286
|
+
def print_table_name(node)
|
|
287
|
+
indexed = node.indexed_by ? "INDEXED BY #{identifier(node.indexed_by)}" : (node.not_indexed && "NOT INDEXED")
|
|
288
|
+
join(qualified(node.schema, node.table), alias_of(node), indexed)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def print_table_function(node)
|
|
292
|
+
join("#{qualified(node.schema, node.function)}(#{list(node.arguments)})", alias_of(node))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def print_subquery_source(node) = join("(#{print(node.select)})", alias_of(node))
|
|
296
|
+
def print_parenthetical_source(node) = join("(#{print(node.source)})", alias_of(node))
|
|
297
|
+
|
|
298
|
+
def print_join_clause(node)
|
|
299
|
+
node.joins.reduce(print(node.left)) do |sql, each|
|
|
300
|
+
operator = print(each.operator)
|
|
301
|
+
right = join(print(each.source), print(each.constraint))
|
|
302
|
+
(operator == ",") ? "#{sql}, #{right}" : "#{sql} #{operator} #{right}"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def print_join_operator(node) = join(node.natural && "NATURAL", JOIN_KINDS.fetch(node.kind))
|
|
307
|
+
def print_join_on_constraint(node) = "ON #{print(node.expression)}"
|
|
308
|
+
def print_join_using_constraint(node) = "USING (#{list(node.columns)})"
|
|
309
|
+
|
|
310
|
+
# -- expressions
|
|
311
|
+
|
|
312
|
+
def print_column_name(node) = qualified(node.schema, node.table, node.column)
|
|
313
|
+
|
|
314
|
+
def print_literal_expression(node)
|
|
315
|
+
# Numbers keep their spelling, so 0x1F stays hex and 1e3 stays an exponent
|
|
316
|
+
return node.value_tk_src if node.value_tk? && node.value_tk_spn.last == :numeric
|
|
317
|
+
|
|
318
|
+
case (value = node.value)
|
|
319
|
+
when nil then "NULL"
|
|
320
|
+
when true then "TRUE"
|
|
321
|
+
when false then "FALSE"
|
|
322
|
+
when Symbol then value.to_s
|
|
323
|
+
when Integer then value.to_s
|
|
324
|
+
when Float
|
|
325
|
+
# SQLite has no literal for NaN or infinity
|
|
326
|
+
value.finite? ? value.to_s : raise(ArgumentError, "cannot print #{value}")
|
|
327
|
+
when String
|
|
328
|
+
(value.encoding == Encoding::BINARY) ? "X'#{value.unpack1("H*").upcase}'" : string(value)
|
|
329
|
+
# A Rational would print as integer division (1/3), a Complex as nothing SQL can read
|
|
330
|
+
else raise ArgumentError, "cannot print #{value.inspect}"
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def print_variable(node) = node.name
|
|
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
|
|
339
|
+
def print_binary_expression(node)
|
|
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
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def print_unary_expression(node)
|
|
351
|
+
case node.operator
|
|
352
|
+
when :NOT then "NOT #{operand(node.operand)}"
|
|
353
|
+
when :NEGATE then "-#{unary_operand(node.operand)}"
|
|
354
|
+
when :IDENTITY then "+#{unary_operand(node.operand)}"
|
|
355
|
+
when :INVERT then "~#{unary_operand(node.operand)}"
|
|
356
|
+
when :IS_NULL then "#{operand(node.operand)} ISNULL"
|
|
357
|
+
when :NOT_NULL then "#{operand(node.operand)} NOTNULL"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Two minus signs in a row would start a comment
|
|
362
|
+
def unary_operand(node)
|
|
363
|
+
sql = operand(node)
|
|
364
|
+
sql.start_with?("-", "+") ? "(#{sql})" : sql
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def print_ternary_expression(node)
|
|
368
|
+
operator = (node.operator == :NOT_BETWEEN) ? "NOT BETWEEN" : "BETWEEN"
|
|
369
|
+
"#{operand(node.left)} #{operator} #{operand(node.middle)} AND #{operand(node.right)}"
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def print_like_expression(node)
|
|
373
|
+
operator = (node.operator == :NOT_LIKE) ? "NOT LIKE" : "LIKE"
|
|
374
|
+
join("#{operand(node.left)} #{operator} #{operand(node.right)}", node.escape && "ESCAPE #{operand(node.escape)}")
|
|
375
|
+
end
|
|
376
|
+
alias_method :print_not_like_expression, :print_like_expression
|
|
377
|
+
|
|
378
|
+
def print_collation_expression(node) = "#{operand(node.expression)} COLLATE #{identifier(node.sequence)}"
|
|
379
|
+
|
|
380
|
+
def print_parenthetical_expression(node) = "(#{print(node.contents)})"
|
|
381
|
+
|
|
382
|
+
def print_cast_expression(node) = "CAST(#{join(print(node.expression), "AS", print(node.as))})"
|
|
383
|
+
|
|
384
|
+
def print_case_expression(node)
|
|
385
|
+
join("CASE", node.predicate && print(node.predicate), list_of(node.conditions), node.else_clause && "ELSE #{print(node.else_clause)}", "END")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def list_of(items) = items.map { |item| print(item) }
|
|
389
|
+
|
|
390
|
+
def print_case_condition(node) = "WHEN #{print(node.predicate)} THEN #{print(node.consequence)}"
|
|
391
|
+
|
|
392
|
+
def print_function_reference(node)
|
|
393
|
+
join(
|
|
394
|
+
"#{qualified(node.schema, node.function)}(#{print(node.arguments)})",
|
|
395
|
+
node.within_group && "WITHIN GROUP (ORDER BY #{print(node.within_group)})",
|
|
396
|
+
node.filter_clause && "FILTER (WHERE #{print(node.filter_clause)})",
|
|
397
|
+
print(node.over_clause),
|
|
398
|
+
)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def print_function_arguments(node)
|
|
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)}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def print_star_function_argument(_node) = "*"
|
|
407
|
+
def print_empty_function_argument(_node) = ""
|
|
408
|
+
|
|
409
|
+
def print_over_clause(node)
|
|
410
|
+
return "OVER #{identifier(node.window_name)}" if node.window_name
|
|
411
|
+
|
|
412
|
+
"OVER (#{window_body(node)})"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def print_frame_spec(node)
|
|
416
|
+
type = node.type.to_s.upcase
|
|
417
|
+
exclude = node.exclude && "EXCLUDE #{node.exclude.to_s.upcase.tr("_", " ")}"
|
|
418
|
+
if node.ending_boundary
|
|
419
|
+
join(type, "BETWEEN", print(node.starting_boundary), "AND", print(node.ending_boundary), exclude)
|
|
420
|
+
else
|
|
421
|
+
join(type, print(node.starting_boundary), exclude)
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def print_frame_boundary(node)
|
|
426
|
+
type = node.type.to_s.upcase.tr("_", " ")
|
|
427
|
+
return type if type == "CURRENT ROW"
|
|
428
|
+
|
|
429
|
+
value = node.value
|
|
430
|
+
join((Symbol === value || String === value) ? value.to_s.upcase : print(value), type)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def print_subquery_expression(node) = "(#{print(node.select)})"
|
|
434
|
+
def print_exists_expression(node) = "EXISTS (#{print(node.select)})"
|
|
435
|
+
|
|
436
|
+
def print_raise_expression(node)
|
|
437
|
+
return "RAISE(IGNORE)" if node.type == :IGNORE
|
|
438
|
+
|
|
439
|
+
"RAISE(#{node.type}, #{print(node.error_message)})"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def print_ordering_term(node)
|
|
443
|
+
join(
|
|
444
|
+
print(node.expression), node.collation && "COLLATE #{identifier(node.collation)}",
|
|
445
|
+
node.direction&.to_s&.upcase, node.nulls && "NULLS #{node.nulls.to_s.upcase}",
|
|
446
|
+
)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# -- column and table definitions
|
|
450
|
+
|
|
451
|
+
def print_column_definition(node) = join(identifier(node.name), print(node.type), list_of(Array(node.constraints)))
|
|
452
|
+
def print_column_type(node) = node.text
|
|
453
|
+
|
|
454
|
+
def constraint_name(node) = node.name && "CONSTRAINT #{identifier(node.name)}"
|
|
455
|
+
|
|
456
|
+
def print_primary_key_column_constraint(node)
|
|
457
|
+
join(constraint_name(node), "PRIMARY KEY", node.direction&.to_s, print(node.conflict_clause), node.autoincrement && "AUTOINCREMENT")
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def print_not_null_column_constraint(node) = join(constraint_name(node), "NOT NULL", print(node.conflict_clause))
|
|
461
|
+
def print_null_column_constraint(node) = join(constraint_name(node), "NULL", print(node.conflict_clause))
|
|
462
|
+
def print_unique_column_constraint(node) = join(constraint_name(node), "UNIQUE", print(node.conflict_clause))
|
|
463
|
+
def print_check_column_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
|
|
464
|
+
def print_collate_column_constraint(node) = join(constraint_name(node), "COLLATE #{identifier(node.collation)}")
|
|
465
|
+
def print_foreign_key_column_constraint(node) = join(constraint_name(node), print(node.foreign_key_clause))
|
|
466
|
+
def print_no_op_column_constraint(node) = constraint_name(node)
|
|
467
|
+
|
|
468
|
+
def print_default_column_constraint(node)
|
|
469
|
+
return join(constraint_name(node), "DEFAULT #{node.value_tk_src}") if node.value_tk?
|
|
470
|
+
|
|
471
|
+
value = node.value
|
|
472
|
+
sql =
|
|
473
|
+
case value
|
|
474
|
+
when Node then "(#{print(value)})"
|
|
475
|
+
else print(LiteralExpression.new(value:))
|
|
476
|
+
end
|
|
477
|
+
join(constraint_name(node), "DEFAULT #{sql}")
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def print_generated_as_column_constraint(node)
|
|
481
|
+
join(constraint_name(node), "GENERATED ALWAYS AS (#{print(node.expression)})", node.type&.to_s)
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def print_conflict_clause(node) = "ON CONFLICT #{node.resolution}"
|
|
485
|
+
|
|
486
|
+
def print_primary_key_table_constraint(node)
|
|
487
|
+
join(constraint_name(node), "PRIMARY KEY (#{list(node.columns)}#{" AUTOINCREMENT" if node.autoincrement})", print(node.conflict_clause))
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def print_unique_table_constraint(node) = join(constraint_name(node), "UNIQUE (#{list(node.columns)})", print(node.conflict_clause))
|
|
491
|
+
def print_check_table_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
|
|
492
|
+
def print_no_op_table_constraint(node) = constraint_name(node)
|
|
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
|
+
|
|
503
|
+
def print_foreign_key_table_constraint(node)
|
|
504
|
+
join(constraint_name(node), "FOREIGN KEY (#{list(node.columns)})", print(node.foreign_key_clause))
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def print_foreign_key_clause(node)
|
|
508
|
+
join(
|
|
509
|
+
"REFERENCES", identifier(node.table), node.columns && "(#{list(node.columns)})",
|
|
510
|
+
list_of(Array(node.actions)), list_of(Array(node.match_clauses)),
|
|
511
|
+
deferrable(node),
|
|
512
|
+
)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def print_foreign_key_action(node) = "ON #{node.trigger.to_s.upcase} #{FOREIGN_KEY_ACTIONS.fetch(node.action)}"
|
|
516
|
+
def print_match_clause(node) = "MATCH #{node.type}"
|
|
517
|
+
|
|
518
|
+
def print_indexed_column(node)
|
|
519
|
+
join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s, node.nulls && "NULLS #{node.nulls.to_s.upcase}")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def print_strict_table_option(_node) = "STRICT"
|
|
523
|
+
def print_without_rowid_table_option(_node) = "WITHOUT ROWID"
|
|
524
|
+
end
|
|
525
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Plume
|
|
7
|
+
module SQLiteGrammar
|
|
8
|
+
extend self
|
|
9
|
+
|
|
10
|
+
PARSE_Y_URI = URI("https://raw.githubusercontent.com/sqlite/sqlite/refs/heads/master/src/parse.y").freeze
|
|
11
|
+
RULE_PATTERN = /^(?:.+::=[^.]+\.)|(?:%token_class[^.]+\.)/
|
|
12
|
+
IGNORED_SNIPPETS = [
|
|
13
|
+
%r{^//},
|
|
14
|
+
/^scanpt.+::=/,
|
|
15
|
+
/^scantok.+::=/,
|
|
16
|
+
].freeze
|
|
17
|
+
TOKEN_CLASS_SIGIL = "%token_class "
|
|
18
|
+
|
|
19
|
+
def rules
|
|
20
|
+
return @hash if defined? @hash
|
|
21
|
+
|
|
22
|
+
# extract the SQL statements from the test files
|
|
23
|
+
@hash = Hash.new { |h, k| h[k] = [] }
|
|
24
|
+
matches = contents.scan(RULE_PATTERN)
|
|
25
|
+
matches.each do |match|
|
|
26
|
+
result = match.tap(&:strip!)
|
|
27
|
+
next if IGNORED_SNIPPETS.any? { |it| result.match?(it) }
|
|
28
|
+
|
|
29
|
+
if result.start_with?(TOKEN_CLASS_SIGIL)
|
|
30
|
+
rule, definition = result.gsub(TOKEN_CLASS_SIGIL, "").split(/\s+/)
|
|
31
|
+
else
|
|
32
|
+
rule, definition = result.gsub(/\(.*?\)/, "").split(/\s+::=\s+/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
tokens = definition.chomp(".").tap(&:strip!).split(/\s+/)
|
|
36
|
+
next (@hash[rule.strip] << tokens) if tokens.empty?
|
|
37
|
+
|
|
38
|
+
sequences = tokens.map { |it| it.split("|") }.reduce(&:product).map { |it| Array(it).flatten }
|
|
39
|
+
sequences.each { |it| @hash[rule.strip] << it.reject { |it| ["scanpt", "scantok"].include?(it) } }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@hash
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def token_names
|
|
46
|
+
rules
|
|
47
|
+
.values
|
|
48
|
+
.flatten
|
|
49
|
+
.uniq
|
|
50
|
+
.select { |it| it.getbyte(0) <= 90 }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def rule_names
|
|
54
|
+
rules.keys
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def rules_with(token_or_rule)
|
|
58
|
+
rules.select do |_, sequences|
|
|
59
|
+
sequences.any? { |sequence| sequence.include? token_or_rule }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def contents
|
|
64
|
+
return @contents if defined? @contents
|
|
65
|
+
|
|
66
|
+
# download the sqlite `parse.y` file for the master branch
|
|
67
|
+
Net::HTTP.start(PARSE_Y_URI.host, PARSE_Y_URI.port, use_ssl: true) do |http|
|
|
68
|
+
request = Net::HTTP::Get.new(PARSE_Y_URI)
|
|
69
|
+
response = http.request(request)
|
|
70
|
+
|
|
71
|
+
# Handle redirects
|
|
72
|
+
if response.is_a?(Net::HTTPRedirection)
|
|
73
|
+
uri = URI(response["location"])
|
|
74
|
+
response = Net::HTTP.get_response(uri)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
78
|
+
@contents = response.body
|
|
79
|
+
return @contents
|
|
80
|
+
else
|
|
81
|
+
raise "Error: #{response.code} - #{response.message}"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -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
|