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,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
|