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,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# Static analysis on top of the AST. The first question it answers is "which tables and
|
|
5
|
+
# columns would this statement read?", see `Plume.reads`; the second is what it would
|
|
6
|
+
# write, see `Plume.writes`.
|
|
7
|
+
module Analysis
|
|
8
|
+
autoload :Resolver, "plume/analysis/resolver"
|
|
9
|
+
|
|
10
|
+
# A read or write of one column, or of a table as a whole when `column` is nil - which is
|
|
11
|
+
# what `SELECT count(*) FROM t` or `DELETE FROM t` amount to.
|
|
12
|
+
#
|
|
13
|
+
# For reads, `usage` is `:projected` when the value itself can leave the statement - in the
|
|
14
|
+
# result rows, through RETURNING, or copied into a column being written - and `:referenced`
|
|
15
|
+
# when it is only used to filter, join, sort or group. For writes it is `:insert`,
|
|
16
|
+
# `:update` or `:delete`.
|
|
17
|
+
Reference = Data.define(:schema, :table, :column, :usage, :node) do
|
|
18
|
+
def table_key = Analysis.table_key(schema, table)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Unresolved = Data.define(:name, :reason, :node) do
|
|
22
|
+
def message
|
|
23
|
+
excerpt = Analysis.excerpt(node)
|
|
24
|
+
excerpt ? "#{reason}: #{name}\n#{excerpt}" : "#{reason}: #{name}"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The source line `node` sits on with a caret under it, for error messages
|
|
29
|
+
def self.excerpt(node)
|
|
30
|
+
loc = node&.location
|
|
31
|
+
Plume.excerpt(node.full_source, *loc) if loc && node.full_source
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Error < StandardError
|
|
35
|
+
attr_reader :unresolved
|
|
36
|
+
|
|
37
|
+
def initialize(message = nil, unresolved: [])
|
|
38
|
+
@unresolved = unresolved
|
|
39
|
+
super(message || unresolved.map(&:message).join("\n"))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Tables in the main schema are keyed by their bare name, any other schema
|
|
44
|
+
# (temp, attached databases) is spelled out as "schema.table"
|
|
45
|
+
def self.table_key(schema, table)
|
|
46
|
+
(schema.nil? || schema.casecmp("main")&.zero?) ? table : "#{schema}.#{table}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class References
|
|
50
|
+
# `ctes` are the names the statement's WITH clauses define, `aliases` maps each table alias
|
|
51
|
+
# to the table, view, CTE or function it stands for - all spelled as written, and none from
|
|
52
|
+
# inside the views read. Reads and Writes of one statement share them
|
|
53
|
+
attr_reader :statement, :references, :unresolved, :ctes, :aliases
|
|
54
|
+
|
|
55
|
+
def initialize(statement, unresolved = [], ctes = Set.new, aliases = {})
|
|
56
|
+
@statement = statement
|
|
57
|
+
@references = []
|
|
58
|
+
@unresolved = unresolved
|
|
59
|
+
@ctes = ctes
|
|
60
|
+
@aliases = aliases
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Every base table touched, including ones touched only as a whole
|
|
64
|
+
def tables(usage = nil) = @references.select { |r| usage.nil? || r.usage == usage }.to_set(&:table_key)
|
|
65
|
+
|
|
66
|
+
# `{ "table" => Set["column", …] }`, optionally narrowed to one usage
|
|
67
|
+
def columns(usage = nil)
|
|
68
|
+
@references.each_with_object({}) do |ref, acc|
|
|
69
|
+
next if ref.column.nil? || (usage && ref.usage != usage)
|
|
70
|
+
|
|
71
|
+
(acc[ref.table_key] ||= Set.new) << ref.column
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def resolved? = @unresolved.empty?
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
"#<#{self.class.name} columns=#{columns.transform_values(&:to_a).inspect} unresolved=#{@unresolved.size}>"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Reads < References
|
|
83
|
+
# `functions` are the table-valued ones in FROM, `calls` every other function called -
|
|
84
|
+
# including those LIKE, GLOB, REGEXP, MATCH, `->`, `->>` and CURRENT_* call - as SQLite's
|
|
85
|
+
# authorizer reports them
|
|
86
|
+
attr_reader :views, :functions, :calls
|
|
87
|
+
|
|
88
|
+
def initialize(...)
|
|
89
|
+
super
|
|
90
|
+
@views = Set.new
|
|
91
|
+
@functions = Set.new
|
|
92
|
+
@calls = Set.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def projected = columns(:projected)
|
|
96
|
+
def referenced = columns(:referenced)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Writes < References
|
|
100
|
+
# INSERT without a column list writes every column; without a catalog that is "*"
|
|
101
|
+
def inserted = columns(:insert)
|
|
102
|
+
def updated = columns(:update)
|
|
103
|
+
def deleted = tables(:delete)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# What a run of statements reads, writes and does, as `Plume.summary` gives it. `accesses` has one
|
|
107
|
+
# entry per statement, the rest is merged across them
|
|
108
|
+
class Summary
|
|
109
|
+
attr_reader :statements, :accesses
|
|
110
|
+
|
|
111
|
+
def initialize(statements, accesses)
|
|
112
|
+
@statements = statements
|
|
113
|
+
@accesses = accesses
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Each statement's node class, like "SelectStatement" or "CreateTableStatement"
|
|
117
|
+
def statement_types = @statements.map { |s| s.class.name.delete_prefix("Plume::") }
|
|
118
|
+
|
|
119
|
+
# Every base table read or written, or only those touched in one way: `:projected`,
|
|
120
|
+
# `:referenced`, `:insert`, `:update` or `:delete`
|
|
121
|
+
def tables(usage = nil) = each_side.reduce(Set.new) { |all, refs| all | refs.tables(usage) }
|
|
122
|
+
|
|
123
|
+
def reads = @accesses.map(&:reads)
|
|
124
|
+
def writes = @accesses.map(&:writes)
|
|
125
|
+
def actions = @accesses.flat_map(&:actions)
|
|
126
|
+
def views = union(reads, :views)
|
|
127
|
+
def functions = union(reads, :functions)
|
|
128
|
+
def calls = union(reads, :calls)
|
|
129
|
+
def ctes = union(reads, :ctes)
|
|
130
|
+
def aliases = reads.reduce({}) { |all, r| all.merge(r.aliases) }
|
|
131
|
+
def unresolved = reads.flat_map(&:unresolved)
|
|
132
|
+
def resolved? = unresolved.empty?
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def each_side = reads + writes
|
|
137
|
+
def union(list, name) = list.reduce(Set.new) { |all, r| all | r.public_send(name) }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Something a statement does besides reading and writing rows, named as SQLite's authorizer
|
|
141
|
+
# names it: `action` is its action code as a symbol (`:create_table`, `:drop_temp_index`,
|
|
142
|
+
# `:alter_table`, `:pragma`, `:transaction`…), and `name`, `detail` and `schema` are the
|
|
143
|
+
# arguments the authorizer gets with it - for `:create_index`, the index, its table and the schema
|
|
144
|
+
Action = Data.define(:action, :name, :detail, :schema, :node)
|
|
145
|
+
|
|
146
|
+
Access = Data.define(:reads, :writes, :actions) do
|
|
147
|
+
def unresolved = reads.unresolved
|
|
148
|
+
def resolved? = reads.resolved?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
module Analysis
|
|
153
|
+
# Which tables and columns `sql` - a single SELECT, INSERT, UPDATE or DELETE - would read.
|
|
154
|
+
# Instead of SQL you can pass what you already parsed: a statement node, or the array
|
|
155
|
+
# `Plume.parse` returns, so the same SQL does not get parsed twice.
|
|
156
|
+
#
|
|
157
|
+
# Without a catalog, columns of tables cannot be enumerated: `SELECT *` comes back as a read
|
|
158
|
+
# of the column "*", and an unqualified column with more than one table in scope is ambiguous.
|
|
159
|
+
#
|
|
160
|
+
# With `strict: true` (the default) anything that cannot be attributed raises
|
|
161
|
+
# `Plume::Analysis::Error`; with `strict: false` it is listed in `Reads#unresolved` instead.
|
|
162
|
+
def self.reads(sql, catalog: nil, strict: true) = access(sql, catalog:, strict:).reads
|
|
163
|
+
|
|
164
|
+
# Which tables and columns an INSERT, UPDATE or DELETE would write, with the same arguments
|
|
165
|
+
# and strictness as `reads`. A SELECT writes nothing. The triggers a statement fires are not looked into, so rows
|
|
166
|
+
# they write elsewhere - or the rows a REPLACE deletes - are not reported.
|
|
167
|
+
def self.writes(sql, catalog: nil, strict: true) = access(sql, catalog:, strict:).writes
|
|
168
|
+
|
|
169
|
+
# Like `reads`, but also reports what an INSERT, UPDATE or DELETE writes, and in `actions` what
|
|
170
|
+
# any other statement does - creating, dropping and altering things, pragmas, transactions. A
|
|
171
|
+
# CREATE TRIGGER is analyzed for what its WHEN and body do each time it fires, a CREATE TABLE … AS
|
|
172
|
+
# reads what its SELECT reads, a CREATE INDEX reads what it indexes, DROP TABLE deletes the rows
|
|
173
|
+
def self.access(sql, catalog: nil, strict: true)
|
|
174
|
+
statements = statements(sql)
|
|
175
|
+
raise Error, "expected exactly one statement, got #{statements.size}" unless statements.size == 1
|
|
176
|
+
|
|
177
|
+
statement = statements.first
|
|
178
|
+
access = begin
|
|
179
|
+
Resolver.new(catalog:).analyze(statement)
|
|
180
|
+
rescue *STACK_EXHAUSTED
|
|
181
|
+
# As with parsing, how deep the resolver can go depends on the stack the caller has left
|
|
182
|
+
raise Error, "statement nested too deeply"
|
|
183
|
+
end
|
|
184
|
+
raise Error.new(unresolved: access.unresolved) if strict && !access.resolved?
|
|
185
|
+
|
|
186
|
+
access
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Reads and writes for any number of statements, a migration or a whole log line - with the
|
|
190
|
+
# same arguments as `access`. Each statement sees the schema as the statements before it left
|
|
191
|
+
# it: a copy of the catalog is changed as they create, alter and drop things, and the catalog
|
|
192
|
+
# passed in stays as it was
|
|
193
|
+
def self.summary(sql, catalog: nil, strict: true)
|
|
194
|
+
statements = statements(sql)
|
|
195
|
+
catalog = catalog&.dup
|
|
196
|
+
accesses = statements.map do |statement|
|
|
197
|
+
access(statement, catalog:, strict:).tap { catalog&.apply(statement) }
|
|
198
|
+
end
|
|
199
|
+
Summary.new(statements, accesses)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.statements(sql)
|
|
203
|
+
case sql
|
|
204
|
+
when Node then [sql]
|
|
205
|
+
when Array then sql
|
|
206
|
+
else Parser.new(sql).parse
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
private_class_method :statements
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# **[SQLite Docs](https://www.sqlite.org/lang_altertable.html)**
|
|
5
|
+
#
|
|
6
|
+
# `action` is :RENAME_TABLE, :RENAME_COLUMN, :ADD_COLUMN, :DROP_COLUMN, :ADD_CONSTRAINT (a CHECK),
|
|
7
|
+
# :DROP_CONSTRAINT, :SET_NOT_NULL or :DROP_NOT_NULL
|
|
8
|
+
class AlterTableStatement < Node
|
|
9
|
+
Action = _Union(
|
|
10
|
+
:RENAME_TABLE, :RENAME_COLUMN, :ADD_COLUMN, :DROP_COLUMN,
|
|
11
|
+
:ADD_CONSTRAINT, :DROP_CONSTRAINT, :SET_NOT_NULL, :DROP_NOT_NULL,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
token :alter_table_kw
|
|
15
|
+
token :action_kw # RENAME [COLUMN], ADD [COLUMN], DROP [COLUMN], DROP CONSTRAINT or ALTER [COLUMN]
|
|
16
|
+
token :column_tk
|
|
17
|
+
token :to_kw
|
|
18
|
+
token :new_name_tk
|
|
19
|
+
token :constraint_name_tk
|
|
20
|
+
token :not_null_kw # SET NOT NULL or DROP NOT NULL
|
|
21
|
+
|
|
22
|
+
node :table, TableName
|
|
23
|
+
attribute :action, Action
|
|
24
|
+
node :column_definition, ColumnDefinition
|
|
25
|
+
node :constraint, CheckTableConstraint
|
|
26
|
+
node :conflict_clause, ConflictClause
|
|
27
|
+
attribute :column, Stringy
|
|
28
|
+
attribute :new_name, Stringy
|
|
29
|
+
attribute :constraint_name, Stringy
|
|
30
|
+
|
|
31
|
+
def self.new(*, table:, action:, **) = super
|
|
32
|
+
def self.concrete(*, alter_table_kw:, table:, action:, **) = super
|
|
33
|
+
|
|
34
|
+
def action = @action
|
|
35
|
+
def column = (@column == LiteralNil) ? column_tk_val : @column
|
|
36
|
+
def new_name = (@new_name == LiteralNil) ? new_name_tk_val : @new_name
|
|
37
|
+
# The constraint dropped, or added - which keeps its name on `constraint`
|
|
38
|
+
def constraint_name = (@constraint_name == LiteralNil) ? (constraint_name_tk_val || constraint&.name) : @constraint_name
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# **[SQLite Docs](https://www.sqlite.org/lang_analyze.html)**
|
|
5
|
+
#
|
|
6
|
+
# `target` names a schema, table or index - SQLite works out which at run time
|
|
7
|
+
class AnalyzeStatement < Node
|
|
8
|
+
token :analyze_kw
|
|
9
|
+
|
|
10
|
+
node :target, TableName
|
|
11
|
+
|
|
12
|
+
def self.new(*, **) = super
|
|
13
|
+
def self.concrete(*, analyze_kw:, **) = super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# One `col = expr` or `(a, b) = expr` from a SET list
|
|
5
|
+
class Assignment < Node
|
|
6
|
+
token :columns_lp
|
|
7
|
+
token :columns_rp
|
|
8
|
+
token :eq_tk
|
|
9
|
+
|
|
10
|
+
nodes :columns, ColumnName
|
|
11
|
+
node :value, Expression
|
|
12
|
+
|
|
13
|
+
def self.new(*, columns:, value:, **) = super
|
|
14
|
+
def self.concrete(*, columns:, value:, **) = super
|
|
15
|
+
|
|
16
|
+
def column_names = columns.map(&:column)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# **[SQLite Docs](https://www.sqlite.org/lang_attach.html)**
|
|
5
|
+
class AttachStatement < Node
|
|
6
|
+
token :attach_kw # ATTACH or ATTACH DATABASE
|
|
7
|
+
token :as_kw
|
|
8
|
+
token :key_kw
|
|
9
|
+
|
|
10
|
+
node :file, Expression
|
|
11
|
+
node :schema, Expression
|
|
12
|
+
node :key, Expression
|
|
13
|
+
|
|
14
|
+
def self.new(*, file:, schema:, **) = super
|
|
15
|
+
def self.concrete(*, attach_kw:, file:, schema:, **) = super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# **[SQLite Docs](https://www.sqlite.org/lang_transaction.html)**
|
|
5
|
+
class BeginStatement < Node
|
|
6
|
+
token :begin_kw
|
|
7
|
+
token :type_kw
|
|
8
|
+
token :transaction_kw
|
|
9
|
+
token :name_tk
|
|
10
|
+
|
|
11
|
+
def self.new(*, **) = super
|
|
12
|
+
def self.concrete(*, begin_kw:, **) = super
|
|
13
|
+
|
|
14
|
+
# :DEFERRED, :IMMEDIATE, :EXCLUSIVE, or nil for SQLite's default (deferred)
|
|
15
|
+
inspectable def type = type_kw_tok
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class BinaryExpression < Node
|
|
5
|
+
TOKEN_TO_OPERATOR = {
|
|
6
|
+
:AND => :ALL,
|
|
7
|
+
:OR => :ANY,
|
|
8
|
+
:CONCAT => :CONCAT,
|
|
9
|
+
:PTR1 => :EXTRACT,
|
|
10
|
+
:PTR2 => :RETRIEVE,
|
|
11
|
+
:STAR => :MULTIPLY,
|
|
12
|
+
:SLASH => :DIVIDE,
|
|
13
|
+
:REM => :MODULO,
|
|
14
|
+
:PLUS => :ADD,
|
|
15
|
+
:MINUS => :SUB,
|
|
16
|
+
:BITAND => :BIT_AND,
|
|
17
|
+
:BITOR => :BIT_OR,
|
|
18
|
+
:LSHIFT => :BIT_LSHIFT,
|
|
19
|
+
:RSHIFT => :BIT_RSHIFT,
|
|
20
|
+
:LT => :BELOW,
|
|
21
|
+
:GT => :ABOVE,
|
|
22
|
+
:LE => :ATMOST,
|
|
23
|
+
:GE => :ATLEAST,
|
|
24
|
+
:EQ => :EQUALS,
|
|
25
|
+
:NE => :NOT_EQUALS,
|
|
26
|
+
:IS => :IS,
|
|
27
|
+
:GLOB => :GLOB,
|
|
28
|
+
:REGEXP => :REGEXP,
|
|
29
|
+
:MATCH => :MATCH,
|
|
30
|
+
:IN => :IN,
|
|
31
|
+
[:IS, :NOT, :DISTINCT, :FROM].freeze => :IS,
|
|
32
|
+
[:IS, :DISTINCT, :FROM].freeze => :IS_NOT,
|
|
33
|
+
[:IS, :NOT].freeze => :IS_NOT,
|
|
34
|
+
[:IS].freeze => :IS,
|
|
35
|
+
[:GLOB].freeze => :GLOB,
|
|
36
|
+
[:REGEXP].freeze => :REGEXP,
|
|
37
|
+
[:MATCH].freeze => :MATCH,
|
|
38
|
+
[:IN].freeze => :IN,
|
|
39
|
+
[:NOT, :GLOB].freeze => :NOT_GLOB,
|
|
40
|
+
[:NOT, :REGEXP].freeze => :NOT_REGEXP,
|
|
41
|
+
[:NOT, :MATCH].freeze => :NOT_MATCH,
|
|
42
|
+
[:NOT, :IN].freeze => :NOT_IN,
|
|
43
|
+
}.freeze
|
|
44
|
+
Operator = _Union(*TOKEN_TO_OPERATOR.values)
|
|
45
|
+
|
|
46
|
+
token :operator_tk # required
|
|
47
|
+
token :left_tk # required
|
|
48
|
+
token :right_tk # required
|
|
49
|
+
|
|
50
|
+
attribute :operator, Operator # required
|
|
51
|
+
attribute :left, Expression # required
|
|
52
|
+
attribute :right, Expression # required
|
|
53
|
+
|
|
54
|
+
def self.new(*, operator:, left:, right:, **) = super
|
|
55
|
+
def self.concrete(*, operator_tk:, **) = super
|
|
56
|
+
|
|
57
|
+
# Parser may pass either tokens or attributes, so
|
|
58
|
+
# require none in `concrete` signature but ensure presence here.
|
|
59
|
+
def after_initialize
|
|
60
|
+
if @left.nil? && @left_tk.nil?
|
|
61
|
+
raise ArgumentError.new("missing either: :left, :left_tk")
|
|
62
|
+
elsif @right.nil? && @right_tk.nil?
|
|
63
|
+
raise ArgumentError.new("missing either: :right, :right_tk")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def operator = (@operator == LiteralNil) ? TOKEN_TO_OPERATOR[operator_tk_tok] : @operator
|
|
68
|
+
def left = (@left == LiteralNil) ? left_tk_val : @left
|
|
69
|
+
def right = (@right == LiteralNil) ? right_tk_val : @right
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CaseCondition < Node
|
|
5
|
+
token :when_kw
|
|
6
|
+
token :predicate_tk
|
|
7
|
+
token :then_kw
|
|
8
|
+
token :consequence_tk
|
|
9
|
+
|
|
10
|
+
attribute :predicate, Expression
|
|
11
|
+
attribute :consequence, Expression
|
|
12
|
+
|
|
13
|
+
def self.new(*, predicate:, consequence:, **) = super
|
|
14
|
+
def self.concrete(*, when_kw:, then_kw:, **) = super
|
|
15
|
+
|
|
16
|
+
# Parser may pass either tokens or attributes, so
|
|
17
|
+
# require none in `concrete` signature but ensure presence here.
|
|
18
|
+
def after_initialize
|
|
19
|
+
if @predicate.nil? && @predicate_tk.nil?
|
|
20
|
+
raise ArgumentError.new("missing either: :predicate, :predicate_tk")
|
|
21
|
+
elsif @consequence.nil? && @consequence_tk.nil?
|
|
22
|
+
raise ArgumentError.new("missing either: :consequence, :consequence_tk")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def predicate = (@predicate == LiteralNil) ? predicate_tk_val : @predicate
|
|
27
|
+
def consequence = (@consequence == LiteralNil) ? consequence_tk_val : @consequence
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CaseExpression < Node
|
|
5
|
+
token :case_kw
|
|
6
|
+
token :predicate_tk
|
|
7
|
+
token :else_kw
|
|
8
|
+
token :else_clause_tk
|
|
9
|
+
token :end_kw
|
|
10
|
+
|
|
11
|
+
attribute :predicate, Expression
|
|
12
|
+
attribute :else_clause, Expression
|
|
13
|
+
|
|
14
|
+
nodes :conditions, CaseCondition
|
|
15
|
+
|
|
16
|
+
def self.new(*, conditions:, **) = super
|
|
17
|
+
def self.concrete(*, case_kw:, conditions:, end_kw:, **) = super
|
|
18
|
+
|
|
19
|
+
def predicate = (@predicate == LiteralNil) ? predicate_tk_val : @predicate
|
|
20
|
+
def else_clause = (@else_clause == LiteralNil) ? else_clause_tk_val : @else_clause
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CastExpression < Node
|
|
5
|
+
token :cast_kw
|
|
6
|
+
token :expression_lp
|
|
7
|
+
token :as_kw
|
|
8
|
+
token :expression_rp
|
|
9
|
+
|
|
10
|
+
node :expression, Expression
|
|
11
|
+
node :as, ColumnType
|
|
12
|
+
|
|
13
|
+
def self.new(*, expression:, as:, **) = super
|
|
14
|
+
def self.concrete(*, cast_kw:, expression_lp:, expression:, as_kw:, as:, expression_rp:, **) = super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CheckColumnConstraint < ColumnConstraint
|
|
5
|
+
token :constraint_kw
|
|
6
|
+
token :name_tk
|
|
7
|
+
token :check_kw
|
|
8
|
+
token :check_lp
|
|
9
|
+
token :check_rp
|
|
10
|
+
|
|
11
|
+
attribute :name, Stringy
|
|
12
|
+
node :expression, Expression
|
|
13
|
+
node :conflict_clause, ConflictClause
|
|
14
|
+
|
|
15
|
+
def self.new(*, expression:, **) = super
|
|
16
|
+
def self.concrete(*, check_kw:, check_lp:, expression:, check_rp:, **) = super
|
|
17
|
+
|
|
18
|
+
def name = (@name == LiteralNil) ? name_tk_val : @name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CheckTableConstraint < TableConstraint
|
|
5
|
+
token :constraint_kw
|
|
6
|
+
token :name_tk
|
|
7
|
+
token :check_kw
|
|
8
|
+
token :check_lp
|
|
9
|
+
token :check_rp
|
|
10
|
+
|
|
11
|
+
attribute :name, Stringy
|
|
12
|
+
node :expression, Expression
|
|
13
|
+
node :conflict_clause, ConflictClause
|
|
14
|
+
|
|
15
|
+
def self.new(*, expression:, **) = super
|
|
16
|
+
def self.concrete(*, expression:, **) = super
|
|
17
|
+
|
|
18
|
+
def name = (@name == LiteralNil) ? name_tk_val : @name
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CollateColumnConstraint < ColumnConstraint
|
|
5
|
+
token :constraint_kw
|
|
6
|
+
token :name_tk
|
|
7
|
+
token :collate_kw
|
|
8
|
+
token :collation_tk
|
|
9
|
+
|
|
10
|
+
attribute :name, Stringy
|
|
11
|
+
attribute :collation, Stringy
|
|
12
|
+
|
|
13
|
+
def self.new(*, collation:, **) = super
|
|
14
|
+
def self.concrete(*, collate_kw:, collation_tk:, **) = super
|
|
15
|
+
|
|
16
|
+
def name = (@name == LiteralNil) ? name_tk_val : @name
|
|
17
|
+
def collation = (@collation == LiteralNil) ? Node.collation_name(collation_tk_val) : @collation
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CollationExpression < Node
|
|
5
|
+
token :collate_kw
|
|
6
|
+
token :expression_tk
|
|
7
|
+
token :sequence_tk
|
|
8
|
+
|
|
9
|
+
attribute :expression, Expression
|
|
10
|
+
attribute :sequence, Stringy
|
|
11
|
+
|
|
12
|
+
def self.new(*, sequence:, **) = super
|
|
13
|
+
def self.concrete(*, sequence_tk:, **) = super
|
|
14
|
+
|
|
15
|
+
# Parser may pass either tokens or attributes, so
|
|
16
|
+
# require none in `concrete` signature but ensure presence here.
|
|
17
|
+
def after_initialize
|
|
18
|
+
if @expression.nil? && @expression_tk.nil?
|
|
19
|
+
raise ArgumentError.new("missing either: :expression, :expression_tk")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def expression = (@expression == LiteralNil) ? expression_tk_val : @expression
|
|
24
|
+
def sequence = (@sequence == LiteralNil) ? Node.collation_name(sequence_tk_val) : @sequence
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class ColumnDefinition < Node
|
|
5
|
+
token :name_tk
|
|
6
|
+
|
|
7
|
+
attribute :name, Stringy
|
|
8
|
+
node :type, ColumnType
|
|
9
|
+
nodes :constraints, ColumnConstraint
|
|
10
|
+
|
|
11
|
+
def self.new(*, name:, **) = super
|
|
12
|
+
def self.concrete(*, name_tk:, **) = super
|
|
13
|
+
|
|
14
|
+
def name = (@name == LiteralNil) ? name_tk_val : @name
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class ColumnName < Node
|
|
5
|
+
token :schema_tk # maybe
|
|
6
|
+
token :dot1 # maybe
|
|
7
|
+
token :table_tk # maybe
|
|
8
|
+
token :dot2 # maybe
|
|
9
|
+
token :column_tk # required
|
|
10
|
+
|
|
11
|
+
attribute :schema, Stringy # maybe
|
|
12
|
+
attribute :table, Stringy # maybe
|
|
13
|
+
attribute :column, Stringy # required
|
|
14
|
+
|
|
15
|
+
def self.new(*, column:, **) = super
|
|
16
|
+
def self.concrete(*, column_tk:, **) = super
|
|
17
|
+
|
|
18
|
+
def schema = (@schema == LiteralNil) ? schema_tk_val : @schema
|
|
19
|
+
def table = (@table == LiteralNil) ? table_tk_val : @table
|
|
20
|
+
def column = (@column == LiteralNil) ? column_tk_val : @column
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class ColumnType < Node
|
|
5
|
+
token :text_span
|
|
6
|
+
|
|
7
|
+
attribute :text, Stringy
|
|
8
|
+
|
|
9
|
+
def self.new(*, text:, **) = super
|
|
10
|
+
def self.concrete(*, text_span:, **) = super
|
|
11
|
+
|
|
12
|
+
def text = (@text == LiteralNil) ? text_span_val : @text
|
|
13
|
+
|
|
14
|
+
inspectable def affinity
|
|
15
|
+
# rules found at: https://www.sqlite.org/datatype3.html#determination_of_column_affinity
|
|
16
|
+
# Matched as bytes, as SQLite does, so a type name that is not valid UTF-8 has one too
|
|
17
|
+
case text&.b
|
|
18
|
+
when /INT/i then :INTEGER
|
|
19
|
+
when /CHAR|CLOB|TEXT/i then :TEXT
|
|
20
|
+
when /BLOB/i then :BLOB
|
|
21
|
+
when /REAL|FLOA|DOUB/i then :REAL
|
|
22
|
+
else :ANY
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
# COMMIT or END. **[SQLite Docs](https://www.sqlite.org/lang_transaction.html)**
|
|
5
|
+
class CommitStatement < Node
|
|
6
|
+
token :commit_kw
|
|
7
|
+
token :transaction_kw
|
|
8
|
+
token :name_tk
|
|
9
|
+
|
|
10
|
+
def self.new(*, **) = super
|
|
11
|
+
def self.concrete(*, commit_kw:, **) = super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
class CommonTableExpression < Node
|
|
5
|
+
token :name_tk
|
|
6
|
+
token :columns_lp
|
|
7
|
+
token :columns_rp
|
|
8
|
+
token :as_kw
|
|
9
|
+
token :materialized_tk
|
|
10
|
+
token :select_lp
|
|
11
|
+
token :select_rp
|
|
12
|
+
|
|
13
|
+
attribute :name, Stringy
|
|
14
|
+
nodes :columns, IndexedColumn
|
|
15
|
+
node :select, SelectStatement
|
|
16
|
+
|
|
17
|
+
def self.new(*, name:, select:, **) = super
|
|
18
|
+
def self.concrete(*, name_tk:, select:, **) = super
|
|
19
|
+
|
|
20
|
+
def name = (@name == LiteralNil) ? name_tk_val : @name
|
|
21
|
+
def column_names = columns&.map(&:name)
|
|
22
|
+
|
|
23
|
+
# true for MATERIALIZED, false for NOT MATERIALIZED, nil when left to the planner
|
|
24
|
+
inspectable def materialized
|
|
25
|
+
case materialized_tk_tok
|
|
26
|
+
when :MATERIALIZED then true
|
|
27
|
+
when [:NOT, :MATERIALIZED] then false
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|