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 @@ -416,7 +416,7 @@ private SqlTableReference.Resolution resolveColumnOrigins() throws SQLException
}
return new SqlTableReference.TableDefinition(
info.createSql(), new ArrayList<>(info.columns().keySet()));
});
}, cols.size());
}
columnOrigins = resolved;
tableContextResolved = true;
Expand Down Expand Up @@ -456,8 +456,9 @@ private TableInfo loadTableInfo(SqlTableReference.TableRef reference) throws SQL
private TableInfo loadTableInfo(String schema, String table) throws SQLException {
String master = schema.equals("temp") ? "sqlite_temp_master" : "sqlite_master";
String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk "
+ "FROM " + master + " m JOIN pragma_table_info(?, ?) p "
+ "FROM " + master + " m JOIN pragma_table_xinfo(?, ?) p "
+ "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? "
+ "AND p.hidden != 1 "
+ "ORDER BY p.cid";
StmtResult result = connection.executeStatement(
new Stmt(query, List.of(Value.of(table), Value.of(schema), Value.of(table))));
Expand All @@ -471,8 +472,9 @@ private TableInfo loadAttachedTableInfo(String schema, String table) throws SQLE
}
String quotedSchema = "\"" + actualSchema.replace("\"", "\"\"") + "\"";
String query = "SELECT m.name, m.sql, p.name, p.\"notnull\", p.pk "
+ "FROM " + quotedSchema + ".sqlite_master m JOIN pragma_table_info(?, ?) p "
+ "FROM " + quotedSchema + ".sqlite_master m JOIN pragma_table_xinfo(?, ?) p "
+ "WHERE m.type IN ('table', 'view') AND m.name COLLATE NOCASE = ? "
+ "AND p.hidden != 1 "
+ "ORDER BY p.cid";
StmtResult result = connection.executeStatement(new Stmt(
query, List.of(Value.of(table), Value.of(actualSchema), Value.of(table))));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,22 @@ boolean matchesQualifier(String qualifier) {

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) {

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);
}
}

private static final Pattern UNQUOTED_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_$]*");

