plume 0.1.0 → 0.2.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 +280 -10
- data/lib/plume/analysis/resolver.rb +574 -0
- data/lib/plume/analysis.rb +135 -0
- data/lib/plume/ast/alter_table_statement.rb +29 -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 +19 -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 +25 -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/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 +17 -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 +21 -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 +207 -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 +180 -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 +180 -0
- data/lib/plume/lexer.rb +639 -0
- data/lib/plume/parser.rb +4015 -0
- data/lib/plume/printer.rb +471 -0
- data/lib/plume/sqlite_grammar.rb +86 -0
- data/lib/plume/union.rb +28 -0
- data/lib/plume/version.rb +1 -1
- data/lib/plume.rb +143 -3
- metadata +167 -12
|
@@ -0,0 +1,471 @@
|
|
|
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 every compound operand gets
|
|
8
|
+
# parentheses so precedence never has to be second-guessed.
|
|
9
|
+
class Printer
|
|
10
|
+
BARE_IDENTIFIER = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
11
|
+
|
|
12
|
+
BINARY_OPERATORS = {
|
|
13
|
+
ALL: "AND", ANY: "OR", CONCAT: "||", EXTRACT: "->", RETRIEVE: "->>",
|
|
14
|
+
MULTIPLY: "*", DIVIDE: "/", MODULO: "%", ADD: "+", SUB: "-",
|
|
15
|
+
BIT_AND: "&", BIT_OR: "|", BIT_LSHIFT: "<<", BIT_RSHIFT: ">>",
|
|
16
|
+
BELOW: "<", ABOVE: ">", ATMOST: "<=", ATLEAST: ">=", EQUALS: "=", NOT_EQUALS: "!=",
|
|
17
|
+
IS: "IS", IS_NOT: "IS NOT", GLOB: "GLOB", NOT_GLOB: "NOT GLOB", REGEXP: "REGEXP",
|
|
18
|
+
NOT_REGEXP: "NOT REGEXP", MATCH: "MATCH", NOT_MATCH: "NOT MATCH", IN: "IN", NOT_IN: "NOT IN",
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
COMPOUND_OPERATORS = { UNION: "UNION", UNION_ALL: "UNION ALL", INTERSECT: "INTERSECT", EXCEPT: "EXCEPT" }.freeze
|
|
22
|
+
JOIN_KINDS = { COMMA: ",", INNER: "JOIN", LEFT: "LEFT JOIN", RIGHT: "RIGHT JOIN", FULL: "FULL JOIN", CROSS: "CROSS JOIN" }.freeze
|
|
23
|
+
FOREIGN_KEY_ACTIONS = { CASCADE: "CASCADE", RESTRICT: "RESTRICT", SET_NULL: "SET NULL", SET_DEFAULT: "SET DEFAULT", NO_ACTION: "NO ACTION" }.freeze
|
|
24
|
+
|
|
25
|
+
# Operands of these need parentheses to keep their grouping when printed inside another expression
|
|
26
|
+
COMPOUND_EXPRESSIONS = [BinaryExpression, UnaryExpression, TernaryExpression, LikeExpression, CollationExpression].freeze
|
|
27
|
+
|
|
28
|
+
def self.print(node) = new.print(node)
|
|
29
|
+
|
|
30
|
+
def print(node)
|
|
31
|
+
case node
|
|
32
|
+
when nil then nil
|
|
33
|
+
when Array then node.map { |n| print(n) }.join(", ")
|
|
34
|
+
when Node then __send__(:"print_#{underscore(node.class.name.split("::").last)}", node)
|
|
35
|
+
# Expressions built by hand may hold plain values, which stand for the literal they spell
|
|
36
|
+
when LiteralValue then print(LiteralExpression.new(value: node))
|
|
37
|
+
else raise ArgumentError, "cannot print #{node.inspect}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def identifier(name)
|
|
42
|
+
name = name.to_s
|
|
43
|
+
return name if name.match?(BARE_IDENTIFIER) && bare_keyword_ok?(name.upcase.to_sym)
|
|
44
|
+
|
|
45
|
+
%("#{name.gsub('"', '""')}")
|
|
46
|
+
end
|
|
47
|
+
|
|
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)
|
|
50
|
+
|
|
51
|
+
def string(value) = "'#{value.to_s.gsub("'", "''")}'"
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def underscore(name) = name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
56
|
+
|
|
57
|
+
def join(*parts) = parts.flatten.reject { |p| !p || (p.respond_to?(:empty?) && p.empty?) }.join(" ")
|
|
58
|
+
|
|
59
|
+
def qualified(*names) = names.compact.map { |n| identifier(n) }.join(".")
|
|
60
|
+
|
|
61
|
+
def operand(node) = (COMPOUND_EXPRESSIONS.any? { |klass| klass === node }) ? "(#{print(node)})" : print(node)
|
|
62
|
+
|
|
63
|
+
def alias_of(node) = node.alias && "AS #{identifier(node.alias)}"
|
|
64
|
+
|
|
65
|
+
def list(items) = Array(items).map { |item| print(item) }.join(", ")
|
|
66
|
+
|
|
67
|
+
def parenthesized_names(names) = names && "(#{names.map { |n| identifier(n) }.join(", ")})"
|
|
68
|
+
|
|
69
|
+
# -- statements
|
|
70
|
+
|
|
71
|
+
def print_explain_statement(node) = join(node.query_plan ? "EXPLAIN QUERY PLAN" : "EXPLAIN", print(node.statement))
|
|
72
|
+
|
|
73
|
+
def print_select_statement(node)
|
|
74
|
+
join(print(node.with_clause), print(node.body), order_and_limit(node))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def order_and_limit(node)
|
|
78
|
+
[
|
|
79
|
+
node.orderings && "ORDER BY #{list(node.orderings)}",
|
|
80
|
+
node.limit && "LIMIT #{print(node.limit)}",
|
|
81
|
+
node.offset && "OFFSET #{print(node.offset)}",
|
|
82
|
+
]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def print_with_clause(node) = join("WITH", node.recursive && "RECURSIVE", list(node.ctes))
|
|
86
|
+
|
|
87
|
+
def print_common_table_expression(node)
|
|
88
|
+
materialized = { true => "MATERIALIZED", false => "NOT MATERIALIZED" }[node.materialized]
|
|
89
|
+
join(identifier(node.name) + (node.columns ? "(#{list(node.columns)})" : ""), "AS", materialized, "(#{print(node.select)})")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def print_compound_select(node)
|
|
93
|
+
join(print(node.left), COMPOUND_OPERATORS.fetch(node.operator), print(node.right))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_select_core(node)
|
|
97
|
+
join(
|
|
98
|
+
"SELECT", node.distinct && "DISTINCT", list(node.columns),
|
|
99
|
+
node.source && "FROM #{print(node.source)}",
|
|
100
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
101
|
+
node.groupings && "GROUP BY #{list(node.groupings)}",
|
|
102
|
+
node.having && "HAVING #{print(node.having)}",
|
|
103
|
+
node.windows && "WINDOW #{list(node.windows)}",
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def print_values_clause(node) = "VALUES #{list(node.rows)}"
|
|
108
|
+
def print_values_row(node) = "(#{list(node.values)})"
|
|
109
|
+
|
|
110
|
+
def print_result_column(node) = join(print(node.result), alias_of(node))
|
|
111
|
+
def print_star_result_column(node) = node.table ? "#{identifier(node.table)}.*" : "*"
|
|
112
|
+
|
|
113
|
+
def print_window_definition(node) = "#{identifier(node.name)} AS (#{window_body(node)})"
|
|
114
|
+
|
|
115
|
+
def window_body(node)
|
|
116
|
+
join(
|
|
117
|
+
node.base_window_name && identifier(node.base_window_name),
|
|
118
|
+
node.partitions && "PARTITION BY #{list(node.partitions)}",
|
|
119
|
+
node.orderings && "ORDER BY #{list(node.orderings)}",
|
|
120
|
+
print(node.frame),
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print_insert_statement(node)
|
|
125
|
+
verb = node.or_action ? ((node.insert_kw_tok == :REPLACE) ? "REPLACE" : "INSERT OR #{node.or_action}") : "INSERT"
|
|
126
|
+
join(
|
|
127
|
+
print(node.with_clause), verb, "INTO", print(node.table),
|
|
128
|
+
node.columns && "(#{list(node.columns)})",
|
|
129
|
+
node.default_values ? "DEFAULT VALUES" : print(node.select),
|
|
130
|
+
node.upserts&.map { |u| print(u) },
|
|
131
|
+
print(node.returning),
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def print_upsert_clause(node)
|
|
136
|
+
target = node.targets && "(#{list(node.targets)})"
|
|
137
|
+
action =
|
|
138
|
+
if node.action == :NOTHING
|
|
139
|
+
"DO NOTHING"
|
|
140
|
+
else
|
|
141
|
+
join("DO UPDATE SET", list(node.assignments), node.condition && "WHERE #{print(node.condition)}")
|
|
142
|
+
end
|
|
143
|
+
join("ON CONFLICT", target, node.target_condition && "WHERE #{print(node.target_condition)}", action)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def print_update_statement(node)
|
|
147
|
+
join(
|
|
148
|
+
print(node.with_clause), node.or_action ? "UPDATE OR #{node.or_action}" : "UPDATE", print(node.table),
|
|
149
|
+
"SET", list(node.assignments),
|
|
150
|
+
node.source && "FROM #{print(node.source)}",
|
|
151
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
152
|
+
print(node.returning), order_and_limit(node),
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def print_delete_statement(node)
|
|
157
|
+
join(
|
|
158
|
+
print(node.with_clause), "DELETE FROM", print(node.table),
|
|
159
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
160
|
+
print(node.returning), order_and_limit(node),
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def print_assignment(node)
|
|
165
|
+
target = (node.columns.size == 1 && !node.columns_lp?) ? print(node.columns[0]) : "(#{list(node.columns)})"
|
|
166
|
+
"#{target} = #{print(node.value)}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def print_returning_clause(node) = "RETURNING #{list(node.columns)}"
|
|
170
|
+
|
|
171
|
+
def print_create_table_statement(node)
|
|
172
|
+
head = join("CREATE", node.temporary && "TEMP", "TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table))
|
|
173
|
+
return join(head, "AS", print(node.select_statement)) if node.select_statement
|
|
174
|
+
|
|
175
|
+
definitions = list([*node.columns, *node.constraints])
|
|
176
|
+
join(head, "(#{definitions})", node.options && list(node.options))
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def print_create_view_statement(node)
|
|
180
|
+
join(
|
|
181
|
+
"CREATE", node.temporary && "TEMP", "VIEW", node.if_not_exists && "IF NOT EXISTS", print(node.view),
|
|
182
|
+
node.columns && "(#{list(node.columns)})", "AS", print(node.select),
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def print_create_index_statement(node)
|
|
187
|
+
join(
|
|
188
|
+
"CREATE", node.unique && "UNIQUE", "INDEX", node.if_not_exists && "IF NOT EXISTS", print(node.index),
|
|
189
|
+
"ON", identifier(node.table), "(#{list(node.columns)})",
|
|
190
|
+
node.condition && "WHERE #{print(node.condition)}",
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def print_create_virtual_table_statement(node)
|
|
195
|
+
arguments = node.arguments_lp? ? "(#{node.arguments.join(", ")})" : nil
|
|
196
|
+
join("CREATE VIRTUAL TABLE", node.if_not_exists && "IF NOT EXISTS", print(node.table), "USING", identifier(node.module_name) + arguments.to_s)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def print_create_trigger_statement(node)
|
|
200
|
+
event = node.columns ? "UPDATE OF #{list(node.columns)}" : node.event.to_s
|
|
201
|
+
join(
|
|
202
|
+
"CREATE", node.temporary && "TEMP", "TRIGGER", node.if_not_exists && "IF NOT EXISTS", print(node.trigger),
|
|
203
|
+
node.time&.to_s&.tr("_", " "), event, "ON", print(node.table),
|
|
204
|
+
node.for_each_row_kw? && "FOR EACH ROW", node.condition && "WHEN #{print(node.condition)}",
|
|
205
|
+
"BEGIN", node.statements.map { |s| "#{print(s)};" }, "END",
|
|
206
|
+
)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def print_alter_table_statement(node)
|
|
210
|
+
action =
|
|
211
|
+
case node.action
|
|
212
|
+
when :RENAME_TABLE then "RENAME TO #{identifier(node.new_name)}"
|
|
213
|
+
when :RENAME_COLUMN then "RENAME COLUMN #{identifier(node.column)} TO #{identifier(node.new_name)}"
|
|
214
|
+
when :ADD_COLUMN then "ADD COLUMN #{print(node.column_definition)}"
|
|
215
|
+
when :DROP_COLUMN then "DROP COLUMN #{identifier(node.column)}"
|
|
216
|
+
end
|
|
217
|
+
join("ALTER TABLE", print(node.table), action)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def print_drop_statement(node) = join("DROP", node.type.to_s, node.if_exists && "IF EXISTS", print(node.target))
|
|
221
|
+
|
|
222
|
+
def print_begin_statement(node) = join("BEGIN", node.type&.to_s)
|
|
223
|
+
def print_commit_statement(_node) = "COMMIT"
|
|
224
|
+
def print_rollback_statement(node) = join("ROLLBACK", node.savepoint && "TO SAVEPOINT #{identifier(node.savepoint)}")
|
|
225
|
+
def print_savepoint_statement(node) = "SAVEPOINT #{identifier(node.name)}"
|
|
226
|
+
def print_release_statement(node) = "RELEASE SAVEPOINT #{identifier(node.name)}"
|
|
227
|
+
|
|
228
|
+
def print_pragma_statement(node)
|
|
229
|
+
name = qualified(node.schema, node.name)
|
|
230
|
+
return "PRAGMA #{name}" if node.value.nil?
|
|
231
|
+
|
|
232
|
+
value =
|
|
233
|
+
case node.value
|
|
234
|
+
when Numeric then node.value.to_s
|
|
235
|
+
else (node.value_tk_tok == :STRING) ? string(node.value) : identifier(node.value)
|
|
236
|
+
end
|
|
237
|
+
(node.eq_tk_tok == :LP) ? "PRAGMA #{name}(#{value})" : "PRAGMA #{name} = #{value}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def print_attach_statement(node)
|
|
241
|
+
join("ATTACH DATABASE", print(node.file), "AS", print(node.schema), node.key && "KEY #{print(node.key)}")
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def print_detach_statement(node) = "DETACH DATABASE #{print(node.schema)}"
|
|
245
|
+
def print_analyze_statement(node) = join("ANALYZE", print(node.target))
|
|
246
|
+
def print_reindex_statement(node) = join("REINDEX", print(node.target))
|
|
247
|
+
def print_vacuum_statement(node) = join("VACUUM", node.schema && identifier(node.schema), node.into && "INTO #{print(node.into)}")
|
|
248
|
+
|
|
249
|
+
# -- FROM
|
|
250
|
+
|
|
251
|
+
def print_table_name(node)
|
|
252
|
+
indexed = node.indexed_by ? "INDEXED BY #{identifier(node.indexed_by)}" : (node.not_indexed && "NOT INDEXED")
|
|
253
|
+
join(qualified(node.schema, node.table), alias_of(node), indexed)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def print_table_function(node)
|
|
257
|
+
join("#{qualified(node.schema, node.function)}(#{list(node.arguments)})", alias_of(node))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def print_subquery_source(node) = join("(#{print(node.select)})", alias_of(node))
|
|
261
|
+
def print_parenthetical_source(node) = join("(#{print(node.source)})", alias_of(node))
|
|
262
|
+
|
|
263
|
+
def print_join_clause(node)
|
|
264
|
+
node.joins.reduce(print(node.left)) do |sql, each|
|
|
265
|
+
operator = print(each.operator)
|
|
266
|
+
right = join(print(each.source), print(each.constraint))
|
|
267
|
+
(operator == ",") ? "#{sql}, #{right}" : "#{sql} #{operator} #{right}"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def print_join_operator(node) = join(node.natural && "NATURAL", JOIN_KINDS.fetch(node.kind))
|
|
272
|
+
def print_join_on_constraint(node) = "ON #{print(node.expression)}"
|
|
273
|
+
def print_join_using_constraint(node) = "USING (#{list(node.columns)})"
|
|
274
|
+
|
|
275
|
+
# -- expressions
|
|
276
|
+
|
|
277
|
+
def print_column_name(node) = qualified(node.schema, node.table, node.column)
|
|
278
|
+
|
|
279
|
+
def print_literal_expression(node)
|
|
280
|
+
# Numbers keep their spelling, so 0x1F stays hex and 1e3 stays an exponent
|
|
281
|
+
return node.value_tk_src if node.value_tk? && node.value_tk_spn.last == :numeric
|
|
282
|
+
|
|
283
|
+
case (value = node.value)
|
|
284
|
+
when nil then "NULL"
|
|
285
|
+
when true then "TRUE"
|
|
286
|
+
when false then "FALSE"
|
|
287
|
+
when Symbol then value.to_s
|
|
288
|
+
when Integer then value.to_s
|
|
289
|
+
when Float
|
|
290
|
+
# SQLite has no literal for NaN or infinity
|
|
291
|
+
value.finite? ? value.to_s : raise(ArgumentError, "cannot print #{value}")
|
|
292
|
+
when String
|
|
293
|
+
(value.encoding == Encoding::BINARY) ? "X'#{value.unpack1("H*").upcase}'" : string(value)
|
|
294
|
+
# A Rational would print as integer division (1/3), a Complex as nothing SQL can read
|
|
295
|
+
else raise ArgumentError, "cannot print #{value.inspect}"
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def print_variable(node) = node.name
|
|
300
|
+
|
|
301
|
+
def print_binary_expression(node)
|
|
302
|
+
"#{operand(node.left)} #{BINARY_OPERATORS.fetch(node.operator)} #{operand(node.right)}"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def print_unary_expression(node)
|
|
306
|
+
case node.operator
|
|
307
|
+
when :NOT then "NOT #{operand(node.operand)}"
|
|
308
|
+
when :NEGATE then "-#{unary_operand(node.operand)}"
|
|
309
|
+
when :IDENTITY then "+#{unary_operand(node.operand)}"
|
|
310
|
+
when :INVERT then "~#{unary_operand(node.operand)}"
|
|
311
|
+
when :IS_NULL then "#{operand(node.operand)} ISNULL"
|
|
312
|
+
when :NOT_NULL then "#{operand(node.operand)} NOTNULL"
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Two minus signs in a row would start a comment
|
|
317
|
+
def unary_operand(node)
|
|
318
|
+
sql = operand(node)
|
|
319
|
+
sql.start_with?("-", "+") ? "(#{sql})" : sql
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def print_ternary_expression(node)
|
|
323
|
+
operator = (node.operator == :NOT_BETWEEN) ? "NOT BETWEEN" : "BETWEEN"
|
|
324
|
+
"#{operand(node.left)} #{operator} #{operand(node.middle)} AND #{operand(node.right)}"
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def print_like_expression(node)
|
|
328
|
+
operator = (node.operator == :NOT_LIKE) ? "NOT LIKE" : "LIKE"
|
|
329
|
+
join("#{operand(node.left)} #{operator} #{operand(node.right)}", node.escape && "ESCAPE #{operand(node.escape)}")
|
|
330
|
+
end
|
|
331
|
+
alias_method :print_not_like_expression, :print_like_expression
|
|
332
|
+
|
|
333
|
+
def print_collation_expression(node) = "#{operand(node.expression)} COLLATE #{identifier(node.sequence)}"
|
|
334
|
+
|
|
335
|
+
def print_parenthetical_expression(node) = "(#{print(node.contents)})"
|
|
336
|
+
|
|
337
|
+
def print_cast_expression(node) = "CAST(#{print(node.expression)} AS #{print(node.as)})"
|
|
338
|
+
|
|
339
|
+
def print_case_expression(node)
|
|
340
|
+
join("CASE", node.predicate && print(node.predicate), list_of(node.conditions), node.else_clause && "ELSE #{print(node.else_clause)}", "END")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def list_of(items) = items.map { |item| print(item) }
|
|
344
|
+
|
|
345
|
+
def print_case_condition(node) = "WHEN #{print(node.predicate)} THEN #{print(node.consequence)}"
|
|
346
|
+
|
|
347
|
+
def print_function_reference(node)
|
|
348
|
+
join(
|
|
349
|
+
"#{qualified(node.schema, node.function)}(#{print(node.arguments)})",
|
|
350
|
+
node.within_group && "WITHIN GROUP (ORDER BY #{print(node.within_group)})",
|
|
351
|
+
node.filter_clause && "FILTER (WHERE #{print(node.filter_clause)})",
|
|
352
|
+
print(node.over_clause),
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def print_function_arguments(node)
|
|
357
|
+
join(node.distinct && "DISTINCT", list(node.expressions), node.order_by && "ORDER BY #{list(node.order_by)}")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def print_star_function_argument(_node) = "*"
|
|
361
|
+
def print_empty_function_argument(_node) = ""
|
|
362
|
+
|
|
363
|
+
def print_over_clause(node)
|
|
364
|
+
return "OVER #{identifier(node.window_name)}" if node.window_name
|
|
365
|
+
|
|
366
|
+
"OVER (#{window_body(node)})"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def print_frame_spec(node)
|
|
370
|
+
type = node.type.to_s.upcase
|
|
371
|
+
exclude = node.exclude && "EXCLUDE #{node.exclude.to_s.upcase.tr("_", " ")}"
|
|
372
|
+
if node.ending_boundary
|
|
373
|
+
join(type, "BETWEEN", print(node.starting_boundary), "AND", print(node.ending_boundary), exclude)
|
|
374
|
+
else
|
|
375
|
+
join(type, print(node.starting_boundary), exclude)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def print_frame_boundary(node)
|
|
380
|
+
type = node.type.to_s.upcase.tr("_", " ")
|
|
381
|
+
return type if type == "CURRENT ROW"
|
|
382
|
+
|
|
383
|
+
value = node.value
|
|
384
|
+
join((Symbol === value || String === value) ? value.to_s.upcase : print(value), type)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def print_subquery_expression(node) = "(#{print(node.select)})"
|
|
388
|
+
def print_exists_expression(node) = "EXISTS (#{print(node.select)})"
|
|
389
|
+
|
|
390
|
+
def print_raise_expression(node)
|
|
391
|
+
return "RAISE(IGNORE)" if node.type == :IGNORE
|
|
392
|
+
|
|
393
|
+
"RAISE(#{node.type}, #{print(node.error_message)})"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def print_ordering_term(node)
|
|
397
|
+
join(
|
|
398
|
+
print(node.expression), node.collation && "COLLATE #{identifier(node.collation)}",
|
|
399
|
+
node.direction&.to_s&.upcase, node.nulls && "NULLS #{node.nulls.to_s.upcase}",
|
|
400
|
+
)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# -- column and table definitions
|
|
404
|
+
|
|
405
|
+
def print_column_definition(node) = join(identifier(node.name), print(node.type), list_of(Array(node.constraints)))
|
|
406
|
+
def print_column_type(node) = node.text
|
|
407
|
+
|
|
408
|
+
def constraint_name(node) = node.name && "CONSTRAINT #{identifier(node.name)}"
|
|
409
|
+
|
|
410
|
+
def print_primary_key_column_constraint(node)
|
|
411
|
+
join(constraint_name(node), "PRIMARY KEY", node.direction&.to_s, print(node.conflict_clause), node.autoincrement && "AUTOINCREMENT")
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def print_not_null_column_constraint(node) = join(constraint_name(node), "NOT NULL", print(node.conflict_clause))
|
|
415
|
+
def print_null_column_constraint(node) = join(constraint_name(node), "NULL", print(node.conflict_clause))
|
|
416
|
+
def print_unique_column_constraint(node) = join(constraint_name(node), "UNIQUE", print(node.conflict_clause))
|
|
417
|
+
def print_check_column_constraint(node) = join(constraint_name(node), "CHECK (#{print(node.expression)})", print(node.conflict_clause))
|
|
418
|
+
def print_collate_column_constraint(node) = join(constraint_name(node), "COLLATE #{identifier(node.collation)}")
|
|
419
|
+
def print_foreign_key_column_constraint(node) = join(constraint_name(node), print(node.foreign_key_clause))
|
|
420
|
+
def print_no_op_column_constraint(node) = constraint_name(node)
|
|
421
|
+
|
|
422
|
+
def print_default_column_constraint(node)
|
|
423
|
+
return join(constraint_name(node), "DEFAULT #{node.value_tk_src}") if node.value_tk?
|
|
424
|
+
|
|
425
|
+
value = node.value
|
|
426
|
+
sql =
|
|
427
|
+
case value
|
|
428
|
+
when Node then "(#{print(value)})"
|
|
429
|
+
else print(LiteralExpression.new(value:))
|
|
430
|
+
end
|
|
431
|
+
join(constraint_name(node), "DEFAULT #{sql}")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def print_generated_as_column_constraint(node)
|
|
435
|
+
join(constraint_name(node), "GENERATED ALWAYS AS (#{print(node.expression)})", node.type&.to_s)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def print_conflict_clause(node) = "ON CONFLICT #{node.resolution}"
|
|
439
|
+
|
|
440
|
+
def print_primary_key_table_constraint(node)
|
|
441
|
+
join(constraint_name(node), "PRIMARY KEY (#{list(node.columns)}#{" AUTOINCREMENT" if node.autoincrement})", print(node.conflict_clause))
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
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)})")
|
|
446
|
+
def print_no_op_table_constraint(node) = constraint_name(node)
|
|
447
|
+
|
|
448
|
+
def print_foreign_key_table_constraint(node)
|
|
449
|
+
join(constraint_name(node), "FOREIGN KEY (#{list(node.columns)})", print(node.foreign_key_clause))
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def print_foreign_key_clause(node)
|
|
453
|
+
deferrable = node.deferrable_span? ? node.deferrable_span_tok : (node.deferred && "DEFERRABLE INITIALLY DEFERRED")
|
|
454
|
+
join(
|
|
455
|
+
"REFERENCES", identifier(node.table), node.columns && "(#{list(node.columns)})",
|
|
456
|
+
list_of(Array(node.actions)), list_of(Array(node.match_clauses)),
|
|
457
|
+
deferrable && Array(deferrable).join(" "),
|
|
458
|
+
)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def print_foreign_key_action(node) = "ON #{node.trigger.to_s.upcase} #{FOREIGN_KEY_ACTIONS.fetch(node.action)}"
|
|
462
|
+
def print_match_clause(node) = "MATCH #{node.type}"
|
|
463
|
+
|
|
464
|
+
def print_indexed_column(node)
|
|
465
|
+
join(identifier(node.name), node.collation && "COLLATE #{identifier(node.collation)}", node.direction&.to_s)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def print_strict_table_option(_node) = "STRICT"
|
|
469
|
+
def print_without_rowid_table_option(_node) = "WITHOUT ROWID"
|
|
470
|
+
end
|
|
471
|
+
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
|
data/lib/plume/union.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# Literal's union looks every value up in a Set of its plain values before trying its types,
|
|
5
|
+
# and from Ruby 4 on Set#include? hashes the value even when the set is empty. Hashing a node
|
|
6
|
+
# (or an array of them) hashes the whole subtree, so type-checking a tree as it is built would
|
|
7
|
+
# be quadratic. This union only looks up values that could be one of the plain ones
|
|
8
|
+
class Union
|
|
9
|
+
PLAIN = [String, Symbol, Integer, Float, Rational, Complex, TrueClass, FalseClass, NilClass].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(*members)
|
|
12
|
+
plain, types = members.partition { |member| PLAIN.include?(member.class) }
|
|
13
|
+
@plain = plain.to_set.freeze
|
|
14
|
+
@types = types.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def ===(value)
|
|
19
|
+
(PLAIN.include?(value.class) && @plain.include?(value)) || @types.any? { |type| type === value }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def inspect = "_Union(#{[*@plain, *@types].map(&:inspect).join(", ")})"
|
|
23
|
+
|
|
24
|
+
module Types
|
|
25
|
+
def _Union(*members) = Union.new(*members)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
data/lib/plume/version.rb
CHANGED