From abb874c58ce1dd822677f6efc214e8d1a36f80aa Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 6 Oct 2026 10:30:52 +0300 Subject: [PATCH 1/2] add synonyms in MetadataProvider. Implementation MetadataProvider for oracle, H2, MsSql Signed-off-by: --- .../daanse/sql/jdbc/api/MetadataProvider.java | 16 + .../sql/jdbc/api/meta/StructureInfo.java | 6 + .../sql/jdbc/api/schema/SchemaObject.java | 2 +- .../daanse/sql/jdbc/api/schema/Synonym.java | 60 ++ .../sql/jdbc/api/schema/SynonymReference.java | 27 + .../sql/jdbc/impl/DatabaseServiceImpl.java | 5 +- .../DatabaseServiceImplSynonymsH2Test.java | 60 ++ jdbc/impl/test.bndrun | 1 + jdbc/metadata/pom.xml | 12 + .../sql/jdbc/metadata/H2MetadataProvider.java | 546 ++++++++++-------- .../MicrosoftSqlServerMetadataProvider.java | 51 ++ .../jdbc/metadata/OracleMetadataProvider.java | 57 ++ .../sql/jdbc/metadata/H2SynonymsTest.java | 110 ++++ .../sql/jdbc/metadata/MsSqlSynonymsTest.java | 110 ++++ .../sql/jdbc/metadata/OracleSynonymsTest.java | 140 +++++ .../jdbc/record/meta/StructureInfoRecord.java | 20 +- .../sql/jdbc/record/schema/SynonymRecord.java | 30 + 17 files changed, 996 insertions(+), 257 deletions(-) create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Synonym.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SynonymReference.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplSynonymsH2Test.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2SynonymsTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlSynonymsTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/OracleSynonymsTest.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SynonymRecord.java diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java index 3517e5c..97fe13f 100644 --- a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java @@ -37,6 +37,7 @@ import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; @@ -125,6 +126,21 @@ default List getAllSequences(Connection connection, String catalog, St return List.of(); } + /** + * Synonyms (aliases) owned by the schema. Dialects that know public synonyms + * include only those pointing into the schema, not every public synonym of the + * database. + * + * @param catalog the catalog name, or null + * @param schema the schema name, or null for the connection's current schema + * @return the synonyms — empty when the engine has no synonyms or the loader is + * not implemented for this dialect + * @throws SQLException on database access error + */ + default List getAllSynonyms(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + /** * @param catalog the catalog name, or null * @return the partition list — empty when the engine has no partitions or the diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java index f977dba..446aa40 100644 --- a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java @@ -25,6 +25,7 @@ import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; import org.eclipse.daanse.sql.model.schema.Trigger; import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; @@ -55,6 +56,11 @@ default List sequences() { return List.of(); } + /** @return the synonyms, empty list if not available */ + default List synonyms() { + return List.of(); + } + /** @return the check constraints, empty list if not available */ default List checkConstraints() { return List.of(); diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java index 54641bd..a03c56f 100644 --- a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java @@ -14,5 +14,5 @@ package org.eclipse.daanse.sql.jdbc.api.schema; public sealed interface SchemaObject permits TableDefinition, ViewDefinition, MaterializedView, Sequence, Function, - Procedure, UserDefinedType { + Procedure, UserDefinedType, Synonym { } diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Synonym.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Synonym.java new file mode 100644 index 0000000..fd74ca8 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Synonym.java @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2026 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.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +/** + * An alias for another schema object (Oracle synonym, SQL Server synonym, DB2 alias). + *

+ * The target is reported as stored in the catalog; it is not resolved further. A + * synonym pointing to another synonym has {@link #targetObjectType()} {@code SYNONYM}, + * and consumers follow the chain themselves. + */ +public non-sealed interface Synonym extends SchemaObject, Named { + + SynonymReference reference(); + + @Override + default String name() { + return reference().name(); + } + + /** Convenience: the schema of this synonym ({@code PUBLIC} for Oracle public synonyms). */ + default Optional schema() { + return reference().schema(); + } + + /** @return the schema of the target object, empty when the catalog does not record one */ + Optional targetSchema(); + + /** @return the name of the target object */ + String targetName(); + + /** @return the database link (Oracle) or linked server (SQL Server) the target lives behind */ + Optional dbLink(); + + /** @return true for a synonym visible to every user (Oracle {@code PUBLIC} synonym) */ + boolean isPublic(); + + /** + * @return the type of the target object as named by the database ({@code TABLE}, {@code VIEW}, + * {@code FUNCTION}, {@code SYNONYM}, ...); empty when the target is remote, missing + * or not visible to the reading user + */ + Optional targetObjectType(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SynonymReference.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SynonymReference.java new file mode 100644 index 0000000..4f1ed0a --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SynonymReference.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2026 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.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public record SynonymReference(Optional schema, String name) implements Named { + + /** Convenience: an unqualified synonym (no schema). */ + public SynonymReference(String name) { + this(Optional.empty(), name); + } +} diff --git a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java index f7f3931..dad4499 100644 --- a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java +++ b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java @@ -221,6 +221,8 @@ protected MetaInfo readMetaInfoWithProvider(Connection connection, DatabaseMetaD // NEW metadata — only via dialect, no JDBC fallback needed List triggers = provider.getAllTriggers(connection, null, null); List sequences = provider.getAllSequences(connection, null, null); + List synonyms = + provider.getAllSynonyms(connection, null, null); List checkConstraints = provider.getAllCheckConstraints(connection, null, null); List uniqueConstraints = provider.getAllUniqueConstraints(connection, null, null); List userDefinedTypes = provider.getAllUserDefinedTypes(connection, null, null); @@ -278,7 +280,8 @@ protected MetaInfo readMetaInfoWithProvider(Connection connection, DatabaseMetaD StructureInfo structureInfo = new StructureInfoRecord(catalogs, schemas, tables, columns, importedKeys, primaryKeys, triggers, sequences, checkConstraints, uniqueConstraints, userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, partitions, - tablePrivileges, List.copyOf(columnPrivileges), objectPrivileges, roleMemberships, principals); + tablePrivileges, List.copyOf(columnPrivileges), objectPrivileges, roleMemberships, principals, + synonyms); return new MetaInfoRecord(databaseInfo, structureInfo, identifierInfo, typeInfos, indexInfos); } diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplSynonymsH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplSynonymsH2Test.java new file mode 100644 index 0000000..23d43b5 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplSynonymsH2Test.java @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.jdbc.metadata.H2MetadataProvider; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** The snapshot carries the provider's synonyms; the plain-JDBC path has none. */ +class DatabaseServiceImplSynonymsH2Test { + + private static Connection connection; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection("jdbc:h2:mem:coreSynonymsTest;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE TABLE CUSTOMERS (ID INT PRIMARY KEY, NAME VARCHAR(100))"); + stmt.execute("CREATE SYNONYM SYN_CUSTOMERS FOR CUSTOMERS"); + } + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + void snapshotWithProviderContainsSynonyms() throws Exception { + MetaInfo metaInfo = new DatabaseServiceImpl().createMetaInfo(connection, new H2MetadataProvider()); + assertThat(metaInfo.structureInfo().synonyms()).singleElement().satisfies(syn -> { + assertThat(syn.name()).isEqualTo("SYN_CUSTOMERS"); + assertThat(syn.targetName()).isEqualTo("CUSTOMERS"); + }); + } + + @Test + void snapshotWithoutProviderHasNoSynonyms() throws Exception { + MetaInfo metaInfo = new DatabaseServiceImpl().createMetaInfo(connection); + assertThat(metaInfo.structureInfo().synonyms()).extracting(Synonym::name).isEmpty(); + } +} diff --git a/jdbc/impl/test.bndrun b/jdbc/impl/test.bndrun index 1b2a5db..6da090f 100644 --- a/jdbc/impl/test.bndrun +++ b/jdbc/impl/test.bndrun @@ -76,6 +76,7 @@ org.mockito.mockito-core;version='[4.9.0,4.9.1)',\ org.objenesis;version='[3.3.0,3.3.1)',\ org.opentest4j;version='[1.3.0,1.3.1)',\ + org.osgi.service.cm;version='[1.6.1,1.6.2)',\ org.osgi.service.component;version='[1.5.1,1.5.2)',\ org.osgi.test.common;version='[1.3.0,1.3.1)',\ org.osgi.test.junit5;version='[1.3.0,1.3.1)',\ diff --git a/jdbc/metadata/pom.xml b/jdbc/metadata/pom.xml index 56001be..f0de741 100644 --- a/jdbc/metadata/pom.xml +++ b/jdbc/metadata/pom.xml @@ -75,6 +75,18 @@ 1.19.7 test + + com.oracle.database.jdbc + ojdbc11 + 23.3.0.23.09 + test + + + org.testcontainers + oracle-free + 1.19.7 + test + org.postgresql postgresql diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java index 72ea817..b0cebcc 100644 --- a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Optional; import java.util.OptionalInt; + import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; @@ -41,6 +42,8 @@ import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.jdbc.api.schema.SynonymReference; import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; @@ -58,6 +61,7 @@ import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SynonymRecord; import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; @@ -73,13 +77,13 @@ public class H2MetadataProvider implements MetadataProvider { @Override public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { String sql = """ - SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, - EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, - EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG - FROM INFORMATION_SCHEMA.TRIGGERS - WHERE TRIGGER_SCHEMA = ? - ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME - """; + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, + EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, + EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG + FROM INFORMATION_SCHEMA.TRIGGERS + WHERE TRIGGER_SCHEMA = ? + ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME + """; String schemaName = resolveSchema(schema, connection); List triggers = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -96,15 +100,15 @@ public List getAllTriggers(Connection connection, String catalog, Strin @Override public List getTriggers(Connection connection, String catalog, String schema, String tableName) - throws SQLException { + throws SQLException { String sql = """ - SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, - EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, - EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG - FROM INFORMATION_SCHEMA.TRIGGERS - WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? - ORDER BY TRIGGER_NAME - """; + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, + EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, + EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG + FROM INFORMATION_SCHEMA.TRIGGERS + WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? + ORDER BY TRIGGER_NAME + """; String schemaName = resolveSchema(schema, connection); List triggers = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -123,12 +127,12 @@ public List getTriggers(Connection connection, String catalog, String s @Override public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { String sql = """ - SELECT SEQUENCE_NAME, START_VALUE, INCREMENT, MINIMUM_VALUE, - MAXIMUM_VALUE, CYCLE_OPTION, CACHE, DATA_TYPE - FROM INFORMATION_SCHEMA.SEQUENCES - WHERE SEQUENCE_SCHEMA = ? - ORDER BY SEQUENCE_NAME - """; + SELECT SEQUENCE_NAME, START_VALUE, INCREMENT, MINIMUM_VALUE, + MAXIMUM_VALUE, CYCLE_OPTION, CACHE, DATA_TYPE + FROM INFORMATION_SCHEMA.SEQUENCES + WHERE SEQUENCE_SCHEMA = ? + ORDER BY SEQUENCE_NAME + """; String schemaName = resolveSchema(schema, connection); List sequences = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -151,7 +155,7 @@ public List getAllSequences(Connection connection, String catalog, Str Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); sequences.add(new SequenceRecord(new SequenceReference(oSchema, name), startValue, increment, - oMinValue, oMaxValue, cycle, oCacheSize, Optional.ofNullable(dataType))); + oMinValue, oMaxValue, cycle, oCacheSize, Optional.ofNullable(dataType))); } } } @@ -159,19 +163,53 @@ public List getAllSequences(Connection connection, String catalog, Str } + @Override + public List getAllSynonyms(Connection connection, String catalog, String schema) throws SQLException { + // H2 synonyms point only to existing tables and views — no chains, no dangling + // targets, no public synonyms (PUBLIC is just the default schema's name). + String sql = """ + SELECT s.SYNONYM_NAME, s.SYNONYM_FOR_SCHEMA, s.SYNONYM_FOR, t.TABLE_TYPE + FROM INFORMATION_SCHEMA.SYNONYMS s + LEFT JOIN INFORMATION_SCHEMA.TABLES t + ON t.TABLE_SCHEMA = s.SYNONYM_FOR_SCHEMA AND t.TABLE_NAME = s.SYNONYM_FOR + WHERE s.SYNONYM_SCHEMA = ? + ORDER BY s.SYNONYM_NAME + """; + String schemaName = resolveSchema(schema, connection); + List synonyms = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String targetSchema = rs.getString("SYNONYM_FOR_SCHEMA"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + Optional oTargetSchema = targetSchema == null ? Optional.empty() + : Optional.of(new SchemaReference(Optional.empty(), targetSchema)); + + synonyms.add(new SynonymRecord(new SynonymReference(oSchema, rs.getString("SYNONYM_NAME")), + oTargetSchema, rs.getString("SYNONYM_FOR"), Optional.empty(), false, + Optional.ofNullable(rs.getString("TABLE_TYPE")))); + } + } + } + return List.copyOf(synonyms); + } + + @Override public List getAllCheckConstraints(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME - FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc - JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA - AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME - WHERE tc.TABLE_SCHEMA = ? - AND tc.CONSTRAINT_TYPE = 'CHECK' - ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME - """; + SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME + FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc + JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA + AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE tc.TABLE_SCHEMA = ? + AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME + """; String schemaName = resolveSchema(schema, connection); List constraints = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -195,18 +233,18 @@ public List getAllCheckConstraints(Connection connection, Strin @Override public List getCheckConstraints(Connection connection, String catalog, String schema, - String tableName) throws SQLException { + String tableName) throws SQLException { String sql = """ - SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME - FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc - JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA - AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME - WHERE tc.TABLE_SCHEMA = ? - AND tc.TABLE_NAME = ? - AND tc.CONSTRAINT_TYPE = 'CHECK' - ORDER BY tc.CONSTRAINT_NAME - """; + SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME + FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc + JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA + AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE tc.TABLE_SCHEMA = ? + AND tc.TABLE_NAME = ? + AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.CONSTRAINT_NAME + """; String schemaName = resolveSchema(schema, connection); List constraints = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -230,58 +268,58 @@ public List getCheckConstraints(Connection connection, String c @Override public List getAllUniqueConstraints(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, - kcu.COLUMN_NAME, kcu.ORDINAL_POSITION - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu - ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA - AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME - AND tc.TABLE_NAME = kcu.TABLE_NAME - WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' - AND tc.TABLE_SCHEMA = ? - ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION - """; + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' + AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; return readUniqueConstraints(connection, sql, schema, null); } @Override public List getUniqueConstraints(Connection connection, String catalog, String schema, - String tableName) throws SQLException { + String tableName) throws SQLException { String sql = """ - SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, - kcu.COLUMN_NAME, kcu.ORDINAL_POSITION - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu - ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA - AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME - AND tc.TABLE_NAME = kcu.TABLE_NAME - WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' - AND tc.TABLE_SCHEMA = ? - AND tc.TABLE_NAME = ? - ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION - """; + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' + AND tc.TABLE_SCHEMA = ? + AND tc.TABLE_NAME = ? + ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; return readUniqueConstraints(connection, sql, schema, tableName); } @Override public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, - kcu.COLUMN_NAME, kcu.ORDINAL_POSITION - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu - ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA - AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME - AND tc.TABLE_NAME = kcu.TABLE_NAME - WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' - AND tc.TABLE_SCHEMA = ? - ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION - """; + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; String schemaName = resolveSchema(schema, connection); // Group by table+constraint to build composite PKs Map pkMap = new LinkedHashMap<>(); @@ -295,7 +333,7 @@ public Optional> getAllPrimaryKeys(Connection connection, Strin String key = tableName + "." + constraintName; pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) - .addColumn(columnName); + .addColumn(columnName); } } } @@ -309,35 +347,35 @@ public Optional> getAllPrimaryKeys(Connection connection, Strin @Override public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, - fk_kcu.TABLE_NAME AS FK_TABLE, - fk_kcu.COLUMN_NAME AS FK_COLUMN, - fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, - fk_kcu.TABLE_CATALOG AS FK_CATALOG, - pk_kcu.TABLE_NAME AS PK_TABLE, - pk_kcu.COLUMN_NAME AS PK_COLUMN, - pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, - pk_kcu.TABLE_CATALOG AS PK_CATALOG, - fk_kcu.ORDINAL_POSITION AS KEY_SEQ, - rc.UPDATE_RULE, rc.DELETE_RULE, - rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu - ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA - AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME - JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc - ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA - AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu - ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA - AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME - AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION - WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' - AND fk_tc.TABLE_SCHEMA = ? - ORDER BY fk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION - """; + SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, + fk_kcu.TABLE_NAME AS FK_TABLE, + fk_kcu.COLUMN_NAME AS FK_COLUMN, + fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, + fk_kcu.TABLE_CATALOG AS FK_CATALOG, + pk_kcu.TABLE_NAME AS PK_TABLE, + pk_kcu.COLUMN_NAME AS PK_COLUMN, + pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, + pk_kcu.TABLE_CATALOG AS PK_CATALOG, + fk_kcu.ORDINAL_POSITION AS KEY_SEQ, + rc.UPDATE_RULE, rc.DELETE_RULE, + rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu + ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu + ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA + AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME + AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION + WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND fk_tc.TABLE_SCHEMA = ? + ORDER BY fk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION + """; String schemaName = resolveSchema(schema, connection); List importedKeys = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -354,37 +392,37 @@ public Optional> getAllImportedKeys(Connection connection, Str @Override public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { // Symmetric to getAllImportedKeys: filter by the referenced (PK-side) schema // rather than the referencing (FK-side) schema. String sql = """ - SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, - fk_kcu.TABLE_NAME AS FK_TABLE, - fk_kcu.COLUMN_NAME AS FK_COLUMN, - fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, - fk_kcu.TABLE_CATALOG AS FK_CATALOG, - pk_kcu.TABLE_NAME AS PK_TABLE, - pk_kcu.COLUMN_NAME AS PK_COLUMN, - pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, - pk_kcu.TABLE_CATALOG AS PK_CATALOG, - fk_kcu.ORDINAL_POSITION AS KEY_SEQ, - rc.UPDATE_RULE, rc.DELETE_RULE, - rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME - FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu - ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA - AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME - JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc - ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA - AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME - JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu - ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA - AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME - AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION - WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' - AND pk_kcu.TABLE_SCHEMA = ? - ORDER BY pk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION - """; + SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, + fk_kcu.TABLE_NAME AS FK_TABLE, + fk_kcu.COLUMN_NAME AS FK_COLUMN, + fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, + fk_kcu.TABLE_CATALOG AS FK_CATALOG, + pk_kcu.TABLE_NAME AS PK_TABLE, + pk_kcu.COLUMN_NAME AS PK_COLUMN, + pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, + pk_kcu.TABLE_CATALOG AS PK_CATALOG, + fk_kcu.ORDINAL_POSITION AS KEY_SEQ, + rc.UPDATE_RULE, rc.DELETE_RULE, + rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu + ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu + ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA + AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME + AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION + WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND pk_kcu.TABLE_SCHEMA = ? + ORDER BY pk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION + """; String schemaName = resolveSchema(schema, connection); List exportedKeys = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -401,20 +439,20 @@ public Optional> getAllExportedKeys(Connection connection, Str @Override public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT i.TABLE_NAME, i.INDEX_NAME, i.INDEX_TYPE_NAME, - ic.COLUMN_NAME, ic.ORDINAL_POSITION, ic.IS_UNIQUE, - i.TABLE_SCHEMA, i.TABLE_CATALOG - FROM INFORMATION_SCHEMA.INDEXES i - JOIN INFORMATION_SCHEMA.INDEX_COLUMNS ic - ON i.INDEX_CATALOG = ic.INDEX_CATALOG - AND i.INDEX_SCHEMA = ic.INDEX_SCHEMA - AND i.INDEX_NAME = ic.INDEX_NAME - AND i.TABLE_NAME = ic.TABLE_NAME - WHERE i.TABLE_SCHEMA = ? - ORDER BY i.TABLE_NAME, i.INDEX_NAME, ic.ORDINAL_POSITION - """; + SELECT i.TABLE_NAME, i.INDEX_NAME, i.INDEX_TYPE_NAME, + ic.COLUMN_NAME, ic.ORDINAL_POSITION, ic.IS_UNIQUE, + i.TABLE_SCHEMA, i.TABLE_CATALOG + FROM INFORMATION_SCHEMA.INDEXES i + JOIN INFORMATION_SCHEMA.INDEX_COLUMNS ic + ON i.INDEX_CATALOG = ic.INDEX_CATALOG + AND i.INDEX_SCHEMA = ic.INDEX_SCHEMA + AND i.INDEX_NAME = ic.INDEX_NAME + AND i.TABLE_NAME = ic.TABLE_NAME + WHERE i.TABLE_SCHEMA = ? + ORDER BY i.TABLE_NAME, i.INDEX_NAME, ic.ORDINAL_POSITION + """; String schemaName = resolveSchema(schema, connection); // Group index items by table Map> tableIndexes = new LinkedHashMap<>(); @@ -432,22 +470,22 @@ public Optional> getAllIndexInfo(Connection connection, String c TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { Optional oSchema = Optional - .of(new SchemaReference(Optional.empty(), schemaName)); + .of(new SchemaReference(Optional.empty(), schemaName)); return new TableReference(oSchema, k); }); Optional colRef = Optional.ofNullable(columnName) - .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); IndexInfoItem.IndexType indexType = mapH2IndexType(indexTypeName); IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), indexType, colRef, - ordinalPosition, Optional.empty(), // H2 doesn't expose ASC/DESC in - // INFORMATION_SCHEMA.INDEXES - 0L, // cardinality not available here - 0L, // pages not available here - Optional.empty(), // filter condition - isUnique); + ordinalPosition, Optional.empty(), // H2 doesn't expose ASC/DESC in + // INFORMATION_SCHEMA.INDEXES + 0L, // cardinality not available here + 0L, // pages not available here + Optional.empty(), // filter condition + isUnique); tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); } @@ -463,13 +501,13 @@ public Optional> getAllIndexInfo(Connection connection, String c @Override public List getAllViewDefinitions(Connection connection, String catalog, String schema) - throws SQLException { + throws SQLException { String sql = """ - SELECT TABLE_NAME, VIEW_DEFINITION, TABLE_SCHEMA, TABLE_CATALOG - FROM INFORMATION_SCHEMA.VIEWS - WHERE TABLE_SCHEMA = ? - ORDER BY TABLE_NAME - """; + SELECT TABLE_NAME, VIEW_DEFINITION, TABLE_SCHEMA, TABLE_CATALOG + FROM INFORMATION_SCHEMA.VIEWS + WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME + """; String schemaName = resolveSchema(schema, connection); List views = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { @@ -497,11 +535,11 @@ public List getAllProcedures(Connection connection, String catalog, S Map> paramMap = loadProcedureColumns(connection, schemaName); String sql = """ - SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION - FROM INFORMATION_SCHEMA.ROUTINES - WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' - ORDER BY ROUTINE_NAME - """; + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' + ORDER BY ROUTINE_NAME + """; List procedures = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, schemaName); @@ -516,8 +554,8 @@ public List getAllProcedures(Connection connection, String catalog, S List cols = paramMap.getOrDefault(specificName, List.of()); procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName, specificName), - Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), cols, - Optional.ofNullable(body), Optional.empty(), Optional.empty())); + Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), Optional.empty(), Optional.empty())); } } } @@ -532,11 +570,11 @@ public List getAllFunctions(Connection connection, String catalog, Str Map> paramMap = loadFunctionColumns(connection, schemaName); String sql = """ - SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION - FROM INFORMATION_SCHEMA.ROUTINES - WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'FUNCTION' - ORDER BY ROUTINE_NAME - """; + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """; List functions = new ArrayList<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, schemaName); @@ -551,8 +589,8 @@ public List getAllFunctions(Connection connection, String catalog, Str List cols = paramMap.getOrDefault(specificName, List.of()); functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName, specificName), - Function.FunctionType.NO_TABLE, Optional.ofNullable(remarks), cols, - Optional.ofNullable(body), Optional.empty(), Optional.empty())); + Function.FunctionType.NO_TABLE, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), Optional.empty(), Optional.empty())); } } } @@ -561,15 +599,15 @@ public List getAllFunctions(Connection connection, String catalog, Str private Map> loadProcedureColumns(Connection connection, String schemaName) - throws SQLException { + throws SQLException { String sql = """ - SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, - ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE, - PARAMETER_DEFAULT - FROM INFORMATION_SCHEMA.PARAMETERS - WHERE SPECIFIC_SCHEMA = ? - ORDER BY SPECIFIC_NAME, ORDINAL_POSITION - """; + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE, + PARAMETER_DEFAULT + FROM INFORMATION_SCHEMA.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; Map> result = new LinkedHashMap<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, schemaName); @@ -590,11 +628,11 @@ private Map> loadProcedureColumns(Connection conne JDBCType jdbcType = mapH2DataType(dataType); ProcedureColumn col = new ProcedureColumnRecord(paramName != null ? paramName : "", colType, - jdbcType, dataType != null ? dataType : "", - precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), - scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), - ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.ofNullable(paramDefault), - ordinalPosition); + jdbcType, dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.ofNullable(paramDefault), + ordinalPosition); result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); } @@ -605,14 +643,14 @@ private Map> loadProcedureColumns(Connection conne private Map> loadFunctionColumns(Connection connection, String schemaName) - throws SQLException { + throws SQLException { String sql = """ - SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, - ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE - FROM INFORMATION_SCHEMA.PARAMETERS - WHERE SPECIFIC_SCHEMA = ? - ORDER BY SPECIFIC_NAME, ORDINAL_POSITION - """; + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM INFORMATION_SCHEMA.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; Map> result = new LinkedHashMap<>(); try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, schemaName); @@ -632,10 +670,10 @@ private Map> loadFunctionColumns(Connection connect JDBCType jdbcType = mapH2DataType(dataType); FunctionColumn col = new FunctionColumnRecord(paramName != null ? paramName : "", colType, jdbcType, - dataType != null ? dataType : "", - precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), - scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), - FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), ordinalPosition); + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), ordinalPosition); result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); } @@ -650,10 +688,10 @@ private static ProcedureColumn.ColumnType mapProcedureColumnType(String mode) { return ProcedureColumn.ColumnType.UNKNOWN; } return switch (mode.toUpperCase()) { - case "IN" -> ProcedureColumn.ColumnType.IN; - case "OUT" -> ProcedureColumn.ColumnType.OUT; - case "INOUT" -> ProcedureColumn.ColumnType.INOUT; - default -> ProcedureColumn.ColumnType.UNKNOWN; + case "IN" -> ProcedureColumn.ColumnType.IN; + case "OUT" -> ProcedureColumn.ColumnType.OUT; + case "INOUT" -> ProcedureColumn.ColumnType.INOUT; + default -> ProcedureColumn.ColumnType.UNKNOWN; }; } @@ -663,10 +701,10 @@ private static FunctionColumn.ColumnType mapFunctionColumnType(String mode) { return FunctionColumn.ColumnType.UNKNOWN; } return switch (mode.toUpperCase()) { - case "IN" -> FunctionColumn.ColumnType.IN; - case "OUT" -> FunctionColumn.ColumnType.OUT; - case "INOUT" -> FunctionColumn.ColumnType.INOUT; - default -> FunctionColumn.ColumnType.UNKNOWN; + case "IN" -> FunctionColumn.ColumnType.IN; + case "OUT" -> FunctionColumn.ColumnType.OUT; + case "INOUT" -> FunctionColumn.ColumnType.INOUT; + default -> FunctionColumn.ColumnType.UNKNOWN; }; } @@ -679,25 +717,25 @@ private static JDBCType mapH2DataType(String dataType) { return JDBCType.valueOf(dataType.toUpperCase().replace(" ", "_")); } catch (IllegalArgumentException e) { return switch (dataType.toUpperCase()) { - case "INT", "INT4", "SIGNED" -> JDBCType.INTEGER; - case "INT8" -> JDBCType.BIGINT; - case "INT2" -> JDBCType.SMALLINT; - case "FLOAT4" -> JDBCType.FLOAT; - case "FLOAT8" -> JDBCType.DOUBLE; - case "BOOL" -> JDBCType.BOOLEAN; - case "STRING", "TEXT" -> JDBCType.VARCHAR; - case "BYTEA" -> JDBCType.VARBINARY; - case "CHARACTER VARYING" -> JDBCType.VARCHAR; - case "CHARACTER LARGE OBJECT" -> JDBCType.CLOB; - case "BINARY LARGE OBJECT" -> JDBCType.BLOB; - case "BINARY VARYING" -> JDBCType.VARBINARY; - case "DOUBLE PRECISION" -> JDBCType.DOUBLE; - case "DECFLOAT" -> JDBCType.DECIMAL; - case "TIMESTAMP WITH TIME ZONE" -> JDBCType.TIMESTAMP_WITH_TIMEZONE; - case "TIME WITH TIME ZONE" -> JDBCType.TIME_WITH_TIMEZONE; - case "INTERVAL" -> JDBCType.OTHER; - case "GEOMETRY", "JSON", "UUID", "ENUM", "ARRAY" -> JDBCType.OTHER; - default -> JDBCType.OTHER; + case "INT", "INT4", "SIGNED" -> JDBCType.INTEGER; + case "INT8" -> JDBCType.BIGINT; + case "INT2" -> JDBCType.SMALLINT; + case "FLOAT4" -> JDBCType.FLOAT; + case "FLOAT8" -> JDBCType.DOUBLE; + case "BOOL" -> JDBCType.BOOLEAN; + case "STRING", "TEXT" -> JDBCType.VARCHAR; + case "BYTEA" -> JDBCType.VARBINARY; + case "CHARACTER VARYING" -> JDBCType.VARCHAR; + case "CHARACTER LARGE OBJECT" -> JDBCType.CLOB; + case "BINARY LARGE OBJECT" -> JDBCType.BLOB; + case "BINARY VARYING" -> JDBCType.VARBINARY; + case "DOUBLE PRECISION" -> JDBCType.DOUBLE; + case "DECFLOAT" -> JDBCType.DECIMAL; + case "TIMESTAMP WITH TIME ZONE" -> JDBCType.TIMESTAMP_WITH_TIMEZONE; + case "TIME WITH TIME ZONE" -> JDBCType.TIME_WITH_TIMEZONE; + case "INTERVAL" -> JDBCType.OTHER; + case "GEOMETRY", "JSON", "UUID", "ENUM", "ARRAY" -> JDBCType.OTHER; + default -> JDBCType.OTHER; }; } } @@ -713,16 +751,16 @@ private Trigger readTrigger(ResultSet rs) throws SQLException { String tableSchema = rs.getString("EVENT_OBJECT_SCHEMA"); Optional oSchema = Optional.ofNullable(tableSchema) - .map(s -> new SchemaReference(Optional.empty(), s)); + .map(s -> new SchemaReference(Optional.empty(), s)); TableReference tableRef = new TableReference(oSchema, tableName); TriggerTiming timing = mapTriggerTiming(actionTiming); TriggerEvent event = mapTriggerEvent(eventManipulation); return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, - Optional.ofNullable(actionStatement), Optional.empty(), // H2 doesn't provide full CREATE TRIGGER DDL - // directly - Optional.ofNullable(actionOrientation)); + Optional.ofNullable(actionStatement), Optional.empty(), // H2 doesn't provide full CREATE TRIGGER DDL + // directly + Optional.ofNullable(actionOrientation)); } @@ -744,25 +782,25 @@ private ImportedKey readImportedKey(ResultSet rs) throws SQLException { // FK side Optional fkCatRef = Optional.ofNullable(fkCatalog).map(CatalogReference::new); Optional fkSchemaRef = Optional.ofNullable(fkSchema) - .map(s -> new SchemaReference(fkCatRef, s)); + .map(s -> new SchemaReference(fkCatRef, s)); TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); // PK side Optional pkCatRef = Optional.ofNullable(pkCatalog).map(CatalogReference::new); Optional pkSchemaRef = Optional.ofNullable(pkSchema) - .map(s -> new SchemaReference(pkCatRef, s)); + .map(s -> new SchemaReference(pkCatRef, s)); TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapReferentialAction(updateRule), - mapReferentialAction(deleteRule), Optional.ofNullable(pkName), - ImportedKey.Deferrability.NOT_DEFERRABLE); + mapReferentialAction(deleteRule), Optional.ofNullable(pkName), + ImportedKey.Deferrability.NOT_DEFERRABLE); } private List readUniqueConstraints(Connection connection, String sql, String schema, - String tableName) throws SQLException { + String tableName) throws SQLException { String schemaName = resolveSchema(schema, connection); // Group by constraint name to collect all columns Map ucMap = new LinkedHashMap<>(); @@ -779,7 +817,7 @@ private List readUniqueConstraints(Connection connection, Stri String key = table + "." + constraintName; ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) - .addColumn(columnName); + .addColumn(columnName); } } } @@ -805,9 +843,9 @@ private static TriggerTiming mapTriggerTiming(String timing) { return TriggerTiming.AFTER; } return switch (timing.toUpperCase()) { - case "BEFORE" -> TriggerTiming.BEFORE; - case "INSTEAD OF" -> TriggerTiming.INSTEAD_OF; - default -> TriggerTiming.AFTER; + case "BEFORE" -> TriggerTiming.BEFORE; + case "INSTEAD OF" -> TriggerTiming.INSTEAD_OF; + default -> TriggerTiming.AFTER; }; } @@ -817,9 +855,9 @@ private static TriggerEvent mapTriggerEvent(String event) { return TriggerEvent.INSERT; } return switch (event.toUpperCase()) { - case "UPDATE" -> TriggerEvent.UPDATE; - case "DELETE" -> TriggerEvent.DELETE; - default -> TriggerEvent.INSERT; + case "UPDATE" -> TriggerEvent.UPDATE; + case "DELETE" -> TriggerEvent.DELETE; + default -> TriggerEvent.INSERT; }; } @@ -829,11 +867,11 @@ private static ImportedKey.ReferentialAction mapReferentialAction(String action) return ImportedKey.ReferentialAction.NO_ACTION; } return switch (action.toUpperCase()) { - case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; - case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; - case "SET DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; - case "RESTRICT" -> ImportedKey.ReferentialAction.RESTRICT; - default -> ImportedKey.ReferentialAction.NO_ACTION; + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; + case "SET DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; + case "RESTRICT" -> ImportedKey.ReferentialAction.RESTRICT; + default -> ImportedKey.ReferentialAction.NO_ACTION; }; } @@ -843,8 +881,8 @@ private static IndexInfoItem.IndexType mapH2IndexType(String indexTypeName) { return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; } return switch (indexTypeName.toUpperCase()) { - case "HASH INDEX" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; - default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + case "HASH INDEX" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; }; } @@ -870,7 +908,7 @@ PrimaryKey build() { Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); TableReference tableRef = new TableReference(oSchema, tableName); List colRefs = columns.stream() - .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); } } @@ -897,7 +935,7 @@ UniqueConstraint build() { Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); TableReference tableRef = new TableReference(oSchema, tableName); List colRefs = columns.stream() - .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); return new UniqueConstraintRecord(constraintName, tableRef, colRefs); } } diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java index d9f47d5..89a40b9 100644 --- a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java @@ -43,9 +43,12 @@ import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.model.schema.CatalogReference; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.jdbc.api.schema.SynonymReference; import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; @@ -69,6 +72,7 @@ import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; import org.eclipse.daanse.sql.jdbc.record.schema.PseudoColumnRecord; import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SynonymRecord; import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; @@ -172,6 +176,53 @@ s.is_cycling, CAST(s.current_value AS BIGINT) AS current_value, } + @Override + public List getAllSynonyms(Connection connection, String catalog, String schema) throws SQLException { + // base_object_name is the target as written in CREATE SYNONYM, up to + // [server].[database].[schema].[object]; PARSENAME splits it and strips brackets. + // The target type is only looked up for objects in the current database — + // sys.objects knows nothing about other databases or linked servers. + String sql = """ + SELECT s.name AS synonym_name, + PARSENAME(s.base_object_name, 4) AS server_name, + PARSENAME(s.base_object_name, 3) AS database_name, + PARSENAME(s.base_object_name, 2) AS target_schema, + PARSENAME(s.base_object_name, 1) AS target_name, + CASE WHEN PARSENAME(s.base_object_name, 4) IS NULL + AND (PARSENAME(s.base_object_name, 3) IS NULL + OR PARSENAME(s.base_object_name, 3) = DB_NAME()) + THEN o.type_desc END AS target_type + FROM sys.synonyms s + LEFT JOIN sys.objects o ON o.object_id = OBJECT_ID(s.base_object_name) + WHERE s.schema_id = SCHEMA_ID(?) + ORDER BY s.name + """; + String schemaName = resolveSchema(schema, connection); + List synonyms = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("synonym_name"); + String databaseName = rs.getString("database_name"); + String targetSchema = rs.getString("target_schema"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + Optional oTargetCatalog = databaseName == null ? Optional.empty() + : Optional.of(new CatalogReference(databaseName)); + Optional oTargetSchema = targetSchema == null ? Optional.empty() + : Optional.of(new SchemaReference(oTargetCatalog, targetSchema)); + + synonyms.add(new SynonymRecord(new SynonymReference(oSchema, name), oTargetSchema, + rs.getString("target_name"), Optional.ofNullable(rs.getString("server_name")), false, + Optional.ofNullable(rs.getString("target_type")))); + } + } + } + return List.copyOf(synonyms); + } + + @Override public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { // SQL Server partitioning is always RANGE; sys.partitions has one row per diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java index 9d766bd..c009845 100644 --- a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java @@ -48,6 +48,8 @@ import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.jdbc.api.schema.SynonymReference; import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; @@ -72,6 +74,7 @@ import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; import org.eclipse.daanse.sql.jdbc.record.schema.PseudoColumnRecord; import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SynonymRecord; import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; @@ -169,6 +172,60 @@ public List getAllSequences(Connection connection, String catalog, Str } + @Override + public List getAllSynonyms(Connection connection, String catalog, String schema) throws SQLException { + // Private synonyms of the schema, plus PUBLIC synonyms pointing into it — all + // PUBLIC synonyms would pull in thousands of SYS/dictionary aliases. + // The target type comes from ALL_OBJECTS; synonyms over a DB link point to a + // remote object, so they are not joined. Object types outside the synonym + // namespace (INDEX, PACKAGE BODY, partitions, ...) can share the target's name + // and are excluded. + String sql = """ + SELECT s.OWNER, s.SYNONYM_NAME, s.TABLE_OWNER, s.TABLE_NAME, s.DB_LINK, o.OBJECT_TYPE + FROM ALL_SYNONYMS s + LEFT JOIN ALL_OBJECTS o + ON s.DB_LINK IS NULL + AND o.OWNER = s.TABLE_OWNER + AND o.OBJECT_NAME = s.TABLE_NAME + AND o.OBJECT_TYPE IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW', 'SEQUENCE', 'SYNONYM', + 'PROCEDURE', 'FUNCTION', 'PACKAGE', 'TYPE', 'JAVA CLASS') + WHERE s.OWNER = ? OR (s.OWNER = 'PUBLIC' AND s.TABLE_OWNER = ?) + ORDER BY s.OWNER, s.SYNONYM_NAME + """; + String schemaName = resolveSchema(schema, connection); + Map synonyms = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String owner = rs.getString("OWNER"); + String name = rs.getString("SYNONYM_NAME"); + String targetOwner = rs.getString("TABLE_OWNER"); + String objectType = rs.getString("OBJECT_TYPE"); + String key = owner + "\u0001" + name; + // A materialized view is listed in ALL_OBJECTS twice — as MATERIALIZED VIEW + // and as its container TABLE; keep the more specific one. + SynonymRecord existing = synonyms.get(key); + if (existing != null && !"MATERIALIZED VIEW".equals(objectType)) { + continue; + } + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), owner)); + Optional oTargetSchema = targetOwner == null ? Optional.empty() + : Optional.of(new SchemaReference(Optional.empty(), targetOwner)); + synonyms.put(key, new SynonymRecord(new SynonymReference(oSchema, name), oTargetSchema, + rs.getString("TABLE_NAME"), Optional.ofNullable(rs.getString("DB_LINK")), + "PUBLIC".equals(owner), Optional.ofNullable(objectType))); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read synonyms from ALL_SYNONYMS: {}", e.getMessage()); + return List.of(); + } + return List.copyOf(synonyms.values()); + } + + @Override public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { // Oracle exposes partitioning via ALL_TAB_PARTITIONS / ALL_TAB_SUBPARTITIONS diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2SynonymsTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2SynonymsTest.java new file mode 100644 index 0000000..3fe3ab2 --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2SynonymsTest.java @@ -0,0 +1,110 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class H2SynonymsTest { + + private static final String SCHEMA = "PUBLIC"; + + private static Connection connection; + private static H2MetadataProvider provider; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection("jdbc:h2:mem:h2SynonymsTest;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE TABLE CUSTOMERS (ID INT PRIMARY KEY, NAME VARCHAR(100))"); + stmt.execute("CREATE VIEW V_CUSTOMERS AS SELECT ID, NAME FROM CUSTOMERS"); + stmt.execute("CREATE SCHEMA OTHER"); + stmt.execute("CREATE TABLE OTHER.ORDERS (ID INT PRIMARY KEY)"); + stmt.execute("CREATE SYNONYM SYN_CUSTOMERS FOR CUSTOMERS"); + stmt.execute("CREATE SYNONYM SYN_VIEW FOR V_CUSTOMERS"); + stmt.execute("CREATE SYNONYM SYN_ORDERS FOR OTHER.ORDERS"); + stmt.execute("CREATE SYNONYM OTHER.SYN_BACK FOR PUBLIC.CUSTOMERS"); + } + provider = new H2MetadataProvider(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + void synonymToTable() throws SQLException { + Synonym syn = find(SCHEMA, "SYN_CUSTOMERS"); + assertThat(syn.schema()).map(SchemaReference::name).contains(SCHEMA); + assertThat(syn.targetSchema()).map(SchemaReference::name).contains(SCHEMA); + assertThat(syn.targetName()).isEqualTo("CUSTOMERS"); + assertThat(syn.targetObjectType()).contains("BASE TABLE"); + assertThat(syn.dbLink()).isEmpty(); + assertThat(syn.isPublic()).isFalse(); + } + + @Test + void synonymToView() throws SQLException { + Synonym syn = find(SCHEMA, "SYN_VIEW"); + assertThat(syn.targetName()).isEqualTo("V_CUSTOMERS"); + assertThat(syn.targetObjectType()).contains("VIEW"); + } + + @Test + void synonymIntoOtherSchema() throws SQLException { + Synonym syn = find(SCHEMA, "SYN_ORDERS"); + assertThat(syn.targetSchema()).map(SchemaReference::name).contains("OTHER"); + assertThat(syn.targetName()).isEqualTo("ORDERS"); + assertThat(syn.targetObjectType()).contains("BASE TABLE"); + } + + @Test + void onlySynonymsOfRequestedSchema() throws SQLException { + assertThat(provider.getAllSynonyms(connection, null, SCHEMA)).extracting(Synonym::name) + .containsExactly("SYN_CUSTOMERS", "SYN_ORDERS", "SYN_VIEW"); + assertThat(provider.getAllSynonyms(connection, null, "OTHER")).extracting(Synonym::name) + .containsExactly("SYN_BACK"); + } + + @Test + void defaultSchemaIsPublic() throws SQLException { + assertThat(provider.getAllSynonyms(connection, null, null)).extracting(Synonym::name) + .containsExactly("SYN_CUSTOMERS", "SYN_ORDERS", "SYN_VIEW"); + } + + @Test + void schemaWithoutSynonyms() throws Exception { + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE SCHEMA IF NOT EXISTS EMPTY_SCHEMA"); + } + assertThat(provider.getAllSynonyms(connection, null, "EMPTY_SCHEMA")).isEmpty(); + } + + private static Synonym find(String schema, String name) throws SQLException { + Optional syn = provider.getAllSynonyms(connection, null, schema).stream() + .filter(s -> s.name().equals(name)).findFirst(); + assertThat(syn).as("synonym %s.%s", schema, name).isPresent(); + return syn.get(); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlSynonymsTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlSynonymsTest.java new file mode 100644 index 0000000..afe4267 --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlSynonymsTest.java @@ -0,0 +1,110 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MsSqlSynonymsTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private static final String SCHEMA = "dbo"; + + private static Connection connection; + private static MicrosoftSqlServerMetadataProvider provider; + + @BeforeAll + void setUp() throws Exception { + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + connection = java.sql.DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE TABLE customers (id INT PRIMARY KEY, name NVARCHAR(100))"); + stmt.execute("CREATE SYNONYM syn_customers FOR dbo.customers"); + stmt.execute("CREATE SYNONYM syn_chain FOR [dbo].[syn_customers]"); + stmt.execute("CREATE SYNONYM syn_other_db FOR otherdb.sales.orders"); + stmt.execute("CREATE SYNONYM syn_linked FOR remotesrv.otherdb.sales.orders"); + } + provider = new MicrosoftSqlServerMetadataProvider(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + void localSynonymResolvesToTable() throws SQLException { + Synonym syn = find("syn_customers"); + assertThat(syn.schema()).map(SchemaReference::name).contains(SCHEMA); + assertThat(syn.targetSchema()).map(SchemaReference::name).contains("dbo"); + assertThat(syn.targetName()).isEqualTo("customers"); + assertThat(syn.targetObjectType()).contains("USER_TABLE"); + assertThat(syn.dbLink()).isEmpty(); + assertThat(syn.isPublic()).isFalse(); + } + + @Test + void chainedSynonymPointsToSynonym() throws SQLException { + Synonym syn = find("syn_chain"); + assertThat(syn.targetName()).isEqualTo("syn_customers"); + assertThat(syn.targetObjectType()).contains("SYNONYM"); + } + + @Test + void otherDatabaseTargetCarriesCatalogAndNoType() throws SQLException { + Synonym syn = find("syn_other_db"); + assertThat(syn.targetSchema()).map(SchemaReference::name).contains("sales"); + assertThat(syn.targetSchema()).flatMap(SchemaReference::catalog).map(CatalogReference::name) + .contains("otherdb"); + assertThat(syn.targetName()).isEqualTo("orders"); + assertThat(syn.targetObjectType()).isEmpty(); + } + + @Test + void linkedServerTargetReportedAsDbLink() throws SQLException { + Synonym syn = find("syn_linked"); + assertThat(syn.dbLink()).contains("remotesrv"); + assertThat(syn.targetObjectType()).isEmpty(); + } + + private Synonym find(String name) throws SQLException { + Optional syn = provider.getAllSynonyms(connection, null, SCHEMA).stream() + .filter(s -> s.name().equals(name)).findFirst(); + assertThat(syn).as("synonym %s", name).isPresent(); + return syn.get(); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/OracleSynonymsTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/OracleSynonymsTest.java new file mode 100644 index 0000000..bfcca60 --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/OracleSynonymsTest.java @@ -0,0 +1,140 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class OracleSynonymsTest { + + @Container + @SuppressWarnings("resource") + static final OracleContainer CONTAINER = new OracleContainer("gvenzl/oracle-free:23-slim-faststart") + .withStartupTimeout(Duration.ofMinutes(5)); + + private static final String SCHEMA = "TEST"; + + private static Connection connection; + private static OracleMetadataProvider provider; + + @BeforeAll + void setUp() throws Exception { + try (Connection system = DriverManager.getConnection(CONTAINER.getJdbcUrl(), "system", + CONTAINER.getPassword()); Statement stmt = system.createStatement()) { + stmt.execute("GRANT CREATE SYNONYM, CREATE PUBLIC SYNONYM, DROP PUBLIC SYNONYM, CREATE MATERIALIZED VIEW TO " + SCHEMA); + } + connection = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + try (Statement stmt = connection.createStatement()) { + stmt.execute("CREATE TABLE customers (id NUMBER PRIMARY KEY, name VARCHAR2(100))"); + stmt.execute("CREATE MATERIALIZED VIEW customer_names AS SELECT id, name FROM customers"); + stmt.execute("CREATE FUNCTION f_one RETURN NUMBER IS BEGIN RETURN 1; END;"); + stmt.execute("CREATE SYNONYM syn_customers FOR customers"); + stmt.execute("CREATE SYNONYM syn_chain FOR syn_customers"); + stmt.execute("CREATE SYNONYM syn_f_one FOR f_one"); + stmt.execute("CREATE SYNONYM syn_customer_names FOR customer_names"); + stmt.execute("CREATE SYNONYM syn_remote FOR customers@nolink"); + stmt.execute("CREATE PUBLIC SYNONYM pub_customers FOR " + SCHEMA + ".customers"); + } + provider = new OracleMetadataProvider(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP PUBLIC SYNONYM pub_customers"); + } + connection.close(); + } + } + + @Test + void privateSynonymResolvesToTable() throws SQLException { + Synonym syn = find("SYN_CUSTOMERS"); + assertThat(syn.schema()).map(SchemaReference::name).contains(SCHEMA); + assertThat(syn.targetSchema()).map(SchemaReference::name).contains(SCHEMA); + assertThat(syn.targetName()).isEqualTo("CUSTOMERS"); + assertThat(syn.targetObjectType()).contains("TABLE"); + assertThat(syn.isPublic()).isFalse(); + assertThat(syn.dbLink()).isEmpty(); + } + + @Test + void chainedSynonymPointsToSynonym() throws SQLException { + Synonym syn = find("SYN_CHAIN"); + assertThat(syn.targetName()).isEqualTo("SYN_CUSTOMERS"); + assertThat(syn.targetObjectType()).contains("SYNONYM"); + } + + @Test + void synonymToFunction() throws SQLException { + assertThat(find("SYN_F_ONE").targetObjectType()).contains("FUNCTION"); + } + + @Test + void synonymToMaterializedViewIsReportedOnce() throws SQLException { + List synonyms = provider.getAllSynonyms(connection, null, SCHEMA); + assertThat(synonyms).filteredOn(s -> s.name().equals("SYN_CUSTOMER_NAMES")).hasSize(1); + assertThat(find("SYN_CUSTOMER_NAMES").targetObjectType()).contains("MATERIALIZED VIEW"); + } + + @Test + void remoteSynonymHasDbLinkAndNoType() throws SQLException { + Synonym syn = find("SYN_REMOTE"); + assertThat(syn.dbLink()).hasValueSatisfying(link -> assertThat(link).startsWithIgnoringCase("NOLINK")); + assertThat(syn.targetObjectType()).isEmpty(); + } + + @Test + void publicSynonymsOnlyThosePointingIntoSchema() throws SQLException { + List publics = provider.getAllSynonyms(connection, null, SCHEMA).stream() + .filter(Synonym::isPublic).toList(); + assertThat(publics).extracting(Synonym::name).containsExactly("PUB_CUSTOMERS"); + Synonym pub = publics.get(0); + assertThat(pub.schema()).map(SchemaReference::name).contains("PUBLIC"); + assertThat(pub.targetObjectType()).contains("TABLE"); + } + + @Test + void defaultSchemaIsConnectionUser() throws SQLException { + assertThat(provider.getAllSynonyms(connection, null, null)).extracting(Synonym::name) + .contains("SYN_CUSTOMERS", "PUB_CUSTOMERS"); + } + + private Synonym find(String name) throws SQLException { + Optional syn = provider.getAllSynonyms(connection, null, SCHEMA).stream() + .filter(s -> s.name().equals(name)).findFirst(); + assertThat(syn).as("synonym %s", name).isPresent(); + return syn.get(); + } +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java index a9dcd66..0ce2362 100644 --- a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java @@ -29,6 +29,7 @@ import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; import org.eclipse.daanse.sql.model.schema.SchemaReference; import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; import org.eclipse.daanse.sql.jdbc.api.schema.DatabasePrincipal; import org.eclipse.daanse.sql.jdbc.api.schema.RoleMembership; @@ -59,7 +60,24 @@ public record StructureInfoRecord( List columnPrivileges, List objectPrivileges, List roleMemberships, - List principals) implements StructureInfo { + List principals, + List synonyms) implements StructureInfo { + + /** Compatibility constructor without synonyms. */ + public StructureInfoRecord(List catalogs, List schemas, + List tables, List columns, List importedKeys, + List primaryKeys, List triggers, List sequences, + List checkConstraints, List uniqueConstraints, + List userDefinedTypes, List viewDefinitions, + List procedures, List functions, List materializedViews, + List partitions, List tablePrivileges, + List columnPrivileges, List objectPrivileges, + List roleMemberships, List principals) { + this(catalogs, schemas, tables, columns, importedKeys, primaryKeys, triggers, sequences, checkConstraints, + uniqueConstraints, userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, + partitions, tablePrivileges, columnPrivileges, objectPrivileges, roleMemberships, principals, + List.of()); + } /** Compatibility constructor without principals. */ public StructureInfoRecord(List catalogs, List schemas, diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SynonymRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SynonymRecord.java new file mode 100644 index 0000000..acfeeb3 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SynonymRecord.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2026 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.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Synonym; +import org.eclipse.daanse.sql.jdbc.api.schema.SynonymReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +public record SynonymRecord( + SynonymReference reference, + Optional targetSchema, + String targetName, + Optional dbLink, + boolean isPublic, + Optional targetObjectType) implements Synonym { + +} From 207a123f1185c950049ec1fc47bf034c16607fab Mon Sep 17 00:00:00 2001 From: dbulahov Date: Tue, 6 Oct 2026 14:39:28 +0300 Subject: [PATCH 2/2] synonyms DDL generator interface implementation, support synonyms in dialects H2, MsSql, Oracle Signed-off-by: --- .../dialect/api/generator/DdlGenerator.java | 72 ++++++++++ .../daanse/sql/dialect/db/h2/H2Dialect.java | 5 + .../db/h2/sqlgen/H2SynonymDdlTest.java | 74 ++++++++++ .../MicrosoftSqlServerDialect.java | 31 +++++ .../sqlgen/MssqlSynonymDdlOfflineTest.java | 75 +++++++++++ .../sqlgen/MssqlSynonymDdlRoundTripTest.java | 104 +++++++++++++++ dialect/db/oracle/pom.xml | 6 + .../sql/dialect/db/oracle/OracleDialect.java | 38 ++++++ .../sqlgen/OracleSynonymDdlOfflineTest.java | 68 ++++++++++ .../sqlgen/OracleSynonymDdlRoundTripTest.java | 126 ++++++++++++++++++ .../impl/sqlgen/DialectDdlOfflineTest.java | 9 ++ 11 files changed, 608 insertions(+) create mode 100644 dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2SynonymDdlTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlOfflineTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlRoundTripTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlOfflineTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlRoundTripTest.java diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java index 11c10e4..c4c34af 100644 --- a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java @@ -525,6 +525,73 @@ default Optional nextValueFor(String schemaName, String name) { return Optional.of("NEXT VALUE FOR " + quoteIdentifier(schemaName, name)); } + // -------------------- DDL — synonyms -------------------- + + /** + * A synonym (alias) for another schema object. + * + * @param schemaName schema of the synonym; ignored for a public synonym + * @param name synonym name + * @param targetCatalogName catalog (database) of the target, or null for the current one + * @param targetSchemaName schema of the target, or null + * @param targetName name of the target + * @param dbLink database link / linked server the target lives behind, or null + * @param isPublic Oracle {@code PUBLIC} synonym + */ + record SynonymDefinition(String schemaName, String name, String targetCatalogName, String targetSchemaName, + String targetName, String dbLink, boolean isPublic) { + + public SynonymDefinition { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("synonym name must not be blank"); + } + if (targetName == null || targetName.isBlank()) { + throw new IllegalArgumentException("synonym target name must not be blank"); + } + } + + /** Private synonym {@code schema.name} for the local object {@code targetSchema.targetName}. */ + public static SynonymDefinition of(String schemaName, String name, String targetSchemaName, + String targetName) { + return new SynonymDefinition(schemaName, name, null, targetSchemaName, targetName, null, false); + } + } + + /** + * {@code CREATE [OR REPLACE] SYNONYM schema.name FOR targetSchema.target}. + * Empty when the dialect has no synonyms or cannot express the definition — + * the default spelling knows neither a target catalog, a DB link nor public + * synonyms. + */ + default Optional createSynonym(SynonymDefinition synonym, boolean orReplace) { + if (!supportsSynonyms() || synonym.isPublic() || synonym.dbLink() != null + || synonym.targetCatalogName() != null) { + return Optional.empty(); + } + StringBuilder sb = new StringBuilder("CREATE "); + if (orReplace) + sb.append("OR REPLACE "); + sb.append("SYNONYM ").append(quoteIdentifier(synonym.schemaName(), synonym.name())); + sb.append(" FOR ").append(quoteIdentifier(synonym.targetSchemaName(), synonym.targetName())); + return Optional.of(sb.toString()); + } + + /** + * {@code DROP SYNONYM [IF EXISTS] schema.name}; {@code IF EXISTS} follows + * {@link #supportsDropTableIfExists()}. Empty when the dialect has no synonyms + * or no public ones. + */ + default Optional dropSynonym(String schemaName, String name, boolean isPublic, boolean ifExists) { + if (!supportsSynonyms() || isPublic) { + return Optional.empty(); + } + StringBuilder sb = new StringBuilder("DROP SYNONYM "); + if (ifExists && supportsDropTableIfExists()) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(schemaName, name)); + return Optional.of(sb.toString()); + } + // -------------------- DDL — table-level constraints -------------------- /** @@ -1077,6 +1144,11 @@ default boolean supportsRenameSequence() { return false; } + /** @return true if {@code createSynonym} / {@code dropSynonym} render statements */ + default boolean supportsSynonyms() { + return false; + } + /** One table-rename step of a multi-rename. */ record TableRename(TableReference table, String newName) { } diff --git a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java index 80ed811..ace7d69 100644 --- a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java +++ b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java @@ -279,4 +279,9 @@ public PreparedPaging paginatePrepared(boolean withLimit, boolean withOffset) { return local; } + /** H2 synonyms point to tables and views of the same database (the default spelling). */ + @Override + public boolean supportsSynonyms() { + return true; + } } diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2SynonymDdlTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2SynonymDdlTest.java new file mode 100644 index 0000000..46beedd --- /dev/null +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/sqlgen/H2SynonymDdlTest.java @@ -0,0 +1,74 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.dialect.db.h2.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; + +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.SynonymDefinition; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.junit.jupiter.api.Test; + +/** Synonym DDL of the H2 dialect — as text and executed against H2. */ +class H2SynonymDdlTest { + + private final H2Dialect dialect = new H2Dialect(); + + @Test + void createAndDrop() { + assertThat(dialect.createSynonym(SynonymDefinition.of("PUBLIC", "EMP", "PUBLIC", "EMPLOYEES"), false)) + .contains("CREATE SYNONYM \"PUBLIC\".\"EMP\" FOR \"PUBLIC\".\"EMPLOYEES\""); + assertThat(dialect.createSynonym(SynonymDefinition.of("PUBLIC", "EMP", "PUBLIC", "EMPLOYEES"), true)) + .contains("CREATE OR REPLACE SYNONYM \"PUBLIC\".\"EMP\" FOR \"PUBLIC\".\"EMPLOYEES\""); + assertThat(dialect.dropSynonym("PUBLIC", "EMP", false, true)) + .contains("DROP SYNONYM IF EXISTS \"PUBLIC\".\"EMP\""); + } + + @Test + void remoteAndPublicTargetsAreNotExpressible() { + assertThat(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "EMP", null, "PUBLIC", "EMPLOYEES", "HQ", false), false)).isEmpty(); + assertThat(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "EMP", "OTHERDB", "PUBLIC", "EMPLOYEES", null, false), false)) + .isEmpty(); + assertThat(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "EMP", null, "PUBLIC", "EMPLOYEES", null, true), false)).isEmpty(); + } + + @Test + void generatedDdlRunsOnH2() throws Exception { + try (Connection c = DriverManager.getConnection("jdbc:h2:mem:h2SynonymDdl", "sa", ""); + Statement s = c.createStatement()) { + s.execute("CREATE SCHEMA HR"); + s.execute("CREATE TABLE HR.EMPLOYEES (ID INT PRIMARY KEY, NAME VARCHAR(50))"); + s.execute("INSERT INTO HR.EMPLOYEES VALUES (1, 'Ann')"); + + SynonymDefinition emp = SynonymDefinition.of("PUBLIC", "EMP", "HR", "EMPLOYEES"); + s.execute(dialect.createSynonym(emp, false).orElseThrow()); + s.execute(dialect.createSynonym(emp, true).orElseThrow()); + try (ResultSet rs = s.executeQuery("SELECT NAME FROM PUBLIC.EMP")) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("Ann"); + } + + s.execute(dialect.dropSynonym("PUBLIC", "EMP", false, false).orElseThrow()); + s.execute(dialect.dropSynonym("PUBLIC", "EMP", false, true).orElseThrow()); + try (ResultSet rs = s.executeQuery( + "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SYNONYMS WHERE SYNONYM_NAME = 'EMP'")) { + rs.next(); + assertThat(rs.getInt(1)).isZero(); + } + } + } +} diff --git a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java index 83655ed..b501e97 100644 --- a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java +++ b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java @@ -778,4 +778,35 @@ private String msDescription(TableReference table, String columnName, String com private static String nLiteral(String s) { return "N'" + s.replace("'", "''") + "'"; } + + @Override + public boolean supportsSynonyms() { + return true; + } + + /** + * {@code CREATE SYNONYM schema.name FOR [server.][database.][schema.]object}; the + * DB link is the linked server, a missing middle part stays empty + * ({@code server.db..object}). SQL Server has no {@code CREATE OR REPLACE SYNONYM} + * — {@code orReplace} is ignored. Empty for a public synonym. + */ + @Override + public Optional createSynonym(SynonymDefinition synonym, boolean orReplace) { + if (synonym.isPublic()) { + return Optional.empty(); + } + String[] parts = { synonym.dbLink(), synonym.targetCatalogName(), synonym.targetSchemaName() }; + int first = 0; + while (first < parts.length && parts[first] == null) + first++; + StringBuilder target = new StringBuilder(); + for (int i = first; i < parts.length; i++) { + if (parts[i] != null) + target.append(quoteIdentifier(parts[i])); + target.append('.'); + } + target.append(quoteIdentifier(synonym.targetName())); + return Optional.of("CREATE SYNONYM " + quoteIdentifier(synonym.schemaName(), synonym.name()) + " FOR " + + target); + } } diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlOfflineTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlOfflineTest.java new file mode 100644 index 0000000..b1984af --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlOfflineTest.java @@ -0,0 +1,75 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.dialect.db.mssqlserver.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.SynonymDefinition; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.junit.jupiter.api.Test; + +class MssqlSynonymDdlOfflineTest { + + private final MicrosoftSqlServerDialect dialect = new MicrosoftSqlServerDialect(); + + @Test + void supportsSynonyms() { + assertThat(dialect.supportsSynonyms()).isTrue(); + } + + @Test + void createLocalSynonym() { + assertThat(dialect.createSynonym(SynonymDefinition.of("dbo", "emp", "hr", "employees"), false)) + .contains("CREATE SYNONYM \"dbo\".\"emp\" FOR \"hr\".\"employees\""); + } + + @Test + void orReplaceIsIgnored() { + assertThat(dialect.createSynonym(SynonymDefinition.of("dbo", "emp", "hr", "employees"), true)) + .contains("CREATE SYNONYM \"dbo\".\"emp\" FOR \"hr\".\"employees\""); + } + + @Test + void targetInOtherDatabase() { + assertThat(dialect.createSynonym( + new SynonymDefinition("dbo", "ord", "otherdb", "sales", "orders", null, false), false)) + .contains("CREATE SYNONYM \"dbo\".\"ord\" FOR \"otherdb\".\"sales\".\"orders\""); + } + + @Test + void linkedServerWithAllParts() { + assertThat(dialect.createSynonym( + new SynonymDefinition("dbo", "ord", "otherdb", "sales", "orders", "remotesrv", false), false)) + .contains("CREATE SYNONYM \"dbo\".\"ord\" FOR \"remotesrv\".\"otherdb\".\"sales\".\"orders\""); + } + + @Test + void missingMiddlePartsStayEmpty() { + assertThat(dialect.createSynonym( + new SynonymDefinition("dbo", "ord", null, "sales", "orders", "remotesrv", false), false)) + .contains("CREATE SYNONYM \"dbo\".\"ord\" FOR \"remotesrv\"..\"sales\".\"orders\""); + assertThat(dialect.createSynonym( + new SynonymDefinition("dbo", "ord", "otherdb", null, "orders", null, false), false)) + .contains("CREATE SYNONYM \"dbo\".\"ord\" FOR \"otherdb\"..\"orders\""); + } + + @Test + void publicSynonymIsNotExpressible() { + assertThat(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "emp", null, "hr", "employees", null, true), false)).isEmpty(); + assertThat(dialect.dropSynonym("PUBLIC", "emp", true, false)).isEmpty(); + } + + @Test + void dropIfExists() { + assertThat(dialect.dropSynonym("dbo", "emp", false, true)).contains("DROP SYNONYM IF EXISTS \"dbo\".\"emp\""); + assertThat(dialect.dropSynonym("dbo", "emp", false, false)).contains("DROP SYNONYM \"dbo\".\"emp\""); + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlRoundTripTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlRoundTripTest.java new file mode 100644 index 0000000..0ff99ae --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlSynonymDdlRoundTripTest.java @@ -0,0 +1,104 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.dialect.db.mssqlserver.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; + +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.SynonymDefinition; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** The SQL Server dialect's synonym DDL, executed against a live SQL Server. */ +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MssqlSynonymDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private final MicrosoftSqlServerDialect dialect = new MicrosoftSqlServerDialect(); + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + connection = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + execute("CREATE TABLE dbo.employees (id INT PRIMARY KEY, name NVARCHAR(50))"); + execute("INSERT INTO dbo.employees VALUES (1, 'Ann')"); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + @Test + void localSynonymCreateQueryDrop() throws Exception { + execute(dialect.createSynonym(SynonymDefinition.of("dbo", "emp", "dbo", "employees"), false).orElseThrow()); + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT name FROM dbo.emp")) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(1)).isEqualTo("Ann"); + } + execute(dialect.dropSynonym("dbo", "emp", false, true).orElseThrow()); + execute(dialect.dropSynonym("dbo", "emp", false, true).orElseThrow()); + assertThat(synonymCount("emp")).isZero(); + } + + @Test + void otherDatabaseAndLinkedServerTargetsAreAccepted() throws Exception { + // SQL Server resolves the base object only on use, not when the synonym is created. + execute(dialect.createSynonym( + new SynonymDefinition("dbo", "ord_db", "otherdb", "sales", "orders", null, false), false) + .orElseThrow()); + execute(dialect.createSynonym( + new SynonymDefinition("dbo", "ord_srv", null, "sales", "orders", "remotesrv", false), false) + .orElseThrow()); + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery( + "SELECT name, base_object_name FROM sys.synonyms WHERE name LIKE 'ord%' ORDER BY name")) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(2)).isEqualTo("[otherdb].[sales].[orders]"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString(2)).isEqualTo("[remotesrv]..[sales].[orders]"); + } + execute(dialect.dropSynonym("dbo", "ord_db", false, false).orElseThrow()); + execute(dialect.dropSynonym("dbo", "ord_srv", false, false).orElseThrow()); + } + + private void execute(String sql) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + } + + private int synonymCount(String name) throws Exception { + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM sys.synonyms WHERE name = '" + name + "'")) { + rs.next(); + return rs.getInt(1); + } + } +} diff --git a/dialect/db/oracle/pom.xml b/dialect/db/oracle/pom.xml index 3c2e3e2..7a18461 100644 --- a/dialect/db/oracle/pom.xml +++ b/dialect/db/oracle/pom.xml @@ -93,6 +93,12 @@ 1.19.7 test + + org.testcontainers + oracle-free + 1.19.7 + test + org.testcontainers junit-jupiter diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java index 8e590bd..4b1b5bd 100644 --- a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java @@ -745,4 +745,42 @@ public Optional commentOnColumn(TableReference table, String columnName, private static String oracleLiteral(String comment) { return "'" + (comment == null ? "" : comment.replace("'", "''")) + "'"; } + + @Override + public boolean supportsSynonyms() { + return true; + } + + /** + * {@code CREATE [OR REPLACE] [PUBLIC] SYNONYM [schema.]name FOR [schema.]object[@link]}. + * A public synonym has no schema. Empty for a target in another catalog — Oracle + * reaches other databases only through a DB link. + */ + @Override + public Optional createSynonym(SynonymDefinition synonym, boolean orReplace) { + if (synonym.targetCatalogName() != null) { + return Optional.empty(); + } + StringBuilder sb = new StringBuilder("CREATE "); + if (orReplace) + sb.append("OR REPLACE "); + if (synonym.isPublic()) + sb.append("PUBLIC "); + sb.append("SYNONYM ").append(synonymName(synonym.schemaName(), synonym.name(), synonym.isPublic())); + sb.append(" FOR ").append(quoteIdentifier(synonym.targetSchemaName(), synonym.targetName())); + if (synonym.dbLink() != null) + sb.append('@').append(synonym.dbLink()); + return Optional.of(sb.toString()); + } + + /** {@code DROP [PUBLIC] SYNONYM [schema.]name}; Oracle has no {@code IF EXISTS} here. */ + @Override + public Optional dropSynonym(String schemaName, String name, boolean isPublic, boolean ifExists) { + return Optional.of((isPublic ? "DROP PUBLIC SYNONYM " : "DROP SYNONYM ") + + synonymName(schemaName, name, isPublic)); + } + + private String synonymName(String schemaName, String name, boolean isPublic) { + return isPublic ? quoteIdentifier(name) : quoteIdentifier(schemaName, name); + } } diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlOfflineTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlOfflineTest.java new file mode 100644 index 0000000..5e02921 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlOfflineTest.java @@ -0,0 +1,68 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.dialect.db.oracle.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.SynonymDefinition; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialect; +import org.junit.jupiter.api.Test; + +class OracleSynonymDdlOfflineTest { + + private final OracleDialect dialect = new OracleDialect(); + + @Test + void supportsSynonyms() { + assertThat(dialect.supportsSynonyms()).isTrue(); + } + + @Test + void createPrivateSynonym() { + assertThat(dialect.createSynonym(SynonymDefinition.of("HR", "EMP", "HR", "EMPLOYEES"), false)) + .contains("CREATE SYNONYM \"HR\".\"EMP\" FOR \"HR\".\"EMPLOYEES\""); + } + + @Test + void createOrReplace() { + assertThat(dialect.createSynonym(SynonymDefinition.of("HR", "EMP", "HR", "EMPLOYEES"), true)) + .contains("CREATE OR REPLACE SYNONYM \"HR\".\"EMP\" FOR \"HR\".\"EMPLOYEES\""); + } + + @Test + void createPublicSynonymHasNoSchema() { + assertThat(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "EMP", null, "HR", "EMPLOYEES", null, true), false)) + .contains("CREATE PUBLIC SYNONYM \"EMP\" FOR \"HR\".\"EMPLOYEES\""); + } + + @Test + void createSynonymOverDbLink() { + assertThat(dialect.createSynonym( + new SynonymDefinition("HR", "EMP_R", null, "HR", "EMPLOYEES", "HQ.EXAMPLE.COM", false), false)) + .contains("CREATE SYNONYM \"HR\".\"EMP_R\" FOR \"HR\".\"EMPLOYEES\"@HQ.EXAMPLE.COM"); + } + + @Test + void targetInOtherCatalogIsNotExpressible() { + assertThat(dialect.createSynonym( + new SynonymDefinition("HR", "EMP", "OTHERDB", "HR", "EMPLOYEES", null, false), false)).isEmpty(); + } + + @Test + void dropHasNoIfExists() { + assertThat(dialect.dropSynonym("HR", "EMP", false, true)).contains("DROP SYNONYM \"HR\".\"EMP\""); + } + + @Test + void dropPublicSynonym() { + assertThat(dialect.dropSynonym("PUBLIC", "EMP", true, false)).contains("DROP PUBLIC SYNONYM \"EMP\""); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlRoundTripTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlRoundTripTest.java new file mode 100644 index 0000000..deebe2c --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleSynonymDdlRoundTripTest.java @@ -0,0 +1,126 @@ +/* +* Copyright (c) 2026 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 +*/ +package org.eclipse.daanse.sql.dialect.db.oracle.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.time.Duration; + +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator.SynonymDefinition; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.oracle.OracleContainer; + +/** The Oracle dialect's synonym DDL, executed against a live Oracle. */ +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class OracleSynonymDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final OracleContainer CONTAINER = new OracleContainer("gvenzl/oracle-free:23-slim-faststart") + .withStartupTimeout(Duration.ofMinutes(5)); + + private static final String SCHEMA = "TEST"; + + private final OracleDialect dialect = new OracleDialect(); + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + try (Connection system = DriverManager.getConnection(CONTAINER.getJdbcUrl(), "system", + CONTAINER.getPassword()); Statement s = system.createStatement()) { + s.execute("GRANT CREATE SYNONYM, CREATE PUBLIC SYNONYM, DROP PUBLIC SYNONYM TO " + SCHEMA); + } + connection = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + execute("CREATE TABLE EMPLOYEES (ID NUMBER PRIMARY KEY, NAME VARCHAR2(50))"); + execute("INSERT INTO EMPLOYEES VALUES (1, 'Ann')"); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + @Test + void privateSynonymCreateReplaceQueryDrop() throws Exception { + SynonymDefinition emp = SynonymDefinition.of(SCHEMA, "EMP", SCHEMA, "EMPLOYEES"); + execute(dialect.createSynonym(emp, false).orElseThrow()); + execute(dialect.createSynonym(emp, true).orElseThrow()); + assertThat(queryName("SELECT NAME FROM EMP")).isEqualTo("Ann"); + + execute(dialect.dropSynonym(SCHEMA, "EMP", false, true).orElseThrow()); + assertThat(count("SELECT COUNT(*) FROM ALL_SYNONYMS WHERE OWNER = ? AND SYNONYM_NAME = 'EMP'", SCHEMA)) + .isZero(); + } + + @Test + void publicSynonymCreateQueryDrop() throws Exception { + execute(dialect.createSynonym( + new SynonymDefinition("PUBLIC", "EMP_ALL", null, SCHEMA, "EMPLOYEES", null, true), false) + .orElseThrow()); + assertThat(count("SELECT COUNT(*) FROM ALL_SYNONYMS WHERE OWNER = ? AND SYNONYM_NAME = 'EMP_ALL'", + "PUBLIC")).isEqualTo(1); + assertThat(queryName("SELECT NAME FROM EMP_ALL")).isEqualTo("Ann"); + + execute(dialect.dropSynonym("PUBLIC", "EMP_ALL", true, false).orElseThrow()); + assertThat(count("SELECT COUNT(*) FROM ALL_SYNONYMS WHERE OWNER = ? AND SYNONYM_NAME = 'EMP_ALL'", + "PUBLIC")).isZero(); + } + + @Test + void dbLinkSynonymIsAccepted() throws Exception { + // Oracle checks neither the link nor the remote object when creating the synonym. + execute(dialect.createSynonym( + new SynonymDefinition(SCHEMA, "EMP_REMOTE", null, "HR", "EMPLOYEES", "HQ_LINK", false), false) + .orElseThrow()); + assertThat(count("SELECT COUNT(*) FROM ALL_SYNONYMS WHERE OWNER = ? AND SYNONYM_NAME = 'EMP_REMOTE'" + + " AND DB_LINK LIKE 'HQ_LINK%'", SCHEMA)).isEqualTo(1); + execute(dialect.dropSynonym(SCHEMA, "EMP_REMOTE", false, false).orElseThrow()); + } + + private void execute(String sql) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + } + + private String queryName(String sql) throws Exception { + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery(sql)) { + assertThat(rs.next()).isTrue(); + return rs.getString(1); + } + } + + private int count(String sql, String owner) throws Exception { + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, owner); + try (ResultSet rs = ps.executeQuery()) { + rs.next(); + return rs.getInt(1); + } + } + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java index 417ffba..02705b3 100644 --- a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java @@ -193,6 +193,15 @@ void createSequence_minimal() { .contains("CREATE SEQUENCE \"ORDER_SEQ\""); } + @Test + void synonyms_are_unsupported_by_default() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.ddlGenerator().supportsSynonyms()).isFalse(); + assertThat(dialect.ddlGenerator().createSynonym( + DdlGenerator.SynonymDefinition.of("PUBLIC", "EMP", "PUBLIC", "EMPLOYEES"), false)).isEmpty(); + assertThat(dialect.ddlGenerator().dropSynonym("PUBLIC", "EMP", false, true)).isEmpty(); + } + @Test void dropSequence_if_exists() { Dialect dialect = new AnsiDialect();