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,797 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plume
|
|
4
|
+
module Analysis
|
|
5
|
+
# Walks a SELECT, works out what every table and column reference points at, and records
|
|
6
|
+
# the reads of base tables. Views and CTEs are looked through: their bodies are analyzed
|
|
7
|
+
# once they are referenced, and everything those bodies read counts as read - whether or
|
|
8
|
+
# not the outer query ends up using it. That matches what SQLite's authorizer reports.
|
|
9
|
+
class Resolver
|
|
10
|
+
# One entry in a FROM clause.
|
|
11
|
+
# kind :table - a base table, reads are recorded against it
|
|
12
|
+
# :derived - CTE, view or subquery, its body has been analyzed already
|
|
13
|
+
# :function - table-valued function, nothing to record
|
|
14
|
+
# :group - an aliased `(a JOIN b) AS x`, columns live in `inner_sources`
|
|
15
|
+
# `columns` is nil when we cannot know them (no catalog, unknown function)
|
|
16
|
+
# `qualified_only` sources answer to `name.column` but never to a bare column name -
|
|
17
|
+
# that is how the `excluded` row of an upsert behaves
|
|
18
|
+
Source = Struct.new(:name, :kind, :schema, :table, :columns, :inner_sources, :merged, :qualified_only, :node, keyword_init: true) do
|
|
19
|
+
def self.table(name, table, node) = new(name:, kind: :table, schema: table.schema, table:, columns: table.columns, node:)
|
|
20
|
+
|
|
21
|
+
def matches?(qualifier, schema_qualifier)
|
|
22
|
+
return false unless name&.casecmp(qualifier)&.zero?
|
|
23
|
+
return true unless schema_qualifier
|
|
24
|
+
|
|
25
|
+
schema&.casecmp(schema_qualifier)&.zero? || (schema.nil? && schema_qualifier.casecmp("main")&.zero?)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def column(name)
|
|
29
|
+
return unless columns
|
|
30
|
+
return table.column(name) if table
|
|
31
|
+
|
|
32
|
+
needle = name.downcase(:ascii)
|
|
33
|
+
columns.find { |c| c&.downcase(:ascii) == needle }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
Scope = Struct.new(:sources, :parent, :ctes, :aliases, keyword_init: true) do
|
|
38
|
+
def each_up
|
|
39
|
+
scope = self
|
|
40
|
+
while scope
|
|
41
|
+
yield scope
|
|
42
|
+
scope = scope.parent
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
Cte = Struct.new(:node, :scope, :analyzed, :columns, :in_progress, keyword_init: true)
|
|
48
|
+
|
|
49
|
+
# The columns of json_each and json_tree are fixed, see https://www.sqlite.org/json1.html#jeach
|
|
50
|
+
JSON_TABLE_COLUMNS = %w[key value type atom id parent fullkey path json root].freeze
|
|
51
|
+
FUNCTION_COLUMNS = {
|
|
52
|
+
"json_each" => JSON_TABLE_COLUMNS,
|
|
53
|
+
"json_tree" => JSON_TABLE_COLUMNS,
|
|
54
|
+
"jsonb_each" => JSON_TABLE_COLUMNS,
|
|
55
|
+
"jsonb_tree" => JSON_TABLE_COLUMNS,
|
|
56
|
+
"generate_series" => %w[value start stop step].freeze,
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
STATEMENTS = [SelectStatement, InsertStatement, UpdateStatement, DeleteStatement, CreateTriggerStatement].freeze # analyzed for reads and writes
|
|
60
|
+
|
|
61
|
+
# What SQLite calls for these operators and keywords, as its authorizer names them
|
|
62
|
+
OPERATOR_FUNCTIONS = {
|
|
63
|
+
LIKE: "like", NOT_LIKE: "like", GLOB: "glob", NOT_GLOB: "glob", REGEXP: "regexp", NOT_REGEXP: "regexp",
|
|
64
|
+
MATCH: "match", NOT_MATCH: "match", EXTRACT: "->", RETRIEVE: "->>",
|
|
65
|
+
}.freeze
|
|
66
|
+
|
|
67
|
+
# SQLite's own limit, which also keeps resolving names across a join from going quadratic
|
|
68
|
+
MAX_JOIN_TABLES = 64
|
|
69
|
+
|
|
70
|
+
def initialize(catalog: nil)
|
|
71
|
+
@catalog = catalog
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def analyze(statement)
|
|
75
|
+
start(statement)
|
|
76
|
+
|
|
77
|
+
case statement
|
|
78
|
+
when SelectStatement then select(statement, parent: nil, usage: :projected)
|
|
79
|
+
when InsertStatement then insert(statement)
|
|
80
|
+
when UpdateStatement then update(statement)
|
|
81
|
+
when DeleteStatement then delete(statement)
|
|
82
|
+
when CreateTriggerStatement then trigger(statement)
|
|
83
|
+
when ExplainStatement then return analyze(statement.statement)
|
|
84
|
+
else schema_statement(statement)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Access.new(reads: @reads, writes: @writes, actions: @actions.freeze)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Names of the columns a select produces, nil for the ones we cannot name
|
|
91
|
+
def output_columns(statement)
|
|
92
|
+
start(statement)
|
|
93
|
+
select(statement, parent: nil, usage: :projected) || []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def start(statement)
|
|
99
|
+
@reads = Reads.new(statement)
|
|
100
|
+
@writes = Writes.new(statement, @reads.unresolved, @reads.ctes, @reads.aliases)
|
|
101
|
+
@actions = []
|
|
102
|
+
# By identity: a view holds its parsed select, and hashing that hashes the whole tree
|
|
103
|
+
@expanding_views = Set.new.compare_by_identity
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def with_scope(with_clause, parent)
|
|
107
|
+
scope = Scope.new(sources: [], parent:, ctes: {})
|
|
108
|
+
with_clause&.ctes&.each do |cte|
|
|
109
|
+
scope.ctes[cte.name.downcase(:ascii)] = Cte.new(node: cte, scope:, analyzed: Set.new)
|
|
110
|
+
@reads.ctes << cte.name if @expanding_views.empty?
|
|
111
|
+
end
|
|
112
|
+
scope
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# -- INSERT, UPDATE, DELETE
|
|
116
|
+
|
|
117
|
+
def insert(statement, parent: nil)
|
|
118
|
+
outer = with_scope(statement.with_clause, parent)
|
|
119
|
+
# The rows being inserted cannot see the target table, upserts and RETURNING can
|
|
120
|
+
select(statement.select, parent: outer, usage: :projected) if statement.select
|
|
121
|
+
|
|
122
|
+
scope = Scope.new(sources: [], parent: outer, ctes: {}, aliases: {})
|
|
123
|
+
target = add_target(statement.table, scope)
|
|
124
|
+
|
|
125
|
+
if statement.default_values
|
|
126
|
+
write(target, nil, :insert, statement.table)
|
|
127
|
+
elsif statement.columns
|
|
128
|
+
statement.columns.each { |column| write(target, column.column, :insert, column) }
|
|
129
|
+
elsif target&.columns
|
|
130
|
+
# A VALUES row without a column list has no slot for generated columns
|
|
131
|
+
(target.columns - (target.table&.generated_columns || [])).each { |column| write(target, column, :insert, statement.table) }
|
|
132
|
+
else
|
|
133
|
+
write(target, "*", :insert, statement.table)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
statement.upserts&.each do |upsert|
|
|
137
|
+
excluded = Source.new(name: "excluded", kind: :derived, columns: target&.columns, qualified_only: true, node: upsert)
|
|
138
|
+
upsert_scope = Scope.new(sources: [target, excluded].compact, parent: outer, ctes: {}, aliases: {})
|
|
139
|
+
|
|
140
|
+
upsert.targets&.each { |term| expression(term.expression, upsert_scope, :referenced) }
|
|
141
|
+
expression(upsert.target_condition, upsert_scope, :referenced)
|
|
142
|
+
assign(upsert.assignments, target, upsert_scope)
|
|
143
|
+
expression(upsert.condition, upsert_scope, :referenced)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
returning(statement.returning, scope, target)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def update(statement, parent: nil)
|
|
150
|
+
outer = with_scope(statement.with_clause, parent)
|
|
151
|
+
scope = Scope.new(sources: [], parent: outer, ctes: {}, aliases: {})
|
|
152
|
+
target = add_target(statement.table, scope)
|
|
153
|
+
if statement.source
|
|
154
|
+
# Pairing the target's rows with FROM's reads the target, named columns or not
|
|
155
|
+
record(target.schema, target.table&.name || statement.table.table, nil, :referenced, statement.table) if target&.kind == :table
|
|
156
|
+
add_from(statement.source, scope, :projected)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
assign(statement.assignments, target, scope)
|
|
160
|
+
expression(statement.condition, scope, :referenced)
|
|
161
|
+
returning(statement.returning, scope, target)
|
|
162
|
+
tail(statement, scope)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def delete(statement, parent: nil)
|
|
166
|
+
outer = with_scope(statement.with_clause, parent)
|
|
167
|
+
scope = Scope.new(sources: [], parent: outer, ctes: {}, aliases: {})
|
|
168
|
+
target = add_target(statement.table, scope)
|
|
169
|
+
|
|
170
|
+
write(target, nil, :delete, statement.table)
|
|
171
|
+
expression(statement.condition, scope, :referenced)
|
|
172
|
+
returning(statement.returning, scope, target)
|
|
173
|
+
tail(statement, scope)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# -- schema changes and the rest
|
|
177
|
+
|
|
178
|
+
def schema_statement(statement)
|
|
179
|
+
case statement
|
|
180
|
+
when CreateTableStatement
|
|
181
|
+
schema = new_schema(statement.table, statement.temporary)
|
|
182
|
+
act(:create_table, temp: schema, name: statement.table.name, node: statement)
|
|
183
|
+
Catalog.autoindexes(statement).each { |index| act(:create_index, temp: schema, name: index, detail: statement.table.name, node: statement) }
|
|
184
|
+
select(statement.select_statement, parent: nil, usage: :projected) if statement.select_statement
|
|
185
|
+
when CreateViewStatement
|
|
186
|
+
# SQLite checks a view's body when the view is used, not when it is made
|
|
187
|
+
act(:create_view, temp: new_schema(statement.view, statement.temporary), name: statement.view.name, node: statement)
|
|
188
|
+
when CreateIndexStatement then create_index(statement)
|
|
189
|
+
when CreateVirtualTableStatement
|
|
190
|
+
act(:create_vtable, name: statement.table.name, detail: statement.module_name, schema: new_schema(statement.table, false), node: statement)
|
|
191
|
+
when DropStatement then drop(statement)
|
|
192
|
+
when AlterTableStatement
|
|
193
|
+
table = @catalog&.table(statement.table.table, schema: statement.table.schema)
|
|
194
|
+
detail = statement.column if statement.action == :DROP_COLUMN
|
|
195
|
+
act(:alter_table, name: table&.name || statement.table.table, detail:, schema: table&.schema || statement.table.schema || "main", node: statement)
|
|
196
|
+
when PragmaStatement
|
|
197
|
+
act(:pragma, name: statement.name, detail: pragma_value(statement), schema: statement.schema, node: statement)
|
|
198
|
+
when BeginStatement then act(:transaction, name: "BEGIN", node: statement)
|
|
199
|
+
when CommitStatement then act(:transaction, name: "COMMIT", node: statement)
|
|
200
|
+
when RollbackStatement
|
|
201
|
+
if statement.savepoint
|
|
202
|
+
act(:savepoint, name: "ROLLBACK", detail: statement.savepoint, node: statement)
|
|
203
|
+
else
|
|
204
|
+
act(:transaction, name: "ROLLBACK", node: statement)
|
|
205
|
+
end
|
|
206
|
+
when SavepointStatement then act(:savepoint, name: "BEGIN", detail: statement.name, node: statement)
|
|
207
|
+
when ReleaseStatement then act(:savepoint, name: "RELEASE", detail: statement.name, node: statement)
|
|
208
|
+
when AttachStatement then act(:attach, name: plain_value(statement.file), node: statement)
|
|
209
|
+
when DetachStatement then act(:detach, name: plain_value(statement.schema), node: statement)
|
|
210
|
+
when ReindexStatement then reindex(statement)
|
|
211
|
+
when AnalyzeStatement then analyze_tables(statement)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def create_index(statement)
|
|
216
|
+
schema = statement.index.schema
|
|
217
|
+
table_node = TableName.new(schema:, table: statement.table)
|
|
218
|
+
act(:create_index, temp: new_schema(statement.index, false, on: table_node), name: statement.index.name, detail: statement.table, node: statement)
|
|
219
|
+
scope = Scope.new(sources: [], parent: nil, ctes: {}, aliases: {})
|
|
220
|
+
add_table_source(table_node, scope, :referenced)
|
|
221
|
+
# The table's rows are read to fill the index, which only an authorizer sees as columns
|
|
222
|
+
@reads.references.pop if @reads.references.last&.column.nil?
|
|
223
|
+
statement.columns&.each { |term| expression(term.expression, scope, :referenced) }
|
|
224
|
+
expression(statement.condition, scope, :referenced)
|
|
225
|
+
act(:reindex, name: statement.index.name, schema: @actions.last.schema, node: statement)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def drop(statement)
|
|
229
|
+
target = statement.target
|
|
230
|
+
case statement.type
|
|
231
|
+
when :TABLE
|
|
232
|
+
table = @catalog&.table(target.table, schema: target.schema)
|
|
233
|
+
if table&.module_name
|
|
234
|
+
act(:drop_vtable, name: table.name, detail: table.module_name, schema: table.schema, node: statement)
|
|
235
|
+
else
|
|
236
|
+
act(:drop_table, temp: table&.schema || target.schema || "main", name: table&.name || target.table, node: statement)
|
|
237
|
+
end
|
|
238
|
+
@writes.references << Reference.new(schema: table&.schema || target.schema, table: table&.name || target.table, column: nil, usage: :delete, node: statement)
|
|
239
|
+
drop_triggers(table, statement)
|
|
240
|
+
when :VIEW
|
|
241
|
+
view = @catalog&.view(target.table, schema: target.schema)
|
|
242
|
+
act(:drop_view, temp: view&.schema || target.schema || "main", name: view&.name || target.table, node: statement)
|
|
243
|
+
drop_triggers(view, statement)
|
|
244
|
+
when :INDEX
|
|
245
|
+
index = @catalog&.index(target.table, schema: target.schema)
|
|
246
|
+
act(:drop_index, temp: index&.schema || target.schema || "main", name: index&.name || target.table, detail: index&.table, node: statement)
|
|
247
|
+
when :TRIGGER
|
|
248
|
+
trigger = @catalog&.trigger(target.table, schema: target.schema)
|
|
249
|
+
act(:drop_trigger, temp: trigger&.schema || target.schema || "main", name: trigger&.name || target.table, detail: trigger&.table, node: statement)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Dropping a table or view drops its triggers, newest first
|
|
254
|
+
def drop_triggers(object, statement)
|
|
255
|
+
return unless object
|
|
256
|
+
|
|
257
|
+
triggers = @catalog.triggers.select { |t| Catalog.same_name?(t.schema, object.schema) && Catalog.same_name?(t.table, object.name) }
|
|
258
|
+
triggers.reverse_each { |t| act(:drop_trigger, temp: t.schema, name: t.name, detail: t.table, node: statement) }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# `temp:` takes the schema and makes the action its _temp_ variant when that schema is temp
|
|
262
|
+
def act(action, node:, name: nil, detail: nil, schema: nil, temp: nil)
|
|
263
|
+
if temp
|
|
264
|
+
schema = temp
|
|
265
|
+
action = action.name.sub(/\A(create|drop)_/) { "#{$1}_temp_" }.to_sym if Catalog.same_name?(temp, "temp")
|
|
266
|
+
end
|
|
267
|
+
@actions << Action.new(action:, name:, detail:, schema:, node:)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Where a new object goes: the schema it names, temp for TEMP, or the schema of the table it is on
|
|
271
|
+
def new_schema(name, temporary, on: nil)
|
|
272
|
+
return name.schema if name.schema
|
|
273
|
+
return "temp" if temporary
|
|
274
|
+
return @catalog&.table(on.table)&.schema || "main" if on
|
|
275
|
+
|
|
276
|
+
"main"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# REINDEX names an index, a table whose indexes to rebuild, or a collation - and SQLite's
|
|
280
|
+
# authorizer is asked about each index rebuilt. Without a catalog only a name is known
|
|
281
|
+
def reindex(statement)
|
|
282
|
+
target = statement.target
|
|
283
|
+
return target && act(:reindex, name: target.table, schema: target.schema || "main", node: statement) unless @catalog
|
|
284
|
+
|
|
285
|
+
indexes =
|
|
286
|
+
if target.nil? then @catalog.indexes
|
|
287
|
+
elsif (index = @catalog.index(target.table, schema: target.schema)) then [index]
|
|
288
|
+
elsif (table = @catalog.table(target.table, schema: target.schema))
|
|
289
|
+
@catalog.indexes.select { |i| i.schema == table.schema && Catalog.same_name?(i.table, table.name) }
|
|
290
|
+
else
|
|
291
|
+
[]
|
|
292
|
+
end
|
|
293
|
+
indexes.each { |i| act(:reindex, name: i.name, schema: i.schema, node: statement) }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# ANALYZE names a table, an index, a schema, or nothing for every schema but temp
|
|
297
|
+
def analyze_tables(statement)
|
|
298
|
+
target = statement.target
|
|
299
|
+
return target && act(:analyze, name: target.table, schema: target.schema || "main", node: statement) unless @catalog
|
|
300
|
+
|
|
301
|
+
tables =
|
|
302
|
+
if target.nil? then @catalog.tables.reject { |t| Catalog.same_name?(t.schema, "temp") }
|
|
303
|
+
elsif (table = @catalog.table(target.table, schema: target.schema)) then [table]
|
|
304
|
+
elsif (index = @catalog.index(target.table, schema: target.schema)) then [@catalog.table(index.table, schema: index.schema)].compact
|
|
305
|
+
elsif target.schema.nil? then @catalog.tables.select { |t| Catalog.same_name?(t.schema, target.table) }
|
|
306
|
+
else
|
|
307
|
+
[]
|
|
308
|
+
end
|
|
309
|
+
tables.each { |t| act(:analyze, name: t.name, schema: t.schema, node: statement) unless t.module_name }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# SQLite hands the authorizer a PRAGMA's value as text, unquoted and without a plus sign
|
|
313
|
+
def pragma_value(statement)
|
|
314
|
+
src = statement.value_tk_src or return
|
|
315
|
+
(src.match?(/\A['"]/) ? statement.value_tk_val.to_s : src.delete_prefix("+"))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# ATTACH and DETACH name a file or schema with any expression; a plain string or name is told
|
|
319
|
+
def plain_value(node)
|
|
320
|
+
case node
|
|
321
|
+
when LiteralExpression then node.value.to_s
|
|
322
|
+
when ColumnName then node.column
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# What a trigger's WHEN and body read and write each time it fires. NEW and OLD stand for
|
|
327
|
+
# the row of the table it is on, and reading them reads that table, as SQLite's authorizer has it
|
|
328
|
+
def trigger(statement)
|
|
329
|
+
on_schema = @catalog&.table(statement.table.table, schema: statement.table.schema)&.schema
|
|
330
|
+
act(:create_trigger, temp: statement.trigger.schema || (statement.temporary ? "temp" : on_schema || "main"), name: statement.trigger.name, detail: statement.table.table, node: statement)
|
|
331
|
+
scope = Scope.new(sources: [], parent: nil, ctes: {}, aliases: {})
|
|
332
|
+
if (on = trigger_table(statement.table))
|
|
333
|
+
rows = [("new" unless statement.event == :DELETE), ("old" unless statement.event == :INSERT)].compact
|
|
334
|
+
rows.each { |name| scope.sources << on.dup.tap { |row| row.name, row.qualified_only = name, true } }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
expression(statement.condition, scope, :referenced)
|
|
338
|
+
statement.statements.each do |body|
|
|
339
|
+
case body
|
|
340
|
+
when SelectStatement then select(body, parent: scope, usage: :projected)
|
|
341
|
+
when InsertStatement then insert(body, parent: scope)
|
|
342
|
+
when UpdateStatement then update(body, parent: scope)
|
|
343
|
+
when DeleteStatement then delete(body, parent: scope)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# The table or view a trigger is on, which it neither reads nor writes by being on it
|
|
349
|
+
def trigger_table(node)
|
|
350
|
+
scope = Scope.new(sources: [], parent: nil, ctes: {}, aliases: {})
|
|
351
|
+
add_target(node, scope)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Assigned values end up in the table, so whatever they read counts as projected
|
|
355
|
+
def assign(assignments, target, scope)
|
|
356
|
+
assignments&.each do |assignment|
|
|
357
|
+
assignment.columns.each { |column| write(target, column.column, :update, column) }
|
|
358
|
+
expression(assignment.value, scope, :projected)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# A bare * in RETURNING means the target's columns only, not those of an UPDATE's FROM
|
|
363
|
+
def returning(clause, scope, target)
|
|
364
|
+
clause&.columns&.each do |column|
|
|
365
|
+
if StarResultColumn === column && column.table.nil?
|
|
366
|
+
star_source(target, :projected, column, skip_merged: false) if target
|
|
367
|
+
elsif StarResultColumn === column
|
|
368
|
+
star(column, scope, :projected)
|
|
369
|
+
else
|
|
370
|
+
expression(column.result, scope, :projected)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def tail(statement, scope)
|
|
376
|
+
statement.orderings&.each { |term| expression(term.expression, scope, :referenced) }
|
|
377
|
+
expression(statement.limit, scope, :referenced)
|
|
378
|
+
expression(statement.offset, scope, :referenced)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# The table an INSERT, UPDATE or DELETE writes to. CTEs never stand in for it, and
|
|
382
|
+
# writing to it is not a read, so it joins the scope without recording one
|
|
383
|
+
def add_target(node, scope)
|
|
384
|
+
name = node.alias || node.table
|
|
385
|
+
alias_of(node.alias, node.schema, node.table)
|
|
386
|
+
source =
|
|
387
|
+
if @catalog.nil?
|
|
388
|
+
Source.new(name:, kind: :table, schema: node.schema, node:)
|
|
389
|
+
elsif (table = @catalog.table(node.table, schema: node.schema))
|
|
390
|
+
Source.table(name, table, node)
|
|
391
|
+
elsif (view = @catalog.view(node.table, schema: node.schema))
|
|
392
|
+
# Writable only through INSTEAD OF triggers, which we do not look into
|
|
393
|
+
@reads.views << Analysis.table_key(view.schema, view.name)
|
|
394
|
+
Source.new(name:, kind: :derived, schema: view.schema, columns: view.columns, node:)
|
|
395
|
+
else
|
|
396
|
+
unresolved(node.table, "no such table", node)
|
|
397
|
+
return
|
|
398
|
+
end
|
|
399
|
+
scope.sources << source
|
|
400
|
+
source
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def write(target, column, action, node)
|
|
404
|
+
return unless target
|
|
405
|
+
|
|
406
|
+
if target.table
|
|
407
|
+
if column && column != "*"
|
|
408
|
+
canonical = target.column(column)
|
|
409
|
+
# SQLite's authorizer names a write to the rowid "ROWID" however it was spelled, alias or not
|
|
410
|
+
canonical ||= "ROWID" if Catalog::ROWID_NAMES.include?(column.downcase(:ascii)) && target.table.rowid?
|
|
411
|
+
return unresolved(column, "no such column in #{target.table.name}", node) unless canonical
|
|
412
|
+
|
|
413
|
+
column = canonical
|
|
414
|
+
end
|
|
415
|
+
@writes.references << Reference.new(schema: target.table.schema, table: target.table.name, column:, usage: action, node:)
|
|
416
|
+
else
|
|
417
|
+
@writes.references << Reference.new(schema: target.schema, table: target.node.table, column:, usage: action, node:)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def select(statement, parent:, usage:)
|
|
422
|
+
scope = with_scope(statement.with_clause, parent)
|
|
423
|
+
|
|
424
|
+
names = nil
|
|
425
|
+
core_scope = nil
|
|
426
|
+
statement.cores.each do |core|
|
|
427
|
+
core_names, core_scope = select_core(core, parent: scope, usage:)
|
|
428
|
+
names ||= core_names
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# ORDER BY of a compound can only name result columns, which reads nothing new
|
|
432
|
+
if statement.cores.size == 1 && core_scope
|
|
433
|
+
statement.orderings&.each do |term|
|
|
434
|
+
expression(term.expression, core_scope, :referenced, aliases: :first)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
expression(statement.limit, scope, :referenced)
|
|
438
|
+
expression(statement.offset, scope, :referenced)
|
|
439
|
+
|
|
440
|
+
names
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def select_core(core, parent:, usage:)
|
|
444
|
+
scope = Scope.new(sources: [], parent:, ctes: {}, aliases: {})
|
|
445
|
+
|
|
446
|
+
if ValuesClause === core
|
|
447
|
+
core.rows.each { |row| row.values.each { |v| expression(v, scope, usage) } }
|
|
448
|
+
width = core.rows.first.values.size
|
|
449
|
+
return [Array.new(width) { |i| "column#{i + 1}" }, scope]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
add_from(core.source, scope, usage) if core.source
|
|
453
|
+
|
|
454
|
+
core.columns.each do |column|
|
|
455
|
+
scope.aliases[column.alias.downcase(:ascii)] = column.result if ResultColumn === column && column.alias
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
names = []
|
|
459
|
+
core.columns.each do |column|
|
|
460
|
+
if StarResultColumn === column
|
|
461
|
+
expanded = star(column, scope, usage)
|
|
462
|
+
expanded ? names.concat(expanded) : names << nil
|
|
463
|
+
else
|
|
464
|
+
expression(column.result, scope, usage)
|
|
465
|
+
names << output_name(column)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
expression(core.condition, scope, :referenced, aliases: :fallback)
|
|
470
|
+
core.groupings&.each { |g| expression(g, scope, :referenced, aliases: :fallback) }
|
|
471
|
+
expression(core.having, scope, :referenced, aliases: :fallback)
|
|
472
|
+
core.windows&.each do |window|
|
|
473
|
+
window.partitions&.each { |p| expression(p, scope, :referenced) }
|
|
474
|
+
window.orderings&.each { |o| expression(o.expression, scope, :referenced) }
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
[names, scope]
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def output_name(column)
|
|
481
|
+
return column.alias if column.alias
|
|
482
|
+
|
|
483
|
+
case (result = column.result)
|
|
484
|
+
when ColumnName then result.column
|
|
485
|
+
when Node then result.source_text
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# -- FROM
|
|
490
|
+
|
|
491
|
+
def add_from(source, scope, usage)
|
|
492
|
+
add_source(source, scope, usage)
|
|
493
|
+
tables = scope.sources.sum { |s| join_tables(s) }
|
|
494
|
+
raise Error, "at most #{MAX_JOIN_TABLES} tables in a join" if tables > MAX_JOIN_TABLES
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def join_tables(source) = (source.kind == :group) ? source.inner_sources.sum { |s| join_tables(s) } : 1
|
|
498
|
+
|
|
499
|
+
def add_source(source, scope, usage)
|
|
500
|
+
case source
|
|
501
|
+
when JoinClause
|
|
502
|
+
add_source(source.left, scope, usage)
|
|
503
|
+
source.joins.each do |join|
|
|
504
|
+
left_sources = scope.sources.dup
|
|
505
|
+
add_source(join.source, scope, usage)
|
|
506
|
+
join_constraint(join, left_sources, scope.sources.last, scope)
|
|
507
|
+
end
|
|
508
|
+
when TableName then add_table_source(source, scope, usage)
|
|
509
|
+
when TableFunction
|
|
510
|
+
alias_of(source.alias, source.schema, source.function)
|
|
511
|
+
source.arguments&.each { |a| expression(a, scope, :referenced) }
|
|
512
|
+
# Any virtual table can be called like this, `fts('query')` reads the fts table
|
|
513
|
+
if @catalog && (table = @catalog.table(source.function, schema: source.schema))
|
|
514
|
+
record(table.schema, table.name, nil, usage, source)
|
|
515
|
+
scope.sources << Source.table(source.alias || source.function, table, source)
|
|
516
|
+
return
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
@reads.functions << source.function.downcase(:ascii)
|
|
520
|
+
scope.sources << Source.new(
|
|
521
|
+
name: source.alias || source.function, kind: :function,
|
|
522
|
+
columns: FUNCTION_COLUMNS[source.function.downcase(:ascii)], node: source,
|
|
523
|
+
)
|
|
524
|
+
when SubquerySource
|
|
525
|
+
# A FROM subquery sees the enclosing queries, but not its siblings in this FROM
|
|
526
|
+
names = select(source.select, parent: scope.parent, usage:)
|
|
527
|
+
scope.sources << Source.new(name: source.alias, kind: :derived, columns: names, node: source)
|
|
528
|
+
when ParentheticalSource
|
|
529
|
+
if source.alias
|
|
530
|
+
inner = Scope.new(sources: [], parent: scope.parent, ctes: {})
|
|
531
|
+
add_source(source.source, inner, usage)
|
|
532
|
+
scope.sources << Source.new(name: source.alias, kind: :group, inner_sources: inner.sources, node: source)
|
|
533
|
+
else
|
|
534
|
+
add_source(source.source, scope, usage)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def add_table_source(source, scope, usage)
|
|
540
|
+
name = source.alias || source.table
|
|
541
|
+
alias_of(source.alias, source.schema, source.table)
|
|
542
|
+
|
|
543
|
+
if source.schema.nil? && (cte = find_cte(source.table, scope))
|
|
544
|
+
columns = analyze_cte(cte, usage)
|
|
545
|
+
scope.sources << Source.new(name:, kind: :derived, columns:, node: source)
|
|
546
|
+
elsif @catalog.nil?
|
|
547
|
+
record(source.schema, source.table, nil, usage, source)
|
|
548
|
+
scope.sources << Source.new(name:, kind: :table, schema: source.schema, table: nil, node: source)
|
|
549
|
+
elsif (table = @catalog.table(source.table, schema: source.schema))
|
|
550
|
+
record(table.schema, table.name, nil, usage, source)
|
|
551
|
+
scope.sources << Source.table(name, table, source)
|
|
552
|
+
elsif (view = @catalog.view(source.table, schema: source.schema))
|
|
553
|
+
@reads.views << Analysis.table_key(view.schema, view.name)
|
|
554
|
+
if @expanding_views.include?(view)
|
|
555
|
+
unresolved(source.table, "circularly defined view", source)
|
|
556
|
+
else
|
|
557
|
+
@expanding_views << view
|
|
558
|
+
# A view's body starts a scope chain of its own, so the CTEs of the query using it stay out of sight
|
|
559
|
+
names = select(view.select, parent: nil, usage:)
|
|
560
|
+
@expanding_views.delete(view)
|
|
561
|
+
end
|
|
562
|
+
scope.sources << Source.new(name:, kind: :derived, columns: view.columns || names, node: source)
|
|
563
|
+
else
|
|
564
|
+
unresolved(source.table, "no such table", source)
|
|
565
|
+
scope.sources << Source.new(name:, kind: :table, schema: source.schema, node: source)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def find_cte(name, scope)
|
|
570
|
+
needle = name.downcase(:ascii)
|
|
571
|
+
scope.each_up do |s|
|
|
572
|
+
return s.ctes[needle] if s.ctes&.key?(needle)
|
|
573
|
+
end
|
|
574
|
+
nil
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def analyze_cte(cte, usage)
|
|
578
|
+
declared = cte.node.column_names
|
|
579
|
+
# A recursive CTE refers to itself, we know its columns at best from the declaration
|
|
580
|
+
return declared || cte.columns if cte.in_progress || cte.analyzed.include?(usage)
|
|
581
|
+
|
|
582
|
+
cte.in_progress = true
|
|
583
|
+
names = select(cte.node.select, parent: cte.scope, usage:)
|
|
584
|
+
cte.in_progress = false
|
|
585
|
+
cte.analyzed << usage
|
|
586
|
+
cte.columns = names
|
|
587
|
+
declared || names
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def join_constraint(join, left_sources, right, scope)
|
|
591
|
+
shared =
|
|
592
|
+
case join.constraint
|
|
593
|
+
when JoinUsingConstraint then join.constraint.column_names
|
|
594
|
+
else
|
|
595
|
+
if join.operator.natural
|
|
596
|
+
if right.columns.nil? || left_sources.any? { |s| s.columns.nil? }
|
|
597
|
+
unresolved(right.name.to_s, "cannot work out the columns of a NATURAL JOIN without a catalog", join)
|
|
598
|
+
[]
|
|
599
|
+
else
|
|
600
|
+
right.columns.select { |c| left_sources.any? { |s| s.column(c) } }
|
|
601
|
+
end
|
|
602
|
+
else
|
|
603
|
+
expression(join.constraint.expression, scope, :referenced) if JoinOnConstraint === join.constraint
|
|
604
|
+
[]
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# Both sides of USING and NATURAL are compared, even though SQLite's authorizer
|
|
609
|
+
# does not report the right-hand one
|
|
610
|
+
shared.each do |column|
|
|
611
|
+
left = left_sources.find { |s| s.column(column) } || left_sources.find { |s| s.columns.nil? }
|
|
612
|
+
read_through(left, column, :referenced, join) if left
|
|
613
|
+
read_through(right, column, :referenced, join)
|
|
614
|
+
(right.merged ||= Set.new) << column.downcase(:ascii)
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def star(column, scope, usage)
|
|
619
|
+
sources =
|
|
620
|
+
if column.table
|
|
621
|
+
found = scope.sources.find { |s| s.matches?(column.table, nil) }
|
|
622
|
+
unless found
|
|
623
|
+
unresolved("#{column.table}.*", "no such table", column)
|
|
624
|
+
return nil
|
|
625
|
+
end
|
|
626
|
+
[found]
|
|
627
|
+
else
|
|
628
|
+
scope.sources
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
names = []
|
|
632
|
+
sources.each do |source|
|
|
633
|
+
expanded = star_source(source, usage, column, skip_merged: column.table.nil?)
|
|
634
|
+
expanded ? names.concat(expanded) : (return nil)
|
|
635
|
+
end
|
|
636
|
+
names
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def star_source(source, usage, node, skip_merged:)
|
|
640
|
+
case source.kind
|
|
641
|
+
when :group
|
|
642
|
+
source.inner_sources.flat_map { |m| star_source(m, usage, node, skip_merged:) || [] }
|
|
643
|
+
when :table
|
|
644
|
+
if source.columns.nil?
|
|
645
|
+
record(source.schema, source.node.table, "*", usage, node)
|
|
646
|
+
nil
|
|
647
|
+
else
|
|
648
|
+
columns = source.columns.reject { |c| skip_merged && source.merged&.include?(c.downcase(:ascii)) }
|
|
649
|
+
columns.each { |c| record(source.table.schema, source.table.name, c, usage, node) }
|
|
650
|
+
columns
|
|
651
|
+
end
|
|
652
|
+
else
|
|
653
|
+
source.columns&.reject { |c| skip_merged && c && source.merged&.include?(c.downcase(:ascii)) }
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
# -- expressions
|
|
658
|
+
|
|
659
|
+
def expression(node, scope, usage, aliases: :none)
|
|
660
|
+
case node
|
|
661
|
+
when nil, Variable then nil
|
|
662
|
+
when LiteralExpression
|
|
663
|
+
@reads.calls << node.value.name.downcase if node.value in :CURRENT_DATE | :CURRENT_TIME | :CURRENT_TIMESTAMP
|
|
664
|
+
when ColumnName then column(node, scope, usage, aliases)
|
|
665
|
+
when FunctionReference
|
|
666
|
+
@reads.calls << node.function.downcase(:ascii)
|
|
667
|
+
node.children.each { |child| expression(child, scope, usage, aliases:) }
|
|
668
|
+
when SelectStatement then select(node, parent: scope, usage:)
|
|
669
|
+
when TableName then whole_table(node, scope)
|
|
670
|
+
when Node
|
|
671
|
+
if node.respond_to?(:operator) && (function = OPERATOR_FUNCTIONS[node.operator])
|
|
672
|
+
@reads.calls << function
|
|
673
|
+
end
|
|
674
|
+
node.children.each { |child| expression(child, scope, usage, aliases:) }
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
# `x IN t` compares against the single column of t
|
|
679
|
+
def whole_table(node, scope)
|
|
680
|
+
fake = Scope.new(sources: [], parent: scope, ctes: {})
|
|
681
|
+
add_table_source(node, fake, :referenced)
|
|
682
|
+
source = fake.sources.last
|
|
683
|
+
star_source(source, :referenced, node, skip_merged: false) if source
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def column(node, scope, usage, aliases)
|
|
687
|
+
name = node.column
|
|
688
|
+
|
|
689
|
+
if node.table
|
|
690
|
+
scope.each_up do |s|
|
|
691
|
+
source = s.sources.find { |src| src.matches?(node.table, node.schema) }
|
|
692
|
+
return read_through(source, name, usage, node) if source
|
|
693
|
+
end
|
|
694
|
+
return unresolved(qualified_name(node), "no such table: #{node.table}", node)
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
return if aliases == :first && scope.aliases&.key?(name.downcase(:ascii))
|
|
698
|
+
|
|
699
|
+
scope.each_up do |s|
|
|
700
|
+
return if resolve_in(s, name, usage, node)
|
|
701
|
+
return if s.equal?(scope) && aliases == :fallback && s.aliases&.key?(name.downcase(:ascii))
|
|
702
|
+
return if rowid(s, name, usage, node)
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
# SQLite reads an unresolvable "double-quoted" identifier as a string literal
|
|
706
|
+
return if node.column_tk_src&.start_with?('"')
|
|
707
|
+
|
|
708
|
+
unresolved(name, "no such column", node)
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
def resolve_in(scope, name, usage, node)
|
|
712
|
+
candidates = scope.sources.reject(&:qualified_only)
|
|
713
|
+
known = candidates.select { |s| source_has?(s, name) }
|
|
714
|
+
unknown = candidates.select { |s| source_unknown?(s) }
|
|
715
|
+
|
|
716
|
+
if known.size == 1 && unknown.empty?
|
|
717
|
+
read_through(known.first, name, usage, node)
|
|
718
|
+
elsif known.size > 1 && unknown.empty?
|
|
719
|
+
# A USING/NATURAL column belongs to both sides and is not ambiguous
|
|
720
|
+
merged = known.select { |s| s.merged&.include?(name.downcase(:ascii)) }
|
|
721
|
+
if merged.any?
|
|
722
|
+
read_through((known - merged).first || known.first, name, usage, node)
|
|
723
|
+
else
|
|
724
|
+
unresolved(name, "ambiguous column name", node)
|
|
725
|
+
end
|
|
726
|
+
elsif known.empty? && unknown.size == 1
|
|
727
|
+
read_through(unknown.first, name, usage, node)
|
|
728
|
+
elsif unknown.any? && (known.size + unknown.size) > 1
|
|
729
|
+
unresolved(name, "ambiguous column name without a catalog", node)
|
|
730
|
+
else
|
|
731
|
+
return false
|
|
732
|
+
end
|
|
733
|
+
true
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def source_has?(source, name)
|
|
737
|
+
return source.inner_sources.any? { |m| source_has?(m, name) } if source.kind == :group
|
|
738
|
+
|
|
739
|
+
!!source.column(name)
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def source_unknown?(source)
|
|
743
|
+
return source.inner_sources.any? { |m| source_unknown?(m) } if source.kind == :group
|
|
744
|
+
|
|
745
|
+
source.columns.nil?
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def rowid(scope, name, usage, node)
|
|
749
|
+
return false unless Catalog::ROWID_NAMES.include?(name.downcase(:ascii))
|
|
750
|
+
|
|
751
|
+
candidates = scope.sources.select { |s| s.kind == :table && (s.table.nil? || s.table.rowid?) }
|
|
752
|
+
return false unless candidates.size == 1
|
|
753
|
+
|
|
754
|
+
read_through(candidates.first, name, usage, node)
|
|
755
|
+
true
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def read_through(source, name, usage, node)
|
|
759
|
+
case source.kind
|
|
760
|
+
when :table
|
|
761
|
+
if source.table
|
|
762
|
+
column = source.table.column(name)
|
|
763
|
+
column ||= source.table.rowid_alias || "rowid" if Catalog::ROWID_NAMES.include?(name.downcase(:ascii)) && source.table.rowid?
|
|
764
|
+
return unresolved(name, "no such column in #{source.table.name}", node) unless column
|
|
765
|
+
|
|
766
|
+
record(source.table.schema, source.table.name, column, usage, node)
|
|
767
|
+
else
|
|
768
|
+
record(source.schema, source.node.table, name, usage, node)
|
|
769
|
+
end
|
|
770
|
+
when :group
|
|
771
|
+
member = source.inner_sources.find { |m| source_has?(m, name) } || source.inner_sources.find { |m| source_unknown?(m) }
|
|
772
|
+
member ? read_through(member, name, usage, node) : unresolved(name, "no such column", node)
|
|
773
|
+
when :derived, :function
|
|
774
|
+
if source.columns && !source.column(name) && !source.columns.include?(nil)
|
|
775
|
+
unresolved(name, "no such column in #{source.name}", node)
|
|
776
|
+
end
|
|
777
|
+
end
|
|
778
|
+
true
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def alias_of(name, schema, table)
|
|
782
|
+
@reads.aliases[name] = Analysis.table_key(schema, table) if name && @expanding_views.empty?
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def record(schema, table, column, usage, node)
|
|
786
|
+
@reads.references << Reference.new(schema:, table:, column:, usage:, node:)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def unresolved(name, reason, node)
|
|
790
|
+
@reads.unresolved << Unresolved.new(name:, reason:, node:)
|
|
791
|
+
true
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def qualified_name(node) = [node.schema, node.table, node.column].compact.join(".")
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
end
|