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
77 changes: 49 additions & 28 deletions src/main/java/net/sf/jsqlparser/schema/Column.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ public Column(List<String> nameParts) {
}

public Column(List<String> nameParts, List<String> delimiters) {
this(
nameParts.size() > 1 ? new Table(nameParts.subList(0, nameParts.size() - 1),
delimiters.subList(0, delimiters.size() - 1)) : null,
nameParts.get(nameParts.size() - 1));
if (nameParts.size() > 1) {
setTable(new Table(nameParts.subList(0, nameParts.size() - 1),
delimiters.subList(0, delimiters.size() - 1)));
// The final component is already separated from the table name.
setName(nameParts.get(nameParts.size() - 1), false);
} else {
setColumnName(nameParts.get(0));
}
setTableDelimiter(delimiters.isEmpty() ? "." : delimiters.get(delimiters.size() - 1));
}

Expand Down Expand Up @@ -149,36 +153,25 @@ public void setColumnName(String name) {
}

public void setName(String name, boolean splitNamesOnDelimiter) {
if (MultiPartName.isQuoted(name) && name.contains(".") && splitNamesOnDelimiter) {
String[] parts = MultiPartName.unquote(name).split("\\.");
switch (parts.length) {
case 3:
this.table = new Table("\"" + parts[0] + "\".\"" + parts[1] + "\"");
this.columnName = "\"" + parts[2] + "\"";
break;
case 2:
this.table = new Table("\"" + parts[0] + "\"");
this.columnName = "\"" + parts[1] + "\"";
break;
case 1:
this.columnName = "\"" + parts[0] + "\"";
break;
default:
throw new RuntimeException("Invalid column name: " + name);
if (name.contains(".") && splitNamesOnDelimiter) {
List<String> parts = splitName(name);
if (parts.size() == 1 && MultiPartName.isQuoted(name)) {
parts.clear();
for (String identifier : MultiPartName.unquote(name).split("\\.")) {
parts.add("\"" + identifier + "\"");
}
}
} else if (name.contains(".") && splitNamesOnDelimiter) {
String[] parts = MultiPartName.unquote(name).split("\\.");
switch (parts.length) {
switch (parts.size()) {
case 3:
this.table = new Table(parts[0] + "." + parts[1]);
this.columnName = parts[2];
this.table = new Table(parts.subList(0, 2));
this.columnName = parts.get(2);
break;
case 2:
this.table = new Table(parts[0]);
this.columnName = parts[1];
this.table = new Table(parts.get(0), false);
this.columnName = parts.get(1);
break;
case 1:
this.columnName = parts[0];
this.columnName = parts.get(0);
break;
default:
throw new RuntimeException("Invalid column name: " + name);
Expand All @@ -188,6 +181,34 @@ public void setName(String name, boolean splitNamesOnDelimiter) {
}
}

private static List<String> splitName(String name) {
List<String> parts = new ArrayList<>();
int start = 0;
char quote = 0;
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (quote != 0) {
if (ch == quote) {
if (i + 1 < name.length() && name.charAt(i + 1) == quote) {
i++;
} else {
quote = 0;
}
}
} else if (ch == '"' || ch == '`' || ch == '[') {
quote = ch == '[' ? ']' : ch;
} else if (ch == '.') {
parts.add(name.substring(start, i));
start = i + 1;
}
}
parts.add(name.substring(start));
while (!parts.isEmpty() && parts.get(parts.size() - 1).isEmpty()) {
parts.remove(parts.size() - 1);
}
return parts;
}

public String getTableDelimiter() {
return tableDelimiter;
}
Expand Down
215 changes: 215 additions & 0 deletions src/test/java/net/sf/jsqlparser/schema/ColumnTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,34 @@
*/
package net.sf.jsqlparser.schema;

import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.select.PlainSelect;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
import org.junit.jupiter.api.parallel.Resources;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
import static net.sf.jsqlparser.test.TestUtils.assertStatementCanBeDeparsedAs;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;

/**
*
* @author tw
*/
@ResourceLock(Resources.SYSTEM_PROPERTIES)
public class ColumnTest {

@Test
Expand Down Expand Up @@ -54,4 +71,202 @@ public void testConstructorNamePartsAndDelimiters() {
assertThat(table.getNamePartDelimiters()).containsExactly(".", ":");
}

@ParameterizedTest
@MethodSource("qualifiedQuotedNames")
void testQualifiedQuotedNames(String name, List<String> tableParts, String columnName) {
assertColumn(new Column(name), name, tableParts, columnName);
Column column = new Column();
column.setName(name, true);
assertColumn(column, name, tableParts, columnName);
assertColumn(new Column().withColumnName(name), name, tableParts, columnName);
}

@ParameterizedTest
@MethodSource("separatedQuotedNames")
void testQuotedNameParts(String name, List<String> tableParts, String columnName) {
List<String> nameParts = new ArrayList<>(tableParts);
Collections.reverse(nameParts);
nameParts.add(columnName);
assertColumn(new Column(nameParts), name, tableParts, columnName);
assertColumn(new Column(nameParts, Collections.nCopies(nameParts.size() - 1, ".")),
name, tableParts, columnName);
}

@ParameterizedTest
@MethodSource("parsedQualifiedQuotedNames")
void testParsedQualifiedQuotedNames(String name, List<String> tableParts, String columnName)
throws JSQLParserException {
String sql = "SELECT " + name + " FROM t1";
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, false,
parser -> parser.withSquareBracketQuotation(true));
assertColumn(select.getSelectItem(0).getExpression(Column.class), name, tableParts,
columnName);
PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(select.toString(),
parser -> parser.withSquareBracketQuotation(true));
assertColumn(reparsed.getSelectItem(0).getExpression(Column.class), name, tableParts,
columnName);
}

@ParameterizedTest
@MethodSource("wholeQuotedNames")
void testWholeQuotedDottedNames(String name, String expected, List<String> tableParts,
String columnName) throws JSQLParserException {
assertColumn(new Column(name), expected, tableParts, columnName);
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + name + " FROM t1",
parser -> parser.withSquareBracketQuotation(true));
assertColumn(select.getSelectItem(0).getExpression(Column.class), expected, tableParts,
columnName);
assertStatementCanBeDeparsedAs(select, "SELECT " + expected + " FROM t1");
}

@ParameterizedTest
@ValueSource(strings = {"id", "`id`", "\"id\"", "[id]"})
void testUnqualifiedNames(String name) {
Column column = new Column(name);
assertEquals(name, column.getColumnName());
assertEquals(name, column.toString());
assertEquals("id", column.getUnquotedColumnName());
assertNull(column.getTable());
}

@ParameterizedTest
@MethodSource("legacyQualifiedTableNames")
void testLegacyQualifiedTableNames(String tableName, String expected,
List<String> tableParts) {
assertColumn(new Column(List.of(tableName, "id")), expected, tableParts, "id");
assertColumn(new Column(List.of(tableName, "id"), List.of(".")), expected,
tableParts, "id");
}

@ParameterizedTest
@ValueSource(booleans = {false, true})
void testBigQueryQualifiedTableColumn(boolean explicitDialect) throws JSQLParserException {
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
"SELECT `project.dataset.table`.id FROM `project.dataset.table`", parser -> {
if (explicitDialect) {
parser.withDialect(Dialect.BIGQUERY);
}
});
Column column = select.getSelectItem(0).getExpression(Column.class);
assertColumn(column, "\"project\".\"dataset\".\"table\".id",
List.of("\"table\"", "\"dataset\"", "\"project\""), "id");
assertEquals("project", column.getUnquotedCatalogName());
assertEquals("dataset", column.getUnquotedSchemaName());
assertEquals("table", column.getUnquotedTableName());
assertEquals(select.getFromItem(Table.class).getNameParts(),
column.getTable().getNameParts());
assertStatementCanBeDeparsedAs(select,
"SELECT \"project\".\"dataset\".\"table\".id FROM \"project\".\"dataset\".\"table\"");
}

@ParameterizedTest
@ValueSource(strings = {"t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id"})
void testExplicitlyDisabledSplitting(String name) {
Column column = new Column();
column.setName(name, false);
assertEquals(name, column.getColumnName());
assertEquals(name, column.toString());
assertNull(column.getTable());
}

@ParameterizedTest
@ValueSource(strings = {"false", "OFF", "0"})
void testSystemPropertyDisablesSplitting(String value) {
String previous = System.getProperty("SPLIT_NAMES_ON_DELIMITER");
try {
System.setProperty("SPLIT_NAMES_ON_DELIMITER", value);
for (String name : List.of("t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id")) {
Column column = new Column(name);
assertEquals(name, column.getColumnName());
assertEquals(name, column.toString());
assertNull(column.getTable());
}
} finally {
if (previous == null) {
System.clearProperty("SPLIT_NAMES_ON_DELIMITER");
} else {
System.setProperty("SPLIT_NAMES_ON_DELIMITER", previous);
}
}
}

private static void assertColumn(Column column, String name, List<String> tableParts,
String columnName) {
assertEquals(name, column.toString());
assertEquals(name, column.getFullyQualifiedName());
assertEquals(columnName, column.getColumnName());
assertThat(column.getTable().getNameParts()).containsExactlyElementsOf(tableParts);
assertEquals(tableParts.get(0), column.getTableName());
assertEquals(tableParts.size() > 1 ? tableParts.get(1) : null, column.getSchemaName());
assertEquals(tableParts.size() > 2 ? tableParts.get(2) : null, column.getCatalogName());
}

static Stream<Arguments> qualifiedQuotedNames() {
return Stream.of(
Arguments.of("t1.`id`", List.of("t1"), "`id`"),
Arguments.of("t1.\"id\"", List.of("t1"), "\"id\""),
Arguments.of("t1.[id]", List.of("t1"), "[id]"),
Arguments.of("`t1`.id", List.of("`t1`"), "id"),
Arguments.of("\"t1\".id", List.of("\"t1\""), "id"),
Arguments.of("[t1].id", List.of("[t1]"), "id"),
Arguments.of("`t1`.`id`", List.of("`t1`"), "`id`"),
Arguments.of("\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
Arguments.of("[t1].[id]", List.of("[t1]"), "[id]"),
Arguments.of("s.t1.`id`", List.of("t1", "s"), "`id`"),
Arguments.of("`s`.`t1`.`id`", List.of("`t1`", "`s`"), "`id`"),
Arguments.of("\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), "\"id\""),
Arguments.of("[s].[t1].[id]", List.of("[t1]", "[s]"), "[id]"),
Arguments.of("[s].`t1`.\"id\"", List.of("`t1`", "[s]"), "\"id\""),
Arguments.of("t1.`i.d`", List.of("t1"), "`i.d`"),
Arguments.of("t1.`i``.d`", List.of("t1"), "`i``.d`"),
Arguments.of("t1.\"i\"\".d\"", List.of("t1"), "\"i\"\".d\""),
Arguments.of("t1.[i]].d]", List.of("t1"), "[i]].d]"),
Arguments.of("`t.1`.id", List.of("`t.1`"), "id"),
Arguments.of("`s.1`.`t.1`.`i.d`", List.of("`t.1`", "`s.1`"), "`i.d`"),
Arguments.of("\"s.1\".\"t.1\".\"i.d\"", List.of("\"t.1\"", "\"s.1\""), "\"i.d\""),
Arguments.of("[s.1].[t.1].[i.d]", List.of("[t.1]", "[s.1]"), "[i.d]"),
Arguments.of("`t``.1`.`i``.d`", List.of("`t``.1`"), "`i``.d`"),
Arguments.of("\"t\"\".1\".\"i\"\".d\"", List.of("\"t\"\".1\""), "\"i\"\".d\""),
Arguments.of("[t]].1].[i]].d]", List.of("[t]].1]"), "[i]].d]"),
Arguments.of("`t\".1`.\"i`.d\"", List.of("`t\".1`"), "\"i`.d\""),
Arguments.of("t1.id", List.of("t1"), "id"),
Arguments.of("s.t1.id", List.of("t1", "s"), "id"));
}

static Stream<Arguments> separatedQuotedNames() {
// A single table token containing dots keeps the legacy BigQuery interpretation.
return qualifiedQuotedNames()
.filter(arguments -> {
List<?> tableParts = (List<?>) arguments.get()[1];
return tableParts.size() > 1 || !((String) tableParts.get(0)).contains(".");
});
}

static Stream<Arguments> parsedQualifiedQuotedNames() {
return separatedQuotedNames()
.filter(arguments -> !((String) arguments.get()[0]).contains("]]"));
}

static Stream<Arguments> legacyQualifiedTableNames() {
return Stream.of(
Arguments.of("schema.table", "schema.table.id", List.of("table", "schema")),
Arguments.of("`schema.table`", "\"schema\".\"table\".id",
List.of("\"table\"", "\"schema\"")),
Arguments.of("`project.dataset.table`", "\"project\".\"dataset\".\"table\".id",
List.of("\"table\"", "\"dataset\"", "\"project\"")));
}

static Stream<Arguments> wholeQuotedNames() {
return Stream.of(
Arguments.of("`t1.id`", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
Arguments.of("\"t1.id\"", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
Arguments.of("[t1.id]", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
Arguments.of("`s.t1.id`", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
"\"id\""),
Arguments.of("\"s.t1.id\"", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
"\"id\""),
Arguments.of("[s.t1.id]", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
"\"id\""));
}

}
Loading