private static final Set<String> FROM_END = Set.of(
Expand Down Expand Up @@ -128,6 +144,19 @@ static Resolution resolve(String sql, Schema schema) throws SQLException {
.parseStatement(0, tokens.size(), 0, new HashMap<>());
}

/**
* Resolves column origins and discards the resolution when the parser expansion does not match
* the result width. The protocol column list is authoritative for width, and a shifted
* expansion would attribute every later column to the wrong table.
*/
static Resolution resolve(String sql, Schema schema, int columnCount) throws SQLException {
Resolution resolution = resolve(sql, schema);
if (resolution == null || resolution.labels().size() != columnCount) {
return null;
}
return resolution;
}

private static final class Parser {

private final List<Token> tokens;
Expand Down Expand Up @@ -233,41 +262,53 @@ private Resolution parseSelect(int start, int end, int depth, Map<String, Source
}
int setOperator = indexOfAnyWord(index, end, depth, SET_OPERATORS);
int selectEnd = setOperator >= 0 ? setOperator : end;
int from = indexOfWord(index, selectEnd, depth, "FROM");
int listEnd = from >= 0 ? from : selectEnd;
List<Source> sources = List.of();
if (from >= 0) {
int fromEnd = indexOfAnyWord(from + 1, selectEnd, depth, FROM_END);
int fromIndex = indexOfWord(index, selectEnd, depth, "FROM");
int listEnd = fromIndex >= 0 ? fromIndex : selectEnd;
FromClause from = FromClause.of(List.of());
if (fromIndex >= 0) {
int fromEnd = indexOfAnyWord(fromIndex + 1, selectEnd, depth, FROM_END);
if (fromEnd < 0) {
fromEnd = selectEnd;
}
List<Source> parsed = parseSources(from + 1, fromEnd, depth, scope);
if (parsed == null || parsed.isEmpty()) {
FromClause parsed = parseSources(fromIndex + 1, fromEnd, depth, scope);
if (parsed == null || parsed.sources().isEmpty()) {
return null;
}
sources = parsed;
from = parsed;
}
return parseSelectItems(index, listEnd, depth, sources, scope);
return parseSelectItems(index, listEnd, depth, from, scope);
}

private List<Source> parseSources(int start, int end, int depth, Map<String, Source> scope)
private FromClause parseSources(int start, int end, int depth, Map<String, Source> scope)
throws SQLException {
List<Source> sources = new ArrayList<>();
List<String> starLabels = new ArrayList<>();
List<TableRef> starTables = new ArrayList<>();
boolean natural = false;
int index = start;
while (index < end) {
SourceList parsed = parseSource(index, end, depth, scope);
if (parsed == null) {
return null;
}
sources.addAll(parsed.sources());
int starStart = starLabels.size();
for (Source source : parsed.sources()) {
sources.add(source);
appendStarColumns(source, natural, starLabels, starTables);
}
index = parsed.next();
index = skipIndexModifiers(index, end, depth);
if (isWordAt(index, depth, "USING")) {
dropStarColumns(usingColumns(index, end, depth), starStart, starLabels, starTables);
}
index = skipJoinCondition(index, end, depth);
if (isSymbolAt(index, ",", depth)) {
natural = false;
index++;
continue;
}
if (index < end && isJoinStart(index, end, depth)) {
natural = isWordAt(index, depth, "NATURAL");
index = skipToJoin(index, end, depth);
if (index < 0) {
break;
Expand All @@ -277,7 +318,7 @@ private List<Source> parseSources(int start, int end, int depth, Map<String, Sou
}
break;
}
return sources;
return new FromClause(sources, starLabels, starTables);
}

private SourceList parseSource(int start, int end, int depth, Map<String, Source> scope)
Expand Down Expand Up @@ -323,7 +364,7 @@ private SourceList parseSource(int start, int end, int depth, Map<String, Source
return new SourceList(
List.of(new Source(alias, labels, inner.tables(), true, null)), index);
}
List<Source> grouped = parseSources(index + 1, close, innerDepth, scope);
FromClause grouped = parseSources(index + 1, close, innerDepth, scope);
if (grouped == null) {
return null;
}
Expand All @@ -341,18 +382,11 @@ private SourceList parseSource(int start, int end, int depth, Map<String, Source
index++;
}
if (alias == null) {
return new SourceList(grouped, index);
}
List<String> labels = new ArrayList<>();
List<TableRef> tables = new ArrayList<>();
for (Source source : grouped) {
if (source.knownColumns()) {
labels.addAll(source.columns());
tables.addAll(source.tables());
}
return new SourceList(grouped.sources(), index);
}
return new SourceList(
List.of(new Source(alias, labels, tables, true, null)), index);
List.of(new Source(alias, grouped.starLabels(), grouped.starTables(), true, null)),
index);
}

String first = identifier(index);
Expand Down Expand Up @@ -502,7 +536,7 @@ private Resolution parseReturning(int start, int end, int depth, Map<String, Sou
true,
target);
}
return parseSelectItems(returning + 1, end, depth, List.of(source), scope);
return parseSelectItems(returning + 1, end, depth, FromClause.of(List.of(source)), scope);
}

private TableRef targetTable(int start, int end, int depth) {
Expand Down Expand Up @@ -555,15 +589,15 @@ private TableRef targetTable(int start, int end, int depth) {
}

private Resolution parseSelectItems(
int start, int end, int depth, List<Source> sources, Map<String, Source> scope)
int start, int end, int depth, FromClause from, Map<String, Source> scope)
throws SQLException {
List<String> labels = new ArrayList<>();
List<TableRef> tables = new ArrayList<>();
int index = start;
while (index < end) {
int comma = indexOfSymbol(index, end, depth, ",");
int itemEnd = comma < 0 ? end : comma;
parseItem(index, itemEnd, depth, sources, scope, labels, tables);
parseItem(index, itemEnd, depth, from, scope, labels, tables);
if (comma < 0) {
break;
}
Expand All @@ -576,7 +610,7 @@ private void parseItem(
int start,
int end,
int depth,
List<Source> sources,
FromClause from,
Map<String, Source> scope,
List<String> labels,
List<TableRef> tables)
Expand Down Expand Up @@ -609,19 +643,15 @@ private void parseItem(
}

if (expressionEnd - start == 1 && isSymbol(tokens.get(start), "*")) {
for (Source source : sources) {
if (source.knownColumns()) {
labels.addAll(source.columns());
tables.addAll(source.tables());
}
}
labels.addAll(from.starLabels());
tables.addAll(from.starTables());
return;
}

ColumnRef qualifiedStar = qualifiedStar(start, expressionEnd);
if (qualifiedStar != null) {
Source source =
findSource(qualifiedStar.parts().get(qualifiedStar.parts().size() - 1), sources);
Source source = findSource(
qualifiedStar.parts().get(qualifiedStar.parts().size() - 1), from.sources());
if (source != null && source.knownColumns()) {
labels.addAll(source.columns());
tables.addAll(source.tables());
Expand All @@ -632,7 +662,7 @@ private void parseItem(
ColumnRef column = simpleColumn(start, expressionEnd);
if (column != null) {
labels.add(alias != null ? alias : column.column());
tables.add(resolveColumn(column, sources));
tables.add(resolveColumn(column, from.sources()));
return;
}

Expand Down Expand Up @@ -850,6 +880,17 @@ private boolean isAlias(int index, int depth) {
return true;
}

private List<String> usingColumns(int start, int end, int depth) {
if (!isSymbolAt(start + 1, "(", depth)) {
return List.of();
}
int close = matchingParen(start + 1);
if (close < 0 || close > end) {
return List.of();
}
return identifiers(start + 2, close, depth + 1);
}

private List<String> identifiers(int start, int end, int depth) {
List<String> identifiers = new ArrayList<>();
int index = start;
Expand Down Expand Up @@ -1036,6 +1077,40 @@ private static String upper(String value) {
return value.toUpperCase(Locale.ROOT);
}

private static void appendStarColumns(
Source source, boolean merged, List<String> labels, List<TableRef> tables) {
if (!source.knownColumns()) {
return;
}
for (int i = 0; i < source.columns().size(); i++) {
String label = source.columns().get(i);
if (merged && containsIgnoreCase(labels, label)) {
continue;
}
labels.add(label);
tables.add(source.tables().get(i));
}
}

private static void dropStarColumns(
List<String> columns, int from, List<String> labels, List<TableRef> tables) {
for (int i = labels.size() - 1; i >= from; i--) {
if (containsIgnoreCase(columns, labels.get(i))) {
labels.remove(i);
tables.remove(i);
}
}
}

private static boolean containsIgnoreCase(List<String> values, String value) {
for (String candidate : values) {
if (candidate.equalsIgnoreCase(value)) {
return true;
}
}
return false;
}

private static List<Token> tokenize(String sql) {
List<Token> tokens = new ArrayList<>();
int depth = 0;
Expand Down
Loading
Loading