Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,73 @@ default Optional<String> 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<String> 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<String> 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 --------------------

/**
Expand Down Expand Up @@ -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) {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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\"");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading
Loading