Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ final class SqlTableReference {

record TableRef(String schema, String table) {}

record Target(TableRef table, String alias) {}

interface Schema {
TableDefinition definition(TableRef table) throws SQLException;
}
Expand All @@ -51,6 +53,7 @@ TableRef table(int index) {
private enum Type {
WORD,
IDENT,
LITERAL,
SYMBOL
}

Expand Down Expand Up @@ -102,15 +105,19 @@ private record SourceList(List<Source> sources, int next) {}
* The sources of a FROM clause together with the column list {@code *} expands to. The star
* list drops the right-side duplicate of a USING or NATURAL join column, as SQLite does.
*/
private record FromClause(List<Source> sources, List<String> starLabels, List<TableRef> starTables) {
private record FromClause(
List<Source> sources,
List<String> starLabels,
List<TableRef> starTables,
List<String> mergedColumns) {

static FromClause of(List<Source> sources) {
List<String> labels = new ArrayList<>();
List<TableRef> tables = new ArrayList<>();
for (Source source : sources) {
appendStarColumns(source, false, labels, tables);
}
return new FromClause(sources, labels, tables);
return new FromClause(sources, labels, tables, List.of());
}
}

Expand All @@ -126,9 +133,9 @@ static FromClause of(List<Source> sources) {
private static final Set<String> SET_OPERATORS = Set.of("UNION", "INTERSECT", "EXCEPT");

private static final Set<String> NOT_AN_ALIAS = Set.of(
"AS", "ON", "USING", "INDEXED", "NOT", "COLLATE", "IS", "LIKE", "GLOB", "REGEXP",
"MATCH", "ESCAPE", "BETWEEN", "IN", "CASE", "WHEN", "THEN", "ELSE", "END", "CAST",
"EXISTS", "AND", "OR", "NULL");
"AS", "ON", "USING", "INDEXED", "NOT", "COLLATE", "IS", "ISNULL", "NOTNULL", "LIKE",
"GLOB", "REGEXP", "MATCH", "ESCAPE", "BETWEEN", "IN", "CASE", "WHEN", "THEN", "ELSE",
"END", "CAST", "EXISTS", "AND", "OR", "NULL");

private SqlTableReference() {}

Expand Down Expand Up @@ -240,6 +247,16 @@ private Resolution parseWith(int start, int end, int depth, Map<String, Source>
resolution.tables(),
true,
null));
} else {
List<String> opaque = columns != null ? columns : List.of();
ctes.put(
name.toLowerCase(Locale.ROOT),
new Source(
name,
opaque,
Collections.nCopies(opaque.size(), null),
columns != null,
null));
}
index = close + 1;
if (isSymbolAt(index, ",", depth)) {
Expand Down Expand Up @@ -284,6 +301,7 @@ private FromClause parseSources(int start, int end, int depth, Map<String, Sourc
List<Source> sources = new ArrayList<>();
List<String> starLabels = new ArrayList<>();
List<TableRef> starTables = new ArrayList<>();
List<String> mergedColumns = new ArrayList<>();
boolean natural = false;
int index = start;
while (index < end) {
Expand All @@ -299,7 +317,13 @@ private FromClause parseSources(int start, int end, int depth, Map<String, Sourc
index = parsed.next();
index = skipIndexModifiers(index, end, depth);
if (isWordAt(index, depth, "USING")) {
dropStarColumns(usingColumns(index, end, depth), starStart, starLabels, starTables);
List<String> merged = usingColumns(index, end, depth);
dropStarColumns(merged, starStart, starLabels, starTables);
for (String column : merged) {
if (!containsIgnoreCase(mergedColumns, column)) {
mergedColumns.add(column);
}
}
}
index = skipJoinCondition(index, end, depth);
if (isSymbolAt(index, ",", depth)) {
Expand All @@ -318,7 +342,7 @@ private FromClause parseSources(int start, int end, int depth, Map<String, Sourc
}
break;
}
return new FromClause(sources, starLabels, starTables);
return new FromClause(sources, starLabels, starTables, mergedColumns);
}

private SourceList parseSource(int start, int end, int depth, Map<String, Source> scope)
Expand Down Expand Up @@ -515,31 +539,32 @@ private Resolution parseReturning(int start, int end, int depth, Map<String, Sou
if (returning < 0) {
return null;
}
TableRef target = targetTable(start, returning, depth);
Target target = targetTable(start, returning, depth);
if (target == null) {
return null;
}
TableDefinition definition = schema.definition(target);
String qualifier = target.alias() != null ? target.alias() : target.table().table();
TableDefinition definition = schema.definition(target.table());
Source source;
if (definition == null) {
source = new Source(target.table(), List.of(), List.of(), false, target);
source = new Source(qualifier, List.of(), List.of(), false, target.table());
} else if (isViewDefinition(definition.sql())) {
List<String> columns = definition.columns();
source = new Source(
target.table(), columns, Collections.nCopies(columns.size(), null), true, null);
qualifier, columns, Collections.nCopies(columns.size(), null), true, null);
} else {
List<String> columns = definition.columns() != null ? definition.columns() : List.of();
source = new Source(
target.table(),
qualifier,
columns,
Collections.nCopies(columns.size(), target),
Collections.nCopies(columns.size(), target.table()),
true,
target);
target.table());
}
return parseSelectItems(returning + 1, end, depth, FromClause.of(List.of(source)), scope);
}

private TableRef targetTable(int start, int end, int depth) {
private Target targetTable(int start, int end, int depth) {
String keyword = wordAt(start, depth);
if (keyword == null) {
return null;
Expand Down Expand Up @@ -585,7 +610,14 @@ private TableRef targetTable(int start, int end, int depth) {
schemaName = first;
first = second;
}
return new TableRef(schemaName, first);
String alias = null;
if (isWordAt(index, depth, "AS")) {
alias = identifier(index + 1);
if (alias == null) {
return null;
}
}
return new Target(new TableRef(schemaName, first), alias);
}

private Resolution parseSelectItems(
Expand Down Expand Up @@ -662,7 +694,7 @@ private void parseItem(
ColumnRef column = simpleColumn(start, expressionEnd);
if (column != null) {
labels.add(alias != null ? alias : column.column());
tables.add(resolveColumn(column, from.sources()));
tables.add(resolveColumn(column, from));
return;
}

Expand All @@ -683,7 +715,8 @@ private void parseItem(
tables.add(null);
}

private TableRef resolveColumn(ColumnRef column, List<Source> sources) {
private TableRef resolveColumn(ColumnRef column, FromClause from) {
List<Source> sources = from.sources();
String qualifier = column.qualifier();
if (qualifier != null) {
Source source = findSource(qualifier, sources);
Expand All @@ -709,6 +742,13 @@ private TableRef resolveColumn(ColumnRef column, List<Source> sources) {
return match;
}
if (matches > 1) {
if (containsIgnoreCase(from.mergedColumns(), column.column())) {
for (Source source : sources) {
if (source.hasColumn(column.column())) {
return source.origin(column.column());
}
}
}
return null;
}
if (sources.size() == 1 && !sources.get(0).knownColumns()) {
Expand All @@ -734,11 +774,14 @@ private int implicitAlias(int start, int end, int depth) {
if (identifierOf(last) == null) {
return -1;
}
if (last.type() == Type.WORD && NOT_AN_ALIAS.contains(upper(last.text()))) {
return -1;
}
Token previous = tokens.get(end - 2);
if (isSymbol(previous, ".") || isSymbol(previous, "(") || isSymbol(previous, ",")) {
return -1;
}
if (previous.type() == Type.SYMBOL) {
if (previous.type() == Type.SYMBOL && !isSymbol(previous, ")")) {
return -1;
}
if (previous.type() == Type.WORD && NOT_AN_ALIAS.contains(upper(previous.text()))) {
Expand Down Expand Up @@ -820,7 +863,7 @@ private int skipIndexModifiers(int start, int end, int depth) {
continue;
}
if (isWordAt(index, depth, "NOT") && isWordAt(index + 1, depth, "INDEXED")) {
index += 3;
index += 2;
continue;
}
break;
Expand Down Expand Up @@ -849,9 +892,17 @@ private boolean isJoinStart(int index, int end, int depth) {
return false;
}
Token token = tokens.get(index);
return token.depth() == depth
&& token.type() == Type.WORD
&& JOIN_START.contains(upper(token.text()));
if (token.depth() != depth || token.type() != Type.WORD) {
return false;
}
String keyword = upper(token.text());
if (!JOIN_START.contains(keyword)) {
return false;
}
if ("LEFT".equals(keyword) || "RIGHT".equals(keyword)) {
return isWordAt(index + 1, depth, "JOIN") || isWordAt(index + 1, depth, "OUTER");
}
return true;
}

private int skipToJoin(int start, int end, int depth) {
Expand Down Expand Up @@ -1136,11 +1187,14 @@ private static List<Token> tokenize(String sql) {
i = end + 2;
continue;
}
if (c == '\'') {
i = scanQuoted(sql, i, '\'');
if (i < 0) {
if (c == '\'' || ((c == 'x' || c == 'X') && i + 1 < sql.length() && sql.charAt(i + 1) == '\'')) {
int quote = c == '\'' ? i : i + 1;
int end = scanQuoted(sql, quote, '\'');
if (end < 0) {
return null;
}
tokens.add(new Token(Type.LITERAL, sql.substring(i, end), depth));
i = end;
continue;
}
if (c == '"' || c == '`') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,80 @@ void leavesExpressionsAndLiteralsUnresolved() throws SQLException {
assertThat(literal.tables()).containsExactly((SqlTableReference.TableRef) null);
}

@Test
void emitsLiteralTokensForStringAndBlobLiterals() throws SQLException {
SqlTableReference.Resolution aliased = resolve("SELECT 'lit' z FROM c");

assertThat(aliased.labels()).containsExactly("z");
assertThat(aliased.tables()).containsExactly((SqlTableReference.TableRef) null);
assertThat(resolve("SELECT x'41' AS b FROM a").tables())
.containsExactly((SqlTableReference.TableRef) null);

SqlTableReference.Resolution compared = resolve("SELECT x LIKE 'a%' FROM a");
assertThat(compared.tables()).containsExactly((SqlTableReference.TableRef) null);
assertThat(compared.labels()).doesNotContain("LIKE", "a%");
}

@Test
void rejectsOperatorKeywordsAsImplicitAliases() throws SQLException {
SqlTableReference.Resolution isnull = resolve("SELECT x ISNULL FROM a");
assertThat(isnull.labels()).doesNotContain("ISNULL");
assertThat(isnull.tables()).containsExactly((SqlTableReference.TableRef) null);

SqlTableReference.Resolution notnull = resolve("SELECT x NOTNULL FROM a");
assertThat(notnull.labels()).doesNotContain("NOTNULL");
assertThat(notnull.tables()).containsExactly((SqlTableReference.TableRef) null);

SqlTableReference.Resolution caseExpression = resolve("SELECT CASE WHEN x = 1 THEN id ELSE 2 END FROM a");
assertThat(caseExpression.labels()).doesNotContain("END");

SqlTableReference.Resolution counted = resolve("SELECT count(*) c FROM a");
assertThat(counted.labels()).containsExactly("c");
assertThat(counted.tables()).containsExactly((SqlTableReference.TableRef) null);
}

@Test
void keepsTheJoinThatFollowsNotIndexed() throws SQLException {
SqlTableReference.Resolution resolution =
resolve("SELECT b.y FROM a NOT INDEXED JOIN b ON a.id = b.id");

assertThat(resolution.labels()).containsExactly("y");
assertThat(resolution.tables()).containsExactly(new SqlTableReference.TableRef(null, "b"));
}

@Test
void doesNotReadLeftOrRightCallsAsJoinStarts() throws SQLException {
assertThat(resolve("SELECT c.z FROM a JOIN b ON left(a.x, 2) = b.y, c").tables())
.containsExactly(new SqlTableReference.TableRef(null, "c"));
assertThat(resolve("SELECT c.z FROM a JOIN b ON right(a.x, 2) = b.y, c").tables())
.containsExactly(new SqlTableReference.TableRef(null, "c"));
}

@Test
void registersUnresolvedCommonTableExpressionsAsOpaque() throws SQLException {
assertThat(resolve("WITH c AS (VALUES (1)) SELECT c.z FROM c").tables())
.containsExactly((SqlTableReference.TableRef) null);
assertThat(resolve("WITH c(z) AS (VALUES (1)) SELECT z FROM c").tables())
.containsExactly((SqlTableReference.TableRef) null);
}

@Test
void resolvesUnqualifiedUsingColumnToTheLeftTable() throws SQLException {
assertThat(resolve("SELECT id FROM a JOIN b USING(id)").tables())
.containsExactly(new SqlTableReference.TableRef(null, "a"));
}

@Test
void resolvesReturningColumnsThroughTheTargetAlias() throws SQLException {
SqlTableReference.Resolution resolution =
resolve("INSERT INTO a AS t (id, x) VALUES (1, 'v') RETURNING t.id, t.x");

assertThat(resolution.tables())
.containsExactly(
new SqlTableReference.TableRef(null, "a"),
new SqlTableReference.TableRef(null, "a"));
}

@Test
void acceptsTrailingSemicolon() throws SQLException {
assertThat(resolve("SELECT a FROM w;").tables())
Expand Down
Loading