diff --git a/deparser/jsqlparser/pom.xml b/deparser/jsqlparser/pom.xml
index 5415803..aef19a3 100644
--- a/deparser/jsqlparser/pom.xml
+++ b/deparser/jsqlparser/pom.xml
@@ -45,5 +45,9 @@
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
diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java
index 5e63603..2c2f7ba 100644
--- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java
+++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java
@@ -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;
@@ -29,6 +34,7 @@
public class BasicDialectExpressionDeParser extends ExpressionDeParser {
private final Dialect dialect;
+ private final Deque 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);
diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java
index 3d02084..ee801b4 100644
--- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java
+++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java
@@ -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;
@@ -36,6 +43,45 @@ public BasicDialectSelectDeParser(ExpressionVisitor StringBuilder visit(PlainSelect plainSelect, S context) {
+ Set StringBuilder visit(Table table, S context) {
// Quote catalog, schema, and table name
@@ -44,17 +90,17 @@ public 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
diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java
index eba899e..65a2a54 100644
--- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java
+++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java
@@ -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\"");
}
@@ -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();
@@ -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
diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java
index 636bef8..b28f02f 100644
--- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java
+++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java
@@ -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
@@ -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
@@ -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
@@ -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\".");
+ }
+
}
diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java
index 32d8778..9645547 100644
--- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java
+++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.when;
import org.eclipse.daanse.jdbc.db.dialect.api.Dialect;
+import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy;
public class MockDialectHelper {
@@ -34,48 +35,19 @@ public static Dialect createMySqlDialect() {
public static Dialect createSqlServerDialect() {
Dialect dialect = mock(Dialect.class);
- when(dialect.getQuoteIdentifierString()).thenReturn("[");
-
- when(dialect.quoteIdentifier(any(CharSequence.class))).thenAnswer(inv -> {
- CharSequence val = inv.getArgument(0);
- return new StringBuilder("[").append(val).append("]");
- });
-
doAnswer(inv -> {
String val = inv.getArgument(0);
StringBuilder buf = inv.getArgument(1);
- if (val != null) {
+ IdentifierQuotingPolicy policy = inv.getArgument(2);
+ if (val == null)
+ return null;
+ if (policy == IdentifierQuotingPolicy.NEVER) {
+ buf.append(val);
+ } else {
buf.append("[").append(val).append("]");
}
return null;
- }).when(dialect).quoteIdentifier(anyString(), any(StringBuilder.class));
-
- when(dialect.quoteIdentifier(anyString(), anyString())).thenAnswer(inv -> {
- String qual = inv.getArgument(0);
- String name = inv.getArgument(1);
- StringBuilder sb = new StringBuilder();
- if (qual != null) {
- sb.append("[").append(qual).append("].");
- }
- sb.append("[").append(name).append("]");
- return sb.toString();
- });
-
- doAnswer(inv -> {
- StringBuilder buf = inv.getArgument(0);
- Object[] args = inv.getArguments();
- boolean first = true;
- for (int i = 1; i < args.length; i++) {
- String name = (String) args[i];
- if (name == null)
- continue;
- if (!first)
- buf.append(".");
- buf.append("[").append(name).append("]");
- first = false;
- }
- return null;
- }).when(dialect).quoteIdentifier(any(StringBuilder.class), (String[]) any());
+ }).when(dialect).quoteIdentifierWith(anyString(), any(StringBuilder.class), any(IdentifierQuotingPolicy.class));
doAnswer(inv -> {
StringBuilder buf = inv.getArgument(0);
@@ -115,7 +87,6 @@ public static Dialect createSqlServerDialect() {
when(dialect.allowsFromAlias()).thenReturn(true);
when(dialect.allowsFieldAlias()).thenReturn(true);
when(dialect.needsExponent(any(), anyString())).thenReturn(false);
- when(dialect.name()).thenReturn("sqlserver");
return dialect;
}
@@ -123,48 +94,19 @@ public static Dialect createSqlServerDialect() {
public static Dialect createDialectWithQuote(String quoteChar) {
Dialect dialect = mock(Dialect.class);
- when(dialect.getQuoteIdentifierString()).thenReturn(quoteChar);
-
- when(dialect.quoteIdentifier(any(CharSequence.class))).thenAnswer(inv -> {
- CharSequence val = inv.getArgument(0);
- return new StringBuilder(quoteChar).append(val).append(quoteChar);
- });
-
doAnswer(inv -> {
String val = inv.getArgument(0);
StringBuilder buf = inv.getArgument(1);
- if (val != null) {
+ IdentifierQuotingPolicy policy = inv.getArgument(2);
+ if (val == null)
+ return null;
+ if (policy == IdentifierQuotingPolicy.NEVER) {
+ buf.append(val);
+ } else {
buf.append(quoteChar).append(val).append(quoteChar);
}
return null;
- }).when(dialect).quoteIdentifier(anyString(), any(StringBuilder.class));
-
- when(dialect.quoteIdentifier(anyString(), anyString())).thenAnswer(inv -> {
- String qual = inv.getArgument(0);
- String name = inv.getArgument(1);
- StringBuilder sb = new StringBuilder();
- if (qual != null) {
- sb.append(quoteChar).append(qual).append(quoteChar).append(".");
- }
- sb.append(quoteChar).append(name).append(quoteChar);
- return sb.toString();
- });
-
- doAnswer(inv -> {
- StringBuilder buf = inv.getArgument(0);
- Object[] args = inv.getArguments();
- boolean first = true;
- for (int i = 1; i < args.length; i++) {
- String name = (String) args[i];
- if (name == null)
- continue;
- if (!first)
- buf.append(".");
- buf.append(quoteChar).append(name).append(quoteChar);
- first = false;
- }
- return null;
- }).when(dialect).quoteIdentifier(any(StringBuilder.class), (String[]) any());
+ }).when(dialect).quoteIdentifierWith(anyString(), any(StringBuilder.class), any(IdentifierQuotingPolicy.class));
doAnswer(inv -> {
StringBuilder buf = inv.getArgument(0);
@@ -204,7 +146,6 @@ public static Dialect createDialectWithQuote(String quoteChar) {
when(dialect.allowsFromAlias()).thenReturn(true);
when(dialect.allowsFieldAlias()).thenReturn(true);
when(dialect.needsExponent(any(), anyString())).thenReturn(false);
- when(dialect.name()).thenReturn("mock");
return dialect;
}
diff --git a/guard/jsqltranspiler/pom.xml b/guard/jsqltranspiler/pom.xml
index 42e116c..2fe318a 100644
--- a/guard/jsqltranspiler/pom.xml
+++ b/guard/jsqltranspiler/pom.xml
@@ -52,5 +52,17 @@