1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! What names are visible, and what they resolve to.
//!
//! A scope is a flat list of visible columns in the order they would come out of a `SELECT *`. It
//! is flat rather than a map because the list is short, because the order is part of the answer,
//! and because ambiguity is a question about the whole list rather than about one bucket of it.
//!
//! Every entry carries the table name it came in under, which is the alias if there was one and the
//! table's own name if there was not. That is the name `t.x` matches against and the name an error
//! message should use, and it is deliberately not the catalog name: after `FROM hits AS h` there is
//! no `hits` to refer to, which is SQL's rule and not ours.
use rudb_catalog::same_name;
use rudb_common::{Error, Field, LogicalType, Result};
use rudb_plan::ColumnBinding;
/// One visible column.
#[derive(Debug, Clone)]
pub(crate) struct Visible {
/// The table name it is reachable through, empty for a column of no table.
pub(crate) table: String,
/// The column name.
pub(crate) name: String,
/// Where it comes from in the plan.
pub(crate) binding: ColumnBinding,
/// What it is.
pub(crate) ty: LogicalType,
/// Whether the column it came from refuses nulls.
///
/// Only `DESCRIBE` reads this, and only to fill the `null` column with `NO` or `YES`. It is
/// carried on the scope rather than asked of the plan because the question is about where a
/// column came from and the scope is the only thing that still knows: by the time a projection
/// is a node, a column that is passed straight through and one that is computed look the same.
///
/// A column that is not a plain reference is nullable whatever it was built from, which is
/// also what the reference binary says. `DESCRIBE SELECT * FROM t` keeps `NO` on a `NOT NULL`
/// column and `DESCRIBE SELECT c + 0 FROM t` does not.
pub(crate) not_null: bool,
}
/// The columns a name can resolve against.
#[derive(Debug, Clone, Default)]
pub(crate) struct Scope {
pub(crate) columns: Vec<Visible>,
}
impl Scope {
/// A scope with nothing in it, which is what `SELECT 1` binds against.
pub(crate) fn empty() -> Self {
Self { columns: Vec::new() }
}
/// Everything on the left followed by everything on the right, which is what a join sees.
pub(crate) fn concat(mut self, other: Self) -> Self {
self.columns.extend(other.columns);
self
}
pub(crate) fn push(&mut self, column: Visible) {
self.columns.push(column);
}
pub(crate) fn len(&self) -> usize {
self.columns.len()
}
/// Resolves a written name to one column.
///
/// One part is a column name and it has to be unique across every table in scope. Two parts are
/// a table and a column. Three and four parts have a schema and a catalog in front, and they
/// are matched against the table part only, because a table in scope has one name here and the
/// qualification is decoration once it is in the `FROM` clause.
///
/// # Errors
///
/// If nothing matches, or if one part matches more than one column. The messages are DuckDB's.
pub(crate) fn resolve(&self, parts: &[&str]) -> Result<&Visible> {
if let Some(visible) = self.resolve_optional(parts)? {
return Ok(visible);
}
let (table, column) = match parts {
[column] => (None, *column),
[table, column] => (Some(*table), *column),
[_, table, column] | [_, _, table, column] => (Some(*table), *column),
_ => {
return Err(Error::binder(format!(
"Referenced column \"{}\" has too many parts to be a column name",
parts.join(".")
)));
}
};
Err(self.not_found(table, column))
}
/// Resolves a name when it is present, while still reporting ambiguity.
pub(crate) fn resolve_optional(&self, parts: &[&str]) -> Result<Option<&Visible>> {
let (table, column) = match parts {
[column] => (None, *column),
[table, column] => (Some(*table), *column),
[_, table, column] | [_, _, table, column] => (Some(*table), *column),
_ => {
return Err(Error::binder(format!(
"Referenced column \"{}\" has too many parts to be a column name",
parts.join(".")
)));
}
};
let matched: Vec<&Visible> = self
.columns
.iter()
.filter(|held| {
same_name(&held.name, column)
&& table.is_none_or(|table| same_name(&held.table, table))
})
.collect();
match matched.as_slice() {
[one] => Ok(Some(one)),
[] => Ok(None),
many => {
let candidates: Vec<String> =
many.iter().map(|held| format!("{}.{}", held.table, held.name)).collect();
// The column name is in double quotes and the candidates under it are in single
// ones, which reads like a mistake and is what the pin prints:
// `Ambiguous reference to column name "a" (use: 't.a' or 'u.a')`.
Err(Error::binder(format!(
"Ambiguous reference to column name \"{column}\" (use: '{}')",
candidates.join("' or '")
)))
}
}
}
/// The columns a star expands to.
///
/// # Errors
///
/// If the qualifier names no table in scope, or if there is nothing in scope at all, which is
/// `SELECT *` with no `FROM` clause and is an error rather than zero columns.
pub(crate) fn star(&self, qualifier: Option<&str>) -> Result<Vec<&Visible>> {
let matched: Vec<&Visible> = match qualifier {
None => self.columns.iter().collect(),
Some(table) => {
self.columns.iter().filter(|held| same_name(&held.table, table)).collect()
}
};
if matched.is_empty() {
return Err(match qualifier {
Some(table) => {
Error::binder(format!("Referenced table \"{table}\" not found in FROM clause!"))
}
None => Error::binder("* is not allowed in a query without a FROM clause"),
});
}
Ok(matched)
}
/// Renames every column's table, which is what an alias on a subquery or a table does.
pub(crate) fn relabel(&mut self, table: &str) {
for column in &mut self.columns {
column.table = table.to_string();
}
}
/// Replaces the column names, which is what `AS t(a, b)` does.
///
/// # Errors
///
/// If there are more names than columns, which DuckDB reports rather than ignoring.
pub(crate) fn rename(&mut self, names: &[&str], what: &str) -> Result<()> {
if names.len() > self.columns.len() {
return Err(Error::binder(format!(
"table \"{what}\" has {} columns available but {} columns specified",
self.columns.len(),
names.len()
)));
}
self.rename_prefix(names);
Ok(())
}
/// Replaces the column names, ignoring every name past the last column.
///
/// The column list a `WITH` definition is written with is the one list DuckDB does not report
/// as too long. `WITH c(a, b, d, e) AS (SELECT 1, 2) SELECT * FROM c` answers two columns named
/// `a` and `b` on the pinned build, where the same list on a table alias is refused and where
/// PostgreSQL refuses both. That is reproduced rather than corrected, and it is filed as
/// tamnd/duckdb#8.
pub(crate) fn rename_prefix(&mut self, names: &[&str]) {
for (column, name) in self.columns.iter_mut().zip(names) {
column.name = (*name).to_string();
}
}
/// The visible columns as fields, which is what a view writes down for the catalog tables.
///
/// The table name each one is reachable through is dropped, because a field is a name and a
/// type and the catalog already knows which view it is looking at.
pub(crate) fn fields(&self) -> Vec<Field> {
self.columns
.iter()
.map(|column| Field::new(column.name.clone(), column.ty.clone()))
.collect()
}
/// Drops the column at `position`, which is what `USING` does to the right side's copy.
pub(crate) fn remove(&mut self, position: usize) {
self.columns.remove(position);
}
/// Drops everything from `position` on, which is what a semi or an anti join does to the right
/// side once its condition has been bound.
pub(crate) fn truncate(&mut self, position: usize) {
self.columns.truncate(position);
}
/// Where a column of that name sits, if exactly one does.
pub(crate) fn position_of(&self, table: Option<&str>, name: &str) -> Option<usize> {
let mut found = None;
for (at, held) in self.columns.iter().enumerate() {
if same_name(&held.name, name)
&& table.is_none_or(|table| same_name(&held.table, table))
{
if found.is_some() {
return None;
}
found = Some(at);
}
}
found
}
/// Whether anything in scope answers to this name, in any table.
///
/// Not the same question as [`Scope::position_of`], which says no when two columns match. This
/// one says yes, because the caller is `current_date` asking whether it is a column here at all
/// and two columns called `current_date` is the ambiguity error rather than the session constant.
/// That was measured: `SELECT current_date FROM t, u` with the name in both is
/// `Ambiguous reference to column name "current_date"` on the pin.
pub(crate) fn names(&self, column: &str) -> bool {
self.columns.iter().any(|held| same_name(&held.name, column))
}
fn not_found(&self, table: Option<&str>, column: &str) -> Error {
match table {
Some(table) if self.columns.iter().all(|held| !same_name(&held.table, table)) => {
Error::binder(format!("Referenced table \"{table}\" not found in FROM clause!"))
}
Some(table) => Error::binder(format!(
"Referenced column \"{column}\" not found in table \"{table}\"!"
)),
None => Error::binder(format!(
"Referenced column \"{column}\" not found in FROM clause!{}",
self.candidates()
)),
}
}
/// The `Candidate bindings:` part of a complaint about a name that is not here, empty when
/// there is nothing in scope to suggest.
///
/// On its own line in the binary and on the same line here, because an error is one line here
/// and the sentence before it is the part anybody matches on.
pub(crate) fn candidates(&self) -> String {
let candidates: Vec<&str> = self.columns.iter().map(|held| held.name.as_str()).collect();
if candidates.is_empty() {
String::new()
} else {
format!(" Candidate bindings: \"{}\"", candidates.join("\", \""))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scope() -> Scope {
let mut scope = Scope::empty();
scope.push(Visible {
table: "hits".into(),
name: "UserID".into(),
binding: ColumnBinding::new(0, 0),
ty: LogicalType::BigInt,
not_null: false,
});
scope.push(Visible {
table: "hits".into(),
name: "url".into(),
binding: ColumnBinding::new(0, 1),
ty: LogicalType::Varchar,
not_null: false,
});
scope.push(Visible {
table: "visits".into(),
name: "url".into(),
binding: ColumnBinding::new(1, 0),
ty: LogicalType::Varchar,
not_null: false,
});
scope
}
#[test]
fn a_unique_name_resolves_without_a_table() {
let scope = scope();
let found = scope.resolve(&["userid"]).expect("one column is called that");
assert_eq!(found.binding, ColumnBinding::new(0, 0));
}
#[test]
fn a_name_in_two_tables_needs_the_table() {
let scope = scope();
let error = scope.resolve(&["url"]).expect_err("two columns are called url");
assert!(error.message().contains("Ambiguous"), "{error}");
let found = scope.resolve(&["visits", "url"]).expect("qualified");
assert_eq!(found.binding, ColumnBinding::new(1, 0));
}
#[test]
fn a_name_that_is_not_there_lists_what_is() {
let error = scope().resolve(&["nope"]).expect_err("no such column");
assert!(error.message().contains("not found in FROM clause"), "{error}");
assert!(error.message().contains("UserID"), "the message should say what is there");
}
#[test]
fn a_table_that_is_not_there_says_that_rather_than_naming_the_column() {
let error = scope().resolve(&["nope", "url"]).expect_err("no such table");
assert!(error.message().contains("Referenced table \"nope\""), "{error}");
}
#[test]
fn a_star_expands_in_order_and_a_qualified_one_expands_to_its_table() {
let scope = scope();
let all = scope.star(None).expect("three columns");
assert_eq!(all.len(), 3);
assert_eq!(all[0].name, "UserID");
let one = scope.star(Some("VISITS")).expect("one column, case insensitively");
assert_eq!(one.len(), 1);
assert_eq!(one[0].binding, ColumnBinding::new(1, 0));
}
#[test]
fn a_qualified_name_ignores_the_schema_in_front_of_it() {
let scope = scope();
let found = scope.resolve(&["memory", "main", "hits", "UserID"]).expect("four parts");
assert_eq!(found.binding, ColumnBinding::new(0, 0));
}
}