From 7ee06bf472438e41bfaa3348ae953eb6bba7cb79 Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Mon, 15 Dec 2025 22:51:32 +0100 Subject: [PATCH] add a dialect-deparser module and use it in guard Signed-off-by: Stefan Bischof --- .editorconfig | 3 + deparser/api/pom.xml | 39 ++++ .../sql/deparser/api/DialectDeparser.java | 31 +++ .../daanse/sql/deparser/api/package-info.java | 23 ++ deparser/jsqlparser/pom.xml | 49 ++++ .../jsqlparser/BasicDialectDeparser.java | 37 +++ .../BasicDialectExpressionDeParser.java | 125 ++++++++++ .../BasicDialectSelectDeParser.java | 106 +++++++++ .../BasicDialectStatementDeParser.java | 40 ++++ .../sql/deparser/jsqlparser/package-info.java | 16 ++ .../BaiscDialectExpressionDeParserTest.java | 211 +++++++++++++++++ .../DialectStatementDeParserTest.java | 110 +++++++++ .../jsqlparser/MockDialectHelper.java | 218 ++++++++++++++++++ deparser/pom.xml | 33 +++ guard/jsqltranspiler/pom.xml | 13 +- .../DaanseExpressionDeParser.java | 59 ----- .../DaanseStatementDeParser.java | 26 --- ...solver.java => DeparserColumResolver.java} | 14 +- .../jsqltranspiler/TranspilerSqlGuard.java | 14 +- .../TranspilerSqlGuardFactory.java | 11 +- .../integration/SqlGuardTest.java | 62 +++-- guard/jsqltranspiler/test.bndrun | 18 +- pom.xml | 1 + 23 files changed, 1129 insertions(+), 130 deletions(-) create mode 100644 deparser/api/pom.xml create mode 100644 deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java create mode 100644 deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/package-info.java create mode 100644 deparser/jsqlparser/pom.xml create mode 100644 deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java create mode 100644 deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java create mode 100644 deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java create mode 100644 deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java create mode 100644 deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/package-info.java create mode 100644 deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java create mode 100644 deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java create mode 100644 deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java create mode 100644 deparser/pom.xml delete mode 100644 guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseExpressionDeParser.java delete mode 100644 guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseStatementDeParser.java rename guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/{DaanseJSQLColumResolver.java => DeparserColumResolver.java} (77%) diff --git a/.editorconfig b/.editorconfig index 18b50af..7062954 100644 --- a/.editorconfig +++ b/.editorconfig @@ -45,3 +45,6 @@ insert_final_newline = unset trim_trailing_whitespace = unset indent_style = unset indent_size = unset + +[.flattened-pom.xml] +insert_final_newline = false diff --git a/deparser/api/pom.xml b/deparser/api/pom.xml new file mode 100644 index 0000000..c720fa3 --- /dev/null +++ b/deparser/api/pom.xml @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.deparser + ${revision} + + org.eclipse.daanse.sql.deparser.api + Daanse SQL Deparser API + API interfaces for SQL deparser factory and dialect-aware SQL + generation. + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.api + 0.0.1-SNAPSHOT + + + com.github.jsqlparser + jsqlparser + 5.4-SNAPSHOT + + + diff --git a/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java new file mode 100644 index 0000000..9dcea0e --- /dev/null +++ b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.api; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; + +/** + * Dialect-aware SQL deparsers. + */ +public interface DialectDeparser { + + /** + * Deparses a SQL statement using the given dialect. + * + * @param statement the JSqlParser statement to deparse + * @param dialect the database dialect to use for SQL generation + * @return the generated SQL string + */ + String deparse(net.sf.jsqlparser.statement.Statement statement, Dialect dialect); +} diff --git a/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/package-info.java b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/package-info.java new file mode 100644 index 0000000..af2612e --- /dev/null +++ b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ + +/** + * API interfaces for SQL deparser factory and dialect-aware SQL generation. + *

+ * This package provides the factory interface for creating dialect-aware SQL deparsers + * that generate proper SQL syntax according to the target database dialect. + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.deparser.api; diff --git a/deparser/jsqlparser/pom.xml b/deparser/jsqlparser/pom.xml new file mode 100644 index 0000000..cb6dbf4 --- /dev/null +++ b/deparser/jsqlparser/pom.xml @@ -0,0 +1,49 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.deparser + ${revision} + + org.eclipse.daanse.sql.deparser.jsqlparser + Daanse SQL Deparser JSqlParser Implementation + JSqlParser-based implementation of SQL deparser with + comprehensive dialect support. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.deparser.api + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.api + 0.0.1-SNAPSHOT + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.db.common + 0.0.1-SNAPSHOT + + + com.github.jsqlparser + jsqlparser + 5.4-SNAPSHOT + + + diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java new file mode 100644 index 0000000..dc41364 --- /dev/null +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.deparser.api.DialectDeparser; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.util.deparser.StatementDeParser; + +/** + * Factory implementation for creating dialect-aware SQL deparsers. + */ +@Component(service = DialectDeparser.class,scope = ServiceScope.SINGLETON) +public class BasicDialectDeparser implements DialectDeparser { + + @Override + public String deparse(Statement statement, Dialect dialect) { + StringBuilder buffer = new StringBuilder(); + StatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + statement.accept(deparser); + return buffer.toString(); + } +} 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 new file mode 100644 index 0000000..5e63603 --- /dev/null +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; + +import net.sf.jsqlparser.expression.DateValue; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.TimeValue; +import net.sf.jsqlparser.expression.TimestampValue; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; + +public class BasicDialectExpressionDeParser extends ExpressionDeParser { + + private final Dialect dialect; + + public BasicDialectExpressionDeParser(Dialect dialect) { + super(); + this.builder = new StringBuilder(); + this.dialect = dialect; + } + + public BasicDialectExpressionDeParser(SelectVisitor selectVisitor, StringBuilder buffer, + Dialect dialect) { + super(selectVisitor, buffer); + this.dialect = dialect; + } + + // Identifier Quoting + + @Override + public 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(); + } + } + + if (tableName != null && !tableName.isEmpty()) { + dialect.quoteIdentifier(builder, tableName, tableColumn.getColumnName()); + } else { + dialect.quoteIdentifier(builder, tableColumn.getColumnName()); + } + + if (tableColumn.getArrayConstructor() != null) { + tableColumn.getArrayConstructor().accept(this, context); + } + + if (tableColumn.getCommentText() != null) { + builder.append(" /* ").append(tableColumn.getCommentText()).append(" */"); + } + + return builder; + } + + // String Literal Quoting + + @Override + public StringBuilder visit(StringValue stringValue, S context) { + if (stringValue.getPrefix() != null) { + builder.append(stringValue.getPrefix()); + } + dialect.quoteStringLiteral(builder, stringValue.getValue()); + return builder; + } + + // Date/Time/Timestamp Literal Quoting + + @Override + public StringBuilder visit(DateValue dateValue, S context) { + dialect.quoteDateLiteral(builder, dateValue.getValue().toString()); + return builder; + } + + @Override + public StringBuilder visit(TimeValue timeValue, S context) { + dialect.quoteTimeLiteral(builder, timeValue.getValue().toString()); + return builder; + } + + @Override + public StringBuilder visit(TimestampValue timestampValue, S context) { + dialect.quoteTimestampLiteral(builder, timestampValue.getValue().toString()); + return builder; + } + + // Numeric Literal Formatting + + @Override + public StringBuilder visit(LongValue longValue, S context) { + dialect.quoteNumericLiteral(builder, longValue.getStringValue()); + return builder; + } + + @Override + public StringBuilder visit(DoubleValue doubleValue, S context) { + String valueString = doubleValue.toString(); + if (dialect.needsExponent(doubleValue.getValue(), valueString)) { + valueString += "E0"; + } + dialect.quoteNumericLiteral(builder, valueString); + return builder; + } +} 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 new file mode 100644 index 0000000..6c23ac3 --- /dev/null +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; + +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.util.deparser.SelectDeParser; + +public class BasicDialectSelectDeParser extends SelectDeParser { + + private final Dialect dialect; + + public BasicDialectSelectDeParser(StringBuilder buffer, Dialect dialect) { + super(buffer); + this.dialect = dialect; + } + + public BasicDialectSelectDeParser(ExpressionVisitor expressionVisitor, StringBuilder buffer, + Dialect dialect) { + super(expressionVisitor, buffer); + this.dialect = dialect; + } + + @Override + public StringBuilder visit(Table table, S context) { + // Quote catalog, schema, and table name + String catalog = table.getDatabaseName(); // Gets database/catalog + String schema = table.getSchemaName(); + String tableName = table.getName(); + + if (catalog != null && !catalog.isEmpty()) { + dialect.quoteIdentifier(builder, catalog); + builder.append("."); + } + + if (schema != null && !schema.isEmpty()) { + dialect.quoteIdentifier(builder, schema); + builder.append("."); + } + + if (tableName != null) { + dialect.quoteIdentifier(builder, tableName); + } + + // Handle table alias + Alias alias = table.getAlias(); + if (alias != null) { + if (dialect.allowsAs()) { + builder.append(" AS "); + } else { + builder.append(" "); + } + // Don't quote the alias - it's user-defined and may be intentionally unquoted + builder.append(alias.getName()); + } + + // Handle pivot/unpivot if present + if (table.getPivot() != null) { + table.getPivot().accept(this, context); + } + if (table.getUnPivot() != null) { + table.getUnPivot().accept(this, context); + } + + // Handle sample clause + if (table.getSampleClause() != null) { + builder.append(table.getSampleClause()); + } + + return builder; + } + + @Override + public StringBuilder visit(SelectItem selectItem, S context) { + // Use parent implementation for the expression + selectItem.getExpression().accept(getExpressionVisitor(), context); + + // Handle alias + Alias alias = selectItem.getAlias(); + if (alias != null) { + if (dialect.allowsFieldAs() && alias.isUseAs()) { + builder.append(" AS "); + } else { + builder.append(" "); + } + builder.append(alias.getName()); + } + + return builder; + } +} diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java new file mode 100644 index 0000000..b9c0514 --- /dev/null +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; + +import net.sf.jsqlparser.util.deparser.StatementDeParser; + +public class BasicDialectStatementDeParser extends StatementDeParser { + + public BasicDialectStatementDeParser(StringBuilder buffer, Dialect dialect) { + + super(createExpressionDeParser(dialect, buffer), createSelectDeParser(buffer, dialect), buffer); + + BasicDialectSelectDeParser selectDeParser = (BasicDialectSelectDeParser) getSelectDeParser(); + BasicDialectExpressionDeParser expressionDeParser = (BasicDialectExpressionDeParser) getExpressionDeParser(); + selectDeParser.setExpressionVisitor(expressionDeParser); + expressionDeParser.setSelectVisitor(selectDeParser); + } + + private static BasicDialectExpressionDeParser createExpressionDeParser(Dialect dialect, StringBuilder buffer) { + return new BasicDialectExpressionDeParser(dialect); + } + + private static BasicDialectSelectDeParser createSelectDeParser(StringBuilder buffer, Dialect dialect) { + return new BasicDialectSelectDeParser(buffer, dialect); + } + +} diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/package-info.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/package-info.java new file mode 100644 index 0000000..cc5a563 --- /dev/null +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/package-info.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ + +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.deparser.jsqlparser; 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 new file mode 100644 index 0000000..eba899e --- /dev/null +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.junit.jupiter.api.Test; + +import net.sf.jsqlparser.expression.DateValue; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.TimeValue; +import net.sf.jsqlparser.expression.TimestampValue; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; + +class BaiscDialectExpressionDeParserTest { + + @Test + void testColumnWithoutTable_AnsiDialect() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Column column = new Column("columnName"); + deparser.visit(column, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("\"columnName\""); + } + + @Test + void testColumnWithoutTable_MySqlDialect() { + Dialect dialect = MockDialectHelper.createMySqlDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Column column = new Column("columnName"); + deparser.visit(column, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("`columnName`"); + } + + @Test + void testColumnWithoutTable_SqlServerDialect() { + Dialect dialect = MockDialectHelper.createSqlServerDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Column column = new Column("columnName"); + deparser.visit(column, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("[columnName]"); + } + + @Test + void testColumnWithTable_AnsiDialect() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Table table = new Table("tableName"); + Column column = new Column(table, "columnName"); + deparser.visit(column, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("\"tableName\".\"columnName\""); + } + + @Test + void testColumnWithTable_MySqlDialect() { + Dialect dialect = MockDialectHelper.createMySqlDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Table table = new Table("tableName"); + Column column = new Column(table, "columnName"); + deparser.visit(column, null); + + 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(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + Table table = new Table("schemaName", "tableName"); + 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\""); + } + + @Test + void testStringValue_SimpleString() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + StringValue stringValue = new StringValue("hello"); + deparser.visit(stringValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("'hello'"); + } + + @Test + void testStringValue_WithSingleQuote() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + StringValue stringValue = new StringValue("it's"); + deparser.visit(stringValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("'it''s'"); + } + + @Test + void testDateValue_AnsiDialect() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + // Use java.sql.Date directly to avoid parsing issues + DateValue dateValue = new DateValue(java.sql.Date.valueOf("2023-12-15")); + deparser.visit(dateValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("DATE '2023-12-15'"); + } + + @Test + void testTimeValue_AnsiDialect() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + // Use withValue to set java.sql.Time directly to avoid timezone issues + TimeValue timeValue = new TimeValue().withValue(java.sql.Time.valueOf("14:30:00")); + deparser.visit(timeValue, null); + + // The output depends on timezone; just verify it contains TIME keyword and + // proper format + assertThat(deparser.getBuilder().toString()).startsWith("TIME '"); + assertThat(deparser.getBuilder().toString()).endsWith("'"); + } + + @Test + void testTimestampValue_AnsiDialect() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + // Use withValue to set java.sql.Timestamp directly + TimestampValue tsValue = new TimestampValue().withValue(java.sql.Timestamp.valueOf("2023-12-15 14:30:00")); + deparser.visit(tsValue, null); + + assertThat(deparser.getBuilder().toString()).startsWith("TIMESTAMP '"); + assertThat(deparser.getBuilder().toString()).contains("2023-12-15"); + } + + @Test + void testDateValue_SqlServerDialect() { + Dialect dialect = MockDialectHelper.createSqlServerDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + DateValue dateValue = new DateValue(java.sql.Date.valueOf("2023-12-15")); + deparser.visit(dateValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("CONVERT(DATE, '2023-12-15')"); + } + + @Test + void testLongValue() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + LongValue longValue = new LongValue(12345); + deparser.visit(longValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("12345"); + } + + @Test + void testDoubleValue() { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + BasicDialectExpressionDeParser deparser = new BasicDialectExpressionDeParser(dialect); + + DoubleValue doubleValue = new DoubleValue("123.45"); + deparser.visit(doubleValue, null); + + assertThat(deparser.getBuilder().toString()).isEqualTo("123.45"); + } + +} 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 new file mode 100644 index 0000000..636bef8 --- /dev/null +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.junit.jupiter.api.Test; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; + +class DialectStatementDeParserTest { + + @Test + void testSimpleSelect_AnsiDialect() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil.parse("SELECT col1 FROM table1"); + stmt.accept(deparser); + + String result = buffer.toString(); + // The column and table names should be quoted + assertThat(result).contains("\"col1\""); + } + + @Test + void testSimpleSelect_MySqlDialect() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createMySqlDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil.parse("SELECT col1 FROM table1"); + stmt.accept(deparser); + + String result = buffer.toString(); + // MySQL uses backticks + assertThat(result).contains("`col1`"); + } + + @Test + void testSelectWithStringLiteral() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM table1 WHERE name = 'test'"); + stmt.accept(deparser); + + String result = buffer.toString(); + assertThat(result).contains("'test'"); + } + + @Test + void testSelectWithDateLiteral() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM table1 WHERE created = DATE '2023-12-15'"); + stmt.accept(deparser); + + String result = buffer.toString(); + assertThat(result).contains("DATE '2023-12-15'"); + } + + @Test + void testSelectWithJoin() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil + .parse("SELECT t1.col1, t2.col2 FROM table1 t1 JOIN table2 t2 ON t1.id = t2.id"); + stmt.accept(deparser); + + String result = buffer.toString(); + // Columns with table aliases should be quoted + assertThat(result).contains("\"t1\""); + assertThat(result).contains("\"col1\""); + } + + @Test + void testSelectWithSubquery() throws JSQLParserException { + Dialect dialect = MockDialectHelper.createAnsiDialect(); + StringBuilder buffer = new StringBuilder(); + BasicDialectStatementDeParser deparser = new BasicDialectStatementDeParser(buffer, dialect); + + Statement stmt = CCJSqlParserUtil.parse("SELECT * FROM table1 WHERE id IN (SELECT id FROM table2)"); + stmt.accept(deparser); + + String result = buffer.toString(); + assertThat(result).contains("\"id\""); + } + +} 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 new file mode 100644 index 0000000..a5a03fe --- /dev/null +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.deparser.jsqlparser; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; + +public class MockDialectHelper { + + public static Dialect createAnsiDialect() { + return createDialectWithQuote("\""); + } + + public static Dialect createMySqlDialect() { + return createDialectWithQuote("`"); + } + + 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) { + 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()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String s = inv.getArgument(1); + buf.append("'").append(s.replace("'", "''")).append("'"); + return null; + }).when(dialect).quoteStringLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("CONVERT(DATE, '").append(value).append("')"); + return null; + }).when(dialect).quoteDateLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("CONVERT(TIME, '").append(value).append("')"); + return null; + }).when(dialect).quoteTimeLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("CONVERT(DATETIME, '").append(value).append("')"); + return null; + }).when(dialect).quoteTimestampLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append(value); + return null; + }).when(dialect).quoteNumericLiteral(any(StringBuilder.class), anyString()); + + when(dialect.allowsAs()).thenReturn(true); + when(dialect.allowsFieldAs()).thenReturn(true); + when(dialect.needsExponent(any(), anyString())).thenReturn(false); + when(dialect.getDialectName()).thenReturn("sqlserver"); + + return dialect; + } + + 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) { + 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()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String s = inv.getArgument(1); + buf.append("'").append(s.replace("'", "''")).append("'"); + return null; + }).when(dialect).quoteStringLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("DATE '").append(value).append("'"); + return null; + }).when(dialect).quoteDateLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("TIME '").append(value).append("'"); + return null; + }).when(dialect).quoteTimeLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append("TIMESTAMP '").append(value).append("'"); + return null; + }).when(dialect).quoteTimestampLiteral(any(StringBuilder.class), anyString()); + + doAnswer(inv -> { + StringBuilder buf = inv.getArgument(0); + String value = inv.getArgument(1); + buf.append(value); + return null; + }).when(dialect).quoteNumericLiteral(any(StringBuilder.class), anyString()); + + when(dialect.allowsAs()).thenReturn(true); + when(dialect.allowsFieldAs()).thenReturn(true); + when(dialect.needsExponent(any(), anyString())).thenReturn(false); + when(dialect.getDialectName()).thenReturn("mock"); + + return dialect; + } + + public static Dialect createDialectWithoutAs() { + Dialect dialect = createAnsiDialect(); + when(dialect.allowsAs()).thenReturn(false); + when(dialect.allowsFieldAs()).thenReturn(false); + return dialect; + } +} diff --git a/deparser/pom.xml b/deparser/pom.xml new file mode 100644 index 0000000..be1dd43 --- /dev/null +++ b/deparser/pom.xml @@ -0,0 +1,33 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql + ${revision} + + org.eclipse.daanse.sql.deparser + pom + Daanse SQL Deparser + SQL deparser framework providing dialect-aware SQL generation capabilities. + Extends JSqlParser deparsers with comprehensive database dialect support for proper + identifier quoting, literal formatting, and SQL syntax adaptation. + + + api + jsqlparser + + diff --git a/guard/jsqltranspiler/pom.xml b/guard/jsqltranspiler/pom.xml index b7393f1..b5b3e17 100644 --- a/guard/jsqltranspiler/pom.xml +++ b/guard/jsqltranspiler/pom.xml @@ -29,7 +29,7 @@ org.eclipse.daanse org.eclipse.daanse.sql.guard.api - ${project.version} + 0.0.1-SNAPSHOT com.github.jsqlparser @@ -41,5 +41,16 @@ jsqltranspiler 1.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.deparser.api + 0.0.1-SNAPSHOT + + + org.eclipse.daanse + org.eclipse.daanse.sql.deparser.jsqlparser + 0.0.1-SNAPSHOT + test + diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseExpressionDeParser.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseExpressionDeParser.java deleted file mode 100644 index 8ae132c..0000000 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseExpressionDeParser.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * SmartCity Jena - initial - * Stefan Bischof (bipolis.org) - initial - */ -package org.eclipse.daanse.sql.guard.jsqltranspiler; - -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; - -import net.sf.jsqlparser.schema.Column; -import net.sf.jsqlparser.schema.Table; -import net.sf.jsqlparser.util.deparser.ExpressionDeParser; - -public class DaanseExpressionDeParser extends ExpressionDeParser { - - private Dialect dialect; - - public DaanseExpressionDeParser(Dialect dialect) { - super(); - this.dialect = dialect; - } - - @Override - public 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(); - } - } - if (tableName != null && !tableName.isEmpty()) { - dialect.quoteIdentifier(builder, tableName, tableColumn.getColumnName()); - } else { - dialect.quoteIdentifier(builder, tableColumn.getColumnName()); - } - - if (tableColumn.getArrayConstructor() != null) { - tableColumn.getArrayConstructor().accept(this, context); - } - - if (tableColumn.getCommentText() != null) { - builder.append(" /* ").append(tableColumn.getCommentText()).append("*/ "); - } - - return builder; - } - -} diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseStatementDeParser.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseStatementDeParser.java deleted file mode 100644 index 62bdf39..0000000 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseStatementDeParser.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2024 Contributors to the Eclipse Foundation. - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * SmartCity Jena - initial - * Stefan Bischof (bipolis.org) - initial - */ -package org.eclipse.daanse.sql.guard.jsqltranspiler; - -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; - -import net.sf.jsqlparser.util.deparser.SelectDeParser; -import net.sf.jsqlparser.util.deparser.StatementDeParser; - -public class DaanseStatementDeParser extends StatementDeParser { - - public DaanseStatementDeParser(StringBuilder buffer, Dialect dialect) { - super(new DaanseExpressionDeParser(dialect), new SelectDeParser(), buffer); - } -} diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseJSQLColumResolver.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java similarity index 77% rename from guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseJSQLColumResolver.java rename to guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java index 8fe7402..64bea82 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DaanseJSQLColumResolver.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java @@ -14,6 +14,7 @@ package org.eclipse.daanse.sql.guard.jsqltranspiler; import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import ai.starlake.transpiler.JSQLColumResolver; import ai.starlake.transpiler.schema.JdbcMetaData; @@ -23,30 +24,29 @@ import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; -import net.sf.jsqlparser.util.deparser.StatementDeParser; -public class DaanseJSQLColumResolver extends JSQLColumResolver { +public class DeparserColumResolver extends JSQLColumResolver { final JdbcMetaData metaData; private Dialect dialect; + private DialectDeparser dialectDeparser; - public DaanseJSQLColumResolver(JdbcMetaData metaData, Dialect dialect) { + public DeparserColumResolver(JdbcMetaData metaData, Dialect dialect, DialectDeparser dialectDeparser) { super(metaData); this.metaData = metaData; this.dialect = dialect; + this.dialectDeparser = dialectDeparser; } public String getResolvedStatementText(String sqlStr) throws JSQLParserException { - StringBuilder builder = new StringBuilder(); - StatementDeParser deParser = new DaanseStatementDeParser(builder, this.dialect); Statement st = CCJSqlParserUtil.parse(sqlStr); if (st instanceof Select) { Select select = (Select) st; select.accept((SelectVisitor) this, JdbcMetaData.copyOf(metaData)); } - st.accept(deParser); - return builder.toString(); + + return dialectDeparser.deparse(st, dialect); } } diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java index 6dddc3c..0956313 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java @@ -20,6 +20,7 @@ import java.util.regex.Pattern; import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import org.eclipse.daanse.sql.guard.api.SqlGuard; import org.eclipse.daanse.sql.guard.api.elements.DatabaseCatalog; import org.eclipse.daanse.sql.guard.api.elements.DatabaseSchema; @@ -59,9 +60,11 @@ public class TranspilerSqlGuard implements SqlGuard { private JdbcMetaData jdbcMetaDataToCopy; private List whitelistFunctionsPatterns = new ArrayList(); private Dialect dialect; + private DialectDeparser dialectDeparser; - public TranspilerSqlGuard(String currentCatalogName, String currentSchemaName, DatabaseCatalog databaseCatalog, List whitelistFunctionsPatterns, Dialect dialect) { + public TranspilerSqlGuard(String currentCatalogName, String currentSchemaName, DatabaseCatalog databaseCatalog, List whitelistFunctionsPatterns, Dialect dialect, DialectDeparser dialectDeparser) { this.whitelistFunctionsPatterns = whitelistFunctionsPatterns; + this.dialectDeparser = dialectDeparser; jdbcMetaDataToCopy = calculateMetaData(currentCatalogName, currentSchemaName, databaseCatalog); this.dialect = dialect; @@ -144,16 +147,11 @@ public String guard(String sqlStr) throws GuardException { } // we can finally resolve for the actually returned columns - JSQLColumResolver columResolver = new DaanseJSQLColumResolver(jdbcMetaDataToCopy, dialect); + JSQLColumResolver columResolver = new DeparserColumResolver(jdbcMetaDataToCopy, dialect,dialectDeparser); columResolver.setCommentFlag(false); columResolver.setErrorMode(JdbcMetaData.ErrorMode.STRICT); String rewritten = columResolver.getResolvedStatementText(sqlStr); - // TODO: get it as object and access to AST that we do not have to reparse - Statement stResolveds = CCJSqlParserUtil.parse(rewritten); - - // TODO: check the count of functions and deepnes and size of statement. we should be able check on - // this variables if we allow statement or if it calls to much. deParser.visit((Select) st);// or rewritten @@ -182,7 +180,7 @@ private static JdbcMetaData calculateMetaData(String currentCatalogName, String for (DatabaseTable table : schema.getDatabaseTables()) { List jdbcColumns = table.getDatabaseColumns().parallelStream() - .map(c -> new JdbcColumn(c.getName())).toList(); + .map(c -> new JdbcColumn(c.getName())).toList(); jdbcMetaData.addTable(schema.getName(), table.getName(), jdbcColumns); } } diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java index 46b3dae..ccb0da3 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java @@ -16,18 +16,25 @@ import java.util.List; import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import org.eclipse.daanse.sql.guard.api.SqlGuard; import org.eclipse.daanse.sql.guard.api.SqlGuardFactory; import org.eclipse.daanse.sql.guard.api.elements.DatabaseCatalog; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ServiceScope; @Component(scope = ServiceScope.SINGLETON) public class TranspilerSqlGuardFactory implements SqlGuardFactory { + @Reference + DialectDeparser dialectDeparser; + @Override - public SqlGuard create(String currentCatalogName, String currentSchemaName, DatabaseCatalog databaseCatalog, List whitelistFunctionsPatterns, Dialect dialect) { - return new TranspilerSqlGuard(currentCatalogName, currentSchemaName, databaseCatalog, whitelistFunctionsPatterns, dialect); + public SqlGuard create(String currentCatalogName, String currentSchemaName, DatabaseCatalog databaseCatalog, + List whitelistFunctionsPatterns, Dialect dialect) { + return new TranspilerSqlGuard(currentCatalogName, currentSchemaName, databaseCatalog, + whitelistFunctionsPatterns, dialect,dialectDeparser); } } diff --git a/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java b/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java index ce2452d..635b649 100644 --- a/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java +++ b/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java @@ -35,8 +35,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.osgi.test.common.annotation.InjectService; public class SqlGuardTest { @@ -168,25 +166,51 @@ public class SqlGuardTest { @BeforeAll public static void setUp() { dialect = mock(Dialect.class); - doAnswer(new Answer() { - @Override - public Void answer(InvocationOnMock invocation) - throws Throwable { - Object[] arguments = invocation.getArguments(); - - if (arguments != null - && - arguments.length > 0 - && - arguments[0] != null && arguments[1] != null && arguments[1] != null) { - StringBuilder sb = (StringBuilder) arguments[0]; - String s1 = (String) arguments[1]; - String s2 = (String) arguments[2]; - sb.append(s1).append(".").append(s2); - } - return null; + + // Mock for quoteIdentifier(StringBuilder, String) - single identifier + doAnswer(invocation -> { + StringBuilder buf = invocation.getArgument(0); + String name = invocation.getArgument(1); + if (name != null) { + buf.append(name); } + return null; }).when(dialect).quoteIdentifier(any(StringBuilder.class), any(String.class)); + + // Mock for quoteIdentifier(StringBuilder, String...) - varargs for qualified names + doAnswer(invocation -> { + StringBuilder buf = invocation.getArgument(0); + Object[] args = invocation.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(name); + first = false; + } + return null; + }).when(dialect).quoteIdentifier(any(StringBuilder.class), (String[]) any()); + + // Mock for quoteNumericLiteral(StringBuilder, String) - numeric values + doAnswer(invocation -> { + StringBuilder buf = invocation.getArgument(0); + String value = invocation.getArgument(1); + buf.append(value); + return null; + }).when(dialect).quoteNumericLiteral(any(StringBuilder.class), any(String.class)); + + // Mock for quoteStringLiteral(StringBuilder, String) - string values + doAnswer(invocation -> { + StringBuilder buf = invocation.getArgument(0); + String value = invocation.getArgument(1); + buf.append("'").append(value.replace("'", "''")).append("'"); + return null; + }).when(dialect).quoteStringLiteral(any(StringBuilder.class), any(String.class)); + + when(dialect.allowsAs()).thenReturn(true); + when(dialect.allowsFieldAs()).thenReturn(true); + when(dialect.needsExponent(any(), any(String.class))).thenReturn(false); } @Test diff --git a/guard/jsqltranspiler/test.bndrun b/guard/jsqltranspiler/test.bndrun index ee3a10c..c1f58f6 100644 --- a/guard/jsqltranspiler/test.bndrun +++ b/guard/jsqltranspiler/test.bndrun @@ -54,17 +54,19 @@ -runbundles: \ ai.starlake.transpiler;version='[1.0.0,1.0.1)',\ json;version='[20250517.0.0,20250517.0.1)',\ - junit-jupiter-api;version='[5.10.2,5.10.3)',\ - junit-jupiter-engine;version='[5.10.2,5.10.3)',\ - junit-jupiter-params;version='[5.10.2,5.10.3)',\ - junit-platform-commons;version='[1.10.2,1.10.3)',\ - junit-platform-engine;version='[1.10.2,1.10.3)',\ - junit-platform-launcher;version='[1.10.2,1.10.3)',\ - net.bytebuddy.byte-buddy;version='[1.12.16,1.12.17)',\ - net.bytebuddy.byte-buddy-agent;version='[1.12.16,1.12.17)',\ + junit-jupiter-api;version='[5.12.2,5.12.3)',\ + junit-jupiter-engine;version='[5.12.2,5.12.3)',\ + junit-jupiter-params;version='[5.12.2,5.12.3)',\ + junit-platform-commons;version='[1.12.2,1.12.3)',\ + junit-platform-engine;version='[1.12.2,1.12.3)',\ + junit-platform-launcher;version='[1.12.2,1.12.3)',\ + net.bytebuddy.byte-buddy;version='[1.17.5,1.17.6)',\ + net.bytebuddy.byte-buddy-agent;version='[1.17.5,1.17.6)',\ net.sf.jsqlparser;version='[5.4.0,5.4.1)',\ org.apache.felix.scr;version='[2.2.10,2.2.11)',\ org.eclipse.daanse.jdbc.db.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.deparser.jsqlparser;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.jsqltranspiler-tests;version='[0.0.1,0.0.2)',\ diff --git a/pom.xml b/pom.xml index 78ed6cf..3c52a72 100644 --- a/pom.xml +++ b/pom.xml @@ -80,5 +80,6 @@ guard + deparser