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
4 changes: 4 additions & 0 deletions deparser/jsqlparser/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,9 @@
<artifactId>jsqlparser</artifactId>
<version>5.3.167</version>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.service.component.annotations</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@

/**
* Factory implementation for creating dialect-aware SQL deparsers.
*
* <p>Identifiers in the AST are expected to already be in their canonical form
* (resolved against the database catalog by the caller). The deparser then
* always wraps them in the dialect's quote character so the resulting SQL is
* resolved by the engine via case-sensitive lookup.
*/
@Component(service = DialectDeparser.class,scope = ServiceScope.SINGLETON)
@Component(service = DialectDeparser.class, scope = ServiceScope.SINGLETON)
public class BasicDialectDeparser implements DialectDeparser {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
*/
package org.eclipse.daanse.sql.deparser.jsqlparser;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Set;

import org.eclipse.daanse.jdbc.db.dialect.api.Dialect;
import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy;

import net.sf.jsqlparser.expression.DateValue;
import net.sf.jsqlparser.expression.DoubleValue;
Expand All @@ -29,6 +34,7 @@
public class BasicDialectExpressionDeParser extends ExpressionDeParser {

private final Dialect dialect;
private final Deque<Set<String>> aliasScopes = new ArrayDeque<>();

public BasicDialectExpressionDeParser(Dialect dialect) {
super();
Expand All @@ -42,26 +48,55 @@ public BasicDialectExpressionDeParser(SelectVisitor<StringBuilder> selectVisitor
this.dialect = dialect;
}

// Alias scope management — populated by BasicDialectSelectDeParser per PlainSelect.

void pushAliasScope(Set<String> aliases) {
aliasScopes.push(aliases);
}

void popAliasScope() {
aliasScopes.pop();
}

private boolean isDeclaredAlias(String name) {
for (Set<String> scope : aliasScopes) {
if (scope.contains(name)) {
return true;
}
}
return false;
}

// Identifier Quoting

@Override
public <S> StringBuilder visit(Column tableColumn, S context) {
final Table table = tableColumn.getTable();
String tableName = null;

if (table != null) {
if (table.getAlias() != null) {
tableName = table.getAlias().getName();
} else {
tableName = table.getFullyQualifiedName();
String name = table.getName();
if (name != null && !name.isEmpty()) {
if (isDeclaredAlias(name)) {
// Alias declared in an enclosing FROM scope — emit verbatim so it
// matches the unquoted alias declaration (case-folding compatible).
builder.append(name).append('.');
} else {
String catalog = table.getDatabaseName();
String schema = table.getSchemaName();
if (catalog != null && !catalog.isEmpty()) {
dialect.quoteIdentifierWith(catalog, builder, IdentifierQuotingPolicy.ALWAYS);
builder.append('.');
}
if (schema != null && !schema.isEmpty()) {
dialect.quoteIdentifierWith(schema, builder, IdentifierQuotingPolicy.ALWAYS);
builder.append('.');
}
dialect.quoteIdentifierWith(name, builder, IdentifierQuotingPolicy.ALWAYS);
builder.append('.');
}
}
}

if (tableName != null && !tableName.isEmpty()) {
dialect.quoteIdentifier(builder, tableName, tableColumn.getColumnName());
} else {
dialect.quoteIdentifier(builder, tableColumn.getColumnName());
}
dialect.quoteIdentifierWith(tableColumn.getColumnName(), builder, IdentifierQuotingPolicy.ALWAYS);

if (tableColumn.getArrayConstructor() != null) {
tableColumn.getArrayConstructor().accept(this, context);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@
*/
package org.eclipse.daanse.sql.deparser.jsqlparser;

import java.util.HashSet;
import java.util.Set;

import org.eclipse.daanse.jdbc.db.dialect.api.Dialect;
import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy;

import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.select.FromItem;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.util.deparser.SelectDeParser;

Expand All @@ -36,6 +43,45 @@ public BasicDialectSelectDeParser(ExpressionVisitor<StringBuilder> expressionVis
this.dialect = dialect;
}

@Override
public <S> StringBuilder visit(PlainSelect plainSelect, S context) {
Set<String> aliases = collectAliases(plainSelect);
BasicDialectExpressionDeParser exprDeParser = currentExpressionDeParser();
if (exprDeParser != null) {
exprDeParser.pushAliasScope(aliases);
try {
return super.visit(plainSelect, context);
} finally {
exprDeParser.popAliasScope();
}
}
return super.visit(plainSelect, context);
}

private BasicDialectExpressionDeParser currentExpressionDeParser() {
ExpressionVisitor<StringBuilder> visitor = getExpressionVisitor();
return visitor instanceof BasicDialectExpressionDeParser
? (BasicDialectExpressionDeParser) visitor
: null;
}

private static Set<String> collectAliases(PlainSelect ps) {
Set<String> out = new HashSet<>();
addAlias(out, ps.getFromItem());
if (ps.getJoins() != null) {
for (Join j : ps.getJoins()) {
addAlias(out, j.getFromItem());
}
}
return out;
}

private static void addAlias(Set<String> out, FromItem fi) {
if (fi != null && fi.getAlias() != null) {
out.add(fi.getAlias().getName());
}
}

@Override
public <S> StringBuilder visit(Table table, S context) {
// Quote catalog, schema, and table name
Expand All @@ -44,17 +90,17 @@ public <S> StringBuilder visit(Table table, S context) {
String tableName = table.getName();

if (catalog != null && !catalog.isEmpty()) {
dialect.quoteIdentifier(builder, catalog);
dialect.quoteIdentifierWith(catalog, builder, IdentifierQuotingPolicy.ALWAYS);
builder.append(".");
}

if (schema != null && !schema.isEmpty()) {
dialect.quoteIdentifier(builder, schema);
dialect.quoteIdentifierWith(schema, builder, IdentifierQuotingPolicy.ALWAYS);
builder.append(".");
}

if (tableName != null) {
dialect.quoteIdentifier(builder, tableName);
dialect.quoteIdentifierWith(tableName, builder, IdentifierQuotingPolicy.ALWAYS);
}

// Handle table alias
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ void testColumnWithTable_AnsiDialect() {
Column column = new Column(table, "columnName");
deparser.visit(column, null);

// Real table name (no alias scope) is quoted alongside the column name.
assertThat(deparser.getBuilder().toString()).isEqualTo("\"tableName\".\"columnName\"");
}

Expand All @@ -86,20 +87,6 @@ void testColumnWithTable_MySqlDialect() {
assertThat(deparser.getBuilder().toString()).isEqualTo("`tableName`.`columnName`");
}

@Test
void testColumnWithTableAlias() {
Dialect dialect = MockDialectHelper.createAnsiDialect();
BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect);

Table table = new Table("tableName");
table.setAlias(new net.sf.jsqlparser.expression.Alias("t"));
Column column = new Column(table, "columnName");
deparser.visit(column, null);

// When table has alias, use alias name
assertThat(deparser.getBuilder().toString()).isEqualTo("\"t\".\"columnName\"");
}

@Test
void testColumnWithSchemaAndTable() {
Dialect dialect = MockDialectHelper.createAnsiDialect();
Expand All @@ -109,8 +96,8 @@ void testColumnWithSchemaAndTable() {
Column column = new Column(table, "columnName");
deparser.visit(column, null);

// Should use fully qualified name from table
assertThat(deparser.getBuilder().toString()).isEqualTo("\"schemaName.tableName\".\"columnName\"");
// Each FQN segment is quoted independently when the qualifier is a real name.
assertThat(deparser.getBuilder().toString()).isEqualTo("\"schemaName\".\"tableName\".\"columnName\"");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ void testSimpleSelect_AnsiDialect() throws JSQLParserException {
stmt.accept(deparser);

String result = buffer.toString();
// The column and table names should be quoted
// Identifiers are always quoted
assertThat(result).contains("\"col1\"");
assertThat(result).contains("\"table1\"");
}

@Test
Expand All @@ -48,8 +49,9 @@ void testSimpleSelect_MySqlDialect() throws JSQLParserException {
stmt.accept(deparser);

String result = buffer.toString();
// MySQL uses backticks
// MySQL uses backticks; identifiers always quoted
assertThat(result).contains("`col1`");
assertThat(result).contains("`table1`");
}

@Test
Expand Down Expand Up @@ -89,9 +91,15 @@ void testSelectWithJoin() throws JSQLParserException {
stmt.accept(deparser);

String result = buffer.toString();
// Columns with table aliases should be quoted
assertThat(result).contains("\"t1\"");
assertThat(result).contains("\"col1\"");
// Aliases used as qualifiers must remain unquoted; FROM table names are quoted
assertThat(result).contains("t1.\"col1\"");
assertThat(result).contains("t2.\"col2\"");
assertThat(result).contains("\"table1\"");
assertThat(result).contains("\"table2\"");
assertThat(result).contains("AS t1");
assertThat(result).contains("AS t2");
assertThat(result).doesNotContain("\"t1\"");
assertThat(result).doesNotContain("\"t2\"");
}

@Test
Expand All @@ -107,4 +115,82 @@ void testSelectWithSubquery() throws JSQLParserException {
assertThat(result).contains("\"id\"");
}

/**
* Regression: when a column reference is qualified by a real (unaliased) table
* name, the qualifier must be quoted just like the column name. Otherwise a
* case-folding engine like H2 will fail to resolve mixed-case real names.
*/
@Test
void testQualifiedColumnQualifierWithRealTableName_IsQuoted() throws JSQLParserException {
Dialect dialect = MockDialectHelper.createAnsiDialect();
StringBuilder buffer = new StringBuilder();
BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect);

Statement stmt = CCJSqlParserUtil.parse(
"SELECT ProductCategory.EnglishProductCategoryName, sum(Fact.OrderQuantity) "
+ "FROM Fact JOIN ProductCategory ON Fact.cat = ProductCategory.cat");
stmt.accept(deparser);

String result = buffer.toString();
assertThat(result).contains("\"Fact\".\"OrderQuantity\"");
assertThat(result).contains("\"ProductCategory\".\"EnglishProductCategoryName\"");
assertThat(result).contains("\"Fact\".\"cat\"");
assertThat(result).contains("\"ProductCategory\".\"cat\"");
assertThat(result).doesNotContain("Fact.\"");
assertThat(result).doesNotContain("ProductCategory.\"");
}

/**
* Regression: when one table is aliased and another isn't, the alias qualifier
* stays unquoted and the real table-name qualifier gets quoted.
*/
@Test
void testMixedAliasAndRealTableQualifiers() throws JSQLParserException {
Dialect dialect = MockDialectHelper.createAnsiDialect();
StringBuilder buffer = new StringBuilder();
BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect);

Statement stmt = CCJSqlParserUtil.parse(
"SELECT f.col1, ProductCategory.col2 "
+ "FROM Fact f JOIN ProductCategory ON f.cat = ProductCategory.cat");
stmt.accept(deparser);

String result = buffer.toString();
// Alias qualifier verbatim, real table qualifier quoted
assertThat(result).contains("f.\"col1\"");
assertThat(result).contains("\"ProductCategory\".\"col2\"");
assertThat(result).contains("f.\"cat\"");
assertThat(result).contains("\"ProductCategory\".\"cat\"");
assertThat(result).doesNotContain("\"f\".");
}

/**
* Regression for the original H2 bug: a column reference's table-qualifier
* must be emitted verbatim so it matches the unquoted alias declared in the
* FROM clause. Mixed-case real table/column names are quoted as usual.
*/
@Test
void testQualifiedColumnQualifierIsNotQuoted_H2Compatible() throws JSQLParserException {
Dialect dialect = MockDialectHelper.createAnsiDialect();
StringBuilder buffer = new StringBuilder();
BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect);

Statement stmt = CCJSqlParserUtil.parse(
"SELECT pc.EnglishProductCategoryName, sum(f.OrderQuantity) "
+ "FROM Fact f JOIN ProductCategory pc ON f.cat = pc.cat");
stmt.accept(deparser);

String result = buffer.toString();
// Column names always quoted
assertThat(result).contains("f.\"OrderQuantity\"");
assertThat(result).contains("pc.\"EnglishProductCategoryName\"");
assertThat(result).contains("f.\"cat\"");
assertThat(result).contains("pc.\"cat\"");
// Aliases as qualifiers stay unquoted
assertThat(result).contains("AS f");
assertThat(result).contains("AS pc");
assertThat(result).doesNotContain("\"f\".");
assertThat(result).doesNotContain("\"pc\".");
}

}
Loading
Loading