Skip to content

Commit bb55bb9

Browse files
authored
fix(schema): preserve quoted column name parts (#2735)
Fixes #2734
1 parent d27b346 commit bb55bb9

2 files changed

Lines changed: 264 additions & 28 deletions

File tree

‎src/main/java/net/sf/jsqlparser/schema/Column.java‎

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,14 @@ public Column(List<String> nameParts) {
4949
}
5050

5151
public Column(List<String> nameParts, List<String> delimiters) {
52-
this(
53-
nameParts.size() > 1 ? new Table(nameParts.subList(0, nameParts.size() - 1),
54-
delimiters.subList(0, delimiters.size() - 1)) : null,
55-
nameParts.get(nameParts.size() - 1));
52+
if (nameParts.size() > 1) {
53+
setTable(new Table(nameParts.subList(0, nameParts.size() - 1),
54+
delimiters.subList(0, delimiters.size() - 1)));
55+
// The final component is already separated from the table name.
56+
setName(nameParts.get(nameParts.size() - 1), false);
57+
} else {
58+
setColumnName(nameParts.get(0));
59+
}
5660
setTableDelimiter(delimiters.isEmpty() ? "." : delimiters.get(delimiters.size() - 1));
5761
}
5862

@@ -149,36 +153,25 @@ public void setColumnName(String name) {
149153
}
150154

151155
public void setName(String name, boolean splitNamesOnDelimiter) {
152-
if (MultiPartName.isQuoted(name) && name.contains(".") && splitNamesOnDelimiter) {
153-
String[] parts = MultiPartName.unquote(name).split("\\.");
154-
switch (parts.length) {
155-
case 3:
156-
this.table = new Table("\"" + parts[0] + "\".\"" + parts[1] + "\"");
157-
this.columnName = "\"" + parts[2] + "\"";
158-
break;
159-
case 2:
160-
this.table = new Table("\"" + parts[0] + "\"");
161-
this.columnName = "\"" + parts[1] + "\"";
162-
break;
163-
case 1:
164-
this.columnName = "\"" + parts[0] + "\"";
165-
break;
166-
default:
167-
throw new RuntimeException("Invalid column name: " + name);
156+
if (name.contains(".") && splitNamesOnDelimiter) {
157+
List<String> parts = splitName(name);
158+
if (parts.size() == 1 && MultiPartName.isQuoted(name)) {
159+
parts.clear();
160+
for (String identifier : MultiPartName.unquote(name).split("\\.")) {
161+
parts.add("\"" + identifier + "\"");
162+
}
168163
}
169-
} else if (name.contains(".") && splitNamesOnDelimiter) {
170-
String[] parts = MultiPartName.unquote(name).split("\\.");
171-
switch (parts.length) {
164+
switch (parts.size()) {
172165
case 3:
173-
this.table = new Table(parts[0] + "." + parts[1]);
174-
this.columnName = parts[2];
166+
this.table = new Table(parts.subList(0, 2));
167+
this.columnName = parts.get(2);
175168
break;
176169
case 2:
177-
this.table = new Table(parts[0]);
178-
this.columnName = parts[1];
170+
this.table = new Table(parts.get(0), false);
171+
this.columnName = parts.get(1);
179172
break;
180173
case 1:
181-
this.columnName = parts[0];
174+
this.columnName = parts.get(0);
182175
break;
183176
default:
184177
throw new RuntimeException("Invalid column name: " + name);
@@ -188,6 +181,34 @@ public void setName(String name, boolean splitNamesOnDelimiter) {
188181
}
189182
}
190183

184+
private static List<String> splitName(String name) {
185+
List<String> parts = new ArrayList<>();
186+
int start = 0;
187+
char quote = 0;
188+
for (int i = 0; i < name.length(); i++) {
189+
char ch = name.charAt(i);
190+
if (quote != 0) {
191+
if (ch == quote) {
192+
if (i + 1 < name.length() && name.charAt(i + 1) == quote) {
193+
i++;
194+
} else {
195+
quote = 0;
196+
}
197+
}
198+
} else if (ch == '"' || ch == '`' || ch == '[') {
199+
quote = ch == '[' ? ']' : ch;
200+
} else if (ch == '.') {
201+
parts.add(name.substring(start, i));
202+
start = i + 1;
203+
}
204+
}
205+
parts.add(name.substring(start));
206+
while (!parts.isEmpty() && parts.get(parts.size() - 1).isEmpty()) {
207+
parts.remove(parts.size() - 1);
208+
}
209+
return parts;
210+
}
211+
191212
public String getTableDelimiter() {
192213
return tableDelimiter;
193214
}

‎src/test/java/net/sf/jsqlparser/schema/ColumnTest.java‎

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,34 @@
99
*/
1010
package net.sf.jsqlparser.schema;
1111

12+
import net.sf.jsqlparser.JSQLParserException;
13+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
14+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
15+
import net.sf.jsqlparser.statement.select.PlainSelect;
1216
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.parallel.ResourceLock;
18+
import org.junit.jupiter.api.parallel.Resources;
19+
import org.junit.jupiter.params.ParameterizedTest;
20+
import org.junit.jupiter.params.provider.Arguments;
21+
import org.junit.jupiter.params.provider.MethodSource;
22+
import org.junit.jupiter.params.provider.ValueSource;
1323

24+
import java.util.ArrayList;
25+
import java.util.Collections;
1426
import java.util.List;
27+
import java.util.stream.Stream;
1528

29+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
30+
import static net.sf.jsqlparser.test.TestUtils.assertStatementCanBeDeparsedAs;
1631
import static org.assertj.core.api.Assertions.assertThat;
1732
import static org.junit.jupiter.api.Assertions.assertEquals;
33+
import static org.junit.jupiter.api.Assertions.assertNull;
1834

1935
/**
2036
*
2137
* @author tw
2238
*/
39+
@ResourceLock(Resources.SYSTEM_PROPERTIES)
2340
public class ColumnTest {
2441

2542
@Test
@@ -54,4 +71,202 @@ public void testConstructorNamePartsAndDelimiters() {
5471
assertThat(table.getNamePartDelimiters()).containsExactly(".", ":");
5572
}
5673

74+
@ParameterizedTest
75+
@MethodSource("qualifiedQuotedNames")
76+
void testQualifiedQuotedNames(String name, List<String> tableParts, String columnName) {
77+
assertColumn(new Column(name), name, tableParts, columnName);
78+
Column column = new Column();
79+
column.setName(name, true);
80+
assertColumn(column, name, tableParts, columnName);
81+
assertColumn(new Column().withColumnName(name), name, tableParts, columnName);
82+
}
83+
84+
@ParameterizedTest
85+
@MethodSource("separatedQuotedNames")
86+
void testQuotedNameParts(String name, List<String> tableParts, String columnName) {
87+
List<String> nameParts = new ArrayList<>(tableParts);
88+
Collections.reverse(nameParts);
89+
nameParts.add(columnName);
90+
assertColumn(new Column(nameParts), name, tableParts, columnName);
91+
assertColumn(new Column(nameParts, Collections.nCopies(nameParts.size() - 1, ".")),
92+
name, tableParts, columnName);
93+
}
94+
95+
@ParameterizedTest
96+
@MethodSource("parsedQualifiedQuotedNames")
97+
void testParsedQualifiedQuotedNames(String name, List<String> tableParts, String columnName)
98+
throws JSQLParserException {
99+
String sql = "SELECT " + name + " FROM t1";
100+
PlainSelect select = (PlainSelect) assertSqlCanBeParsedAndDeparsed(sql, false,
101+
parser -> parser.withSquareBracketQuotation(true));
102+
assertColumn(select.getSelectItem(0).getExpression(Column.class), name, tableParts,
103+
columnName);
104+
PlainSelect reparsed = (PlainSelect) CCJSqlParserUtil.parse(select.toString(),
105+
parser -> parser.withSquareBracketQuotation(true));
106+
assertColumn(reparsed.getSelectItem(0).getExpression(Column.class), name, tableParts,
107+
columnName);
108+
}
109+
110+
@ParameterizedTest
111+
@MethodSource("wholeQuotedNames")
112+
void testWholeQuotedDottedNames(String name, String expected, List<String> tableParts,
113+
String columnName) throws JSQLParserException {
114+
assertColumn(new Column(name), expected, tableParts, columnName);
115+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse("SELECT " + name + " FROM t1",
116+
parser -> parser.withSquareBracketQuotation(true));
117+
assertColumn(select.getSelectItem(0).getExpression(Column.class), expected, tableParts,
118+
columnName);
119+
assertStatementCanBeDeparsedAs(select, "SELECT " + expected + " FROM t1");
120+
}
121+
122+
@ParameterizedTest
123+
@ValueSource(strings = {"id", "`id`", "\"id\"", "[id]"})
124+
void testUnqualifiedNames(String name) {
125+
Column column = new Column(name);
126+
assertEquals(name, column.getColumnName());
127+
assertEquals(name, column.toString());
128+
assertEquals("id", column.getUnquotedColumnName());
129+
assertNull(column.getTable());
130+
}
131+
132+
@ParameterizedTest
133+
@MethodSource("legacyQualifiedTableNames")
134+
void testLegacyQualifiedTableNames(String tableName, String expected,
135+
List<String> tableParts) {
136+
assertColumn(new Column(List.of(tableName, "id")), expected, tableParts, "id");
137+
assertColumn(new Column(List.of(tableName, "id"), List.of(".")), expected,
138+
tableParts, "id");
139+
}
140+
141+
@ParameterizedTest
142+
@ValueSource(booleans = {false, true})
143+
void testBigQueryQualifiedTableColumn(boolean explicitDialect) throws JSQLParserException {
144+
PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(
145+
"SELECT `project.dataset.table`.id FROM `project.dataset.table`", parser -> {
146+
if (explicitDialect) {
147+
parser.withDialect(Dialect.BIGQUERY);
148+
}
149+
});
150+
Column column = select.getSelectItem(0).getExpression(Column.class);
151+
assertColumn(column, "\"project\".\"dataset\".\"table\".id",
152+
List.of("\"table\"", "\"dataset\"", "\"project\""), "id");
153+
assertEquals("project", column.getUnquotedCatalogName());
154+
assertEquals("dataset", column.getUnquotedSchemaName());
155+
assertEquals("table", column.getUnquotedTableName());
156+
assertEquals(select.getFromItem(Table.class).getNameParts(),
157+
column.getTable().getNameParts());
158+
assertStatementCanBeDeparsedAs(select,
159+
"SELECT \"project\".\"dataset\".\"table\".id FROM \"project\".\"dataset\".\"table\"");
160+
}
161+
162+
@ParameterizedTest
163+
@ValueSource(strings = {"t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id"})
164+
void testExplicitlyDisabledSplitting(String name) {
165+
Column column = new Column();
166+
column.setName(name, false);
167+
assertEquals(name, column.getColumnName());
168+
assertEquals(name, column.toString());
169+
assertNull(column.getTable());
170+
}
171+
172+
@ParameterizedTest
173+
@ValueSource(strings = {"false", "OFF", "0"})
174+
void testSystemPropertyDisablesSplitting(String value) {
175+
String previous = System.getProperty("SPLIT_NAMES_ON_DELIMITER");
176+
try {
177+
System.setProperty("SPLIT_NAMES_ON_DELIMITER", value);
178+
for (String name : List.of("t1.`id`", "`t1.id`", "\"t1.id\"", "[t1.id]", "s.t1.id")) {
179+
Column column = new Column(name);
180+
assertEquals(name, column.getColumnName());
181+
assertEquals(name, column.toString());
182+
assertNull(column.getTable());
183+
}
184+
} finally {
185+
if (previous == null) {
186+
System.clearProperty("SPLIT_NAMES_ON_DELIMITER");
187+
} else {
188+
System.setProperty("SPLIT_NAMES_ON_DELIMITER", previous);
189+
}
190+
}
191+
}
192+
193+
private static void assertColumn(Column column, String name, List<String> tableParts,
194+
String columnName) {
195+
assertEquals(name, column.toString());
196+
assertEquals(name, column.getFullyQualifiedName());
197+
assertEquals(columnName, column.getColumnName());
198+
assertThat(column.getTable().getNameParts()).containsExactlyElementsOf(tableParts);
199+
assertEquals(tableParts.get(0), column.getTableName());
200+
assertEquals(tableParts.size() > 1 ? tableParts.get(1) : null, column.getSchemaName());
201+
assertEquals(tableParts.size() > 2 ? tableParts.get(2) : null, column.getCatalogName());
202+
}
203+
204+
static Stream<Arguments> qualifiedQuotedNames() {
205+
return Stream.of(
206+
Arguments.of("t1.`id`", List.of("t1"), "`id`"),
207+
Arguments.of("t1.\"id\"", List.of("t1"), "\"id\""),
208+
Arguments.of("t1.[id]", List.of("t1"), "[id]"),
209+
Arguments.of("`t1`.id", List.of("`t1`"), "id"),
210+
Arguments.of("\"t1\".id", List.of("\"t1\""), "id"),
211+
Arguments.of("[t1].id", List.of("[t1]"), "id"),
212+
Arguments.of("`t1`.`id`", List.of("`t1`"), "`id`"),
213+
Arguments.of("\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
214+
Arguments.of("[t1].[id]", List.of("[t1]"), "[id]"),
215+
Arguments.of("s.t1.`id`", List.of("t1", "s"), "`id`"),
216+
Arguments.of("`s`.`t1`.`id`", List.of("`t1`", "`s`"), "`id`"),
217+
Arguments.of("\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""), "\"id\""),
218+
Arguments.of("[s].[t1].[id]", List.of("[t1]", "[s]"), "[id]"),
219+
Arguments.of("[s].`t1`.\"id\"", List.of("`t1`", "[s]"), "\"id\""),
220+
Arguments.of("t1.`i.d`", List.of("t1"), "`i.d`"),
221+
Arguments.of("t1.`i``.d`", List.of("t1"), "`i``.d`"),
222+
Arguments.of("t1.\"i\"\".d\"", List.of("t1"), "\"i\"\".d\""),
223+
Arguments.of("t1.[i]].d]", List.of("t1"), "[i]].d]"),
224+
Arguments.of("`t.1`.id", List.of("`t.1`"), "id"),
225+
Arguments.of("`s.1`.`t.1`.`i.d`", List.of("`t.1`", "`s.1`"), "`i.d`"),
226+
Arguments.of("\"s.1\".\"t.1\".\"i.d\"", List.of("\"t.1\"", "\"s.1\""), "\"i.d\""),
227+
Arguments.of("[s.1].[t.1].[i.d]", List.of("[t.1]", "[s.1]"), "[i.d]"),
228+
Arguments.of("`t``.1`.`i``.d`", List.of("`t``.1`"), "`i``.d`"),
229+
Arguments.of("\"t\"\".1\".\"i\"\".d\"", List.of("\"t\"\".1\""), "\"i\"\".d\""),
230+
Arguments.of("[t]].1].[i]].d]", List.of("[t]].1]"), "[i]].d]"),
231+
Arguments.of("`t\".1`.\"i`.d\"", List.of("`t\".1`"), "\"i`.d\""),
232+
Arguments.of("t1.id", List.of("t1"), "id"),
233+
Arguments.of("s.t1.id", List.of("t1", "s"), "id"));
234+
}
235+
236+
static Stream<Arguments> separatedQuotedNames() {
237+
// A single table token containing dots keeps the legacy BigQuery interpretation.
238+
return qualifiedQuotedNames()
239+
.filter(arguments -> {
240+
List<?> tableParts = (List<?>) arguments.get()[1];
241+
return tableParts.size() > 1 || !((String) tableParts.get(0)).contains(".");
242+
});
243+
}
244+
245+
static Stream<Arguments> parsedQualifiedQuotedNames() {
246+
return separatedQuotedNames()
247+
.filter(arguments -> !((String) arguments.get()[0]).contains("]]"));
248+
}
249+
250+
static Stream<Arguments> legacyQualifiedTableNames() {
251+
return Stream.of(
252+
Arguments.of("schema.table", "schema.table.id", List.of("table", "schema")),
253+
Arguments.of("`schema.table`", "\"schema\".\"table\".id",
254+
List.of("\"table\"", "\"schema\"")),
255+
Arguments.of("`project.dataset.table`", "\"project\".\"dataset\".\"table\".id",
256+
List.of("\"table\"", "\"dataset\"", "\"project\"")));
257+
}
258+
259+
static Stream<Arguments> wholeQuotedNames() {
260+
return Stream.of(
261+
Arguments.of("`t1.id`", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
262+
Arguments.of("\"t1.id\"", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
263+
Arguments.of("[t1.id]", "\"t1\".\"id\"", List.of("\"t1\""), "\"id\""),
264+
Arguments.of("`s.t1.id`", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
265+
"\"id\""),
266+
Arguments.of("\"s.t1.id\"", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
267+
"\"id\""),
268+
Arguments.of("[s.t1.id]", "\"s\".\"t1\".\"id\"", List.of("\"t1\"", "\"s\""),
269+
"\"id\""));
270+
}
271+
57272
}

0 commit comments

Comments
 (0)