diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 84fe26aa8a0..3d7b000a379 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: run: mvn -T 1C formatter:validate build-deploy: - needs: [ tests, integration-tests-h2, integration-tests-postgresql ] + needs: [ tests, integration-tests-h2, integration-tests-postgresql, integration-tests-mssql ] runs-on: ${{ matrix.runner }} strategy: matrix: @@ -207,7 +207,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - retention-days: 1 + retention-days: 3 name: ${{ env.ARTIFACT_NAME }} path: tests/tests-integrations/build/reports/tests @@ -285,7 +285,77 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - retention-days: 1 + retention-days: 3 + name: ${{ env.ARTIFACT_NAME }} + path: tests/tests-integrations/build/reports/tests + + integration-tests-mssql: + runs-on: ubuntu-latest + env: + MSSQL_PASS: 'mYPass1234!' + services: + mssql: + image: mcr.microsoft.com/mssql/server:2022-CU22-ubuntu-22.04 + ports: + - 1433:1433 + env: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: ${{ env.MSSQL_PASS }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + + - name: Set up JDK Corretto 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + architecture: x64 + + - name: Install NodeJS + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install TypeScript and esbuild + run: npm install -g typescript esbuild + + - name: Install ttyd (prebuilt) + run: | + sudo apt update + sudo apt install -y ttyd + + - name: Verify ttyd installation + run: ttyd --version + + - name: Integration tests + run: mvn clean install -P integration-tests + env: + DIRIGIBLE_DATASOURCE_DEFAULT_DRIVER: 'com.microsoft.sqlserver.jdbc.SQLServerDriver' + DIRIGIBLE_DATASOURCE_DEFAULT_URL: 'jdbc:sqlserver://localhost:1433;databaseName=master;encrypt=true;trustServerCertificate=true' + DIRIGIBLE_DATASOURCE_DEFAULT_USERNAME: 'sa' + DIRIGIBLE_DATASOURCE_DEFAULT_PASSWORD: ${{ env.MSSQL_PASS }} + + - name: Generate a random artifact name + if: always() + id: generate_name + run: | + TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + echo "ARTIFACT_NAME=selenide-screenshots-${TIMESTAMP}.zip" >> $GITHUB_ENV + + - name: Upload selenide screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + retention-days: 3 name: ${{ env.ARTIFACT_NAME }} path: tests/tests-integrations/build/reports/tests diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 61748afc550..35ae1b702b8 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -152,7 +152,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - retention-days: 1 + retention-days: 3 name: ${{ env.ARTIFACT_NAME }} path: tests/tests-integrations/build/reports/tests @@ -230,6 +230,77 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - retention-days: 1 + retention-days: 3 name: ${{ env.ARTIFACT_NAME }} path: tests/tests-integrations/build/reports/tests + + integration-tests-mssql: + runs-on: ubuntu-latest + env: + MSSQL_PASS: 'mYPass1234!' + services: + mssql: + image: mcr.microsoft.com/mssql/server:2022-CU22-ubuntu-22.04 + ports: + - 1433:1433 + env: + MSSQL_SA_PASSWORD: ${{ env.MSSQL_PASS }} + ACCEPT_EULA: Y + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- + + - name: Set up JDK Corretto 21 + uses: actions/setup-java@v4 + with: + distribution: 'corretto' + java-version: '21' + architecture: x64 + + - name: Install NodeJS + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Install TypeScript and esbuild + run: npm install -g typescript esbuild + + - name: Install ttyd (prebuilt) + run: | + sudo apt update + sudo apt install -y ttyd + + - name: Verify ttyd installation + run: ttyd --version + + - name: Integration tests + run: mvn clean install -P integration-tests + env: + DIRIGIBLE_DATASOURCE_DEFAULT_DRIVER: 'com.microsoft.sqlserver.jdbc.SQLServerDriver' + DIRIGIBLE_DATASOURCE_DEFAULT_URL: 'jdbc:sqlserver://localhost:1433;databaseName=master;encrypt=true;trustServerCertificate=true' + DIRIGIBLE_DATASOURCE_DEFAULT_USERNAME: 'sa' + DIRIGIBLE_DATASOURCE_DEFAULT_PASSWORD: ${{ env.MSSQL_PASS }} + + - name: Generate a random artifact name + if: always() + id: generate_name + run: | + TIMESTAMP=$(date +"%Y%m%d_%H%M%S") + echo "ARTIFACT_NAME=selenide-screenshots-${TIMESTAMP}.zip" >> $GITHUB_ENV + + - name: Upload selenide screenshots + uses: actions/upload-artifact@v4 + if: always() + with: + retention-days: 3 + name: ${{ env.ARTIFACT_NAME }} + path: tests/tests-integrations/build/reports/tests + diff --git a/components/api/api-database/src/main/java/org/eclipse/dirigible/components/api/db/DatabaseFacade.java b/components/api/api-database/src/main/java/org/eclipse/dirigible/components/api/db/DatabaseFacade.java index 04a513da675..b600a046437 100644 --- a/components/api/api-database/src/main/java/org/eclipse/dirigible/components/api/db/DatabaseFacade.java +++ b/components/api/api-database/src/main/java/org/eclipse/dirigible/components/api/db/DatabaseFacade.java @@ -66,6 +66,10 @@ public class DatabaseFacade implements InitializingBean { private static final Logger logger = LoggerFactory.getLogger(DatabaseFacade.class); private static final Set DATA_SOURCES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE = new HashSet<>(); + private static final Set DATABASES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE = Set.of(// + // executeBatch() + getGeneratedKeys() is unsupported in MSSQL, + // possible refactoring with INSERT … OUTPUT INSERTED + DatabaseSystem.MSSQL); /** The database facade. */ private static DatabaseFacade INSTANCE; @@ -390,7 +394,8 @@ public static List> insert(String sql, String parametersJson return LoggingExecutor.executeWithException(dataSource, () -> { - if (DATA_SOURCES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getName())) { + if (DATA_SOURCES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getName()) + || DATABASES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getDatabaseSystem())) { logger.debug("RETURN_GENERATED_KEYS not supported for data source [{}]. Will execute insert without this option.", dataSource); try (Connection connection = dataSource.getConnection()) { @@ -465,7 +470,8 @@ static List> insertMany(String sql, Optional pa throws Throwable { return LoggingExecutor.executeWithException(dataSource, () -> { - if (DATA_SOURCES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getName())) { + if (DATA_SOURCES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getName()) + || DATABASES_NOT_SUPPORTING_RETURN_GENERATED_KEYS_FEATURE.contains(dataSource.getDatabaseSystem())) { logger.debug("RETURN_GENERATED_KEYS not supported for data source [{}]. Will execute insert without this option.", dataSource); insertManyWithoutResult(sql, parameters, dataSource); @@ -527,6 +533,9 @@ private static void insertManyWithoutResult(String sql, Optional pa } else { ParametersSetter.setManyIndexedParameters(parameters.get(), new ParameterizedStatement(preparedStatement)); } + } else { + // required for DBs like MSSQL, DBs like H2 and Postgresql doesn't need it + preparedStatement.addBatch(); } preparedStatement.executeBatch(); connection.commit(); diff --git a/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/db/database.ts b/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/db/database.ts index 0f0050db1e0..d70013efc3c 100644 --- a/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/db/database.ts +++ b/components/api/api-modules-javascript/src/main/resources/META-INF/dirigible/modules/src/db/database.ts @@ -41,7 +41,7 @@ export const SQLTypes = Object.freeze({ }); export enum DatabaseSystem { - UNKNOWN, DERBY, POSTGRESQL, H2, MARIADB, HANA, SNOWFLAKE, MYSQL, MONGODB, SYBASE + UNKNOWN, DERBY, POSTGRESQL, H2, MARIADB, HANA, SNOWFLAKE, MYSQL, MONGODB, SYBASE, MSSQL } // --- Metadata Interfaces --- @@ -1201,6 +1201,8 @@ export class Connection { return DatabaseSystem.HANA; case "SNOWFLAKE": return DatabaseSystem.SNOWFLAKE; + case "MSSQL": + return DatabaseSystem.MSSQL; case "MYSQL": return DatabaseSystem.MYSQL; case "MONGODB": diff --git a/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystem.java b/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystem.java index d8df26ff43a..1a0b4520bb1 100644 --- a/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystem.java +++ b/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystem.java @@ -13,16 +13,24 @@ public enum DatabaseSystem { // when adding or changing enum values do NOT forget to // update the JavaScript API in the connection class located at: // dirigible/modules/src/db/database.ts - UNKNOWN, DERBY, POSTGRESQL, H2, MARIADB, HANA, SNOWFLAKE, MYSQL, MONGODB; + UNKNOWN, DERBY, POSTGRESQL, H2, MARIADB, HANA, SNOWFLAKE, MYSQL, MONGODB, MSSQL; public boolean isH2() { return isOfType(H2); } + public boolean isOfType(DatabaseSystem databaseSystem) { + return this == databaseSystem; + } + public boolean isSnowflake() { return isOfType(SNOWFLAKE); } + public boolean isMSSQL() { + return isOfType(MSSQL); + } + public boolean isHANA() { return isOfType(HANA); } @@ -50,8 +58,4 @@ public boolean isMongoDB() { public boolean isDerby() { return isOfType(DERBY); } - - public boolean isOfType(DatabaseSystem databaseSystem) { - return this == databaseSystem; - } } diff --git a/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminer.java b/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminer.java index b797a5e4b76..1fe15dc9165 100644 --- a/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminer.java +++ b/components/core/core-database/src/main/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminer.java @@ -33,7 +33,8 @@ public class DatabaseSystemDeterminer { DatabaseSystem.MARIADB, "jdbc:mariadb", // DatabaseSystem.MYSQL, "jdbc:mysql", // DatabaseSystem.MONGODB, "jdbc:mongodb", // - DatabaseSystem.DERBY, "jdbc:derby"// + DatabaseSystem.DERBY, "jdbc:derby", // + DatabaseSystem.MSSQL, "jdbc:sqlserver"// ); private static final Map> DB_DRIVERS = Map.of(// @@ -44,6 +45,7 @@ public class DatabaseSystemDeterminer { DatabaseSystem.MARIADB, List.of("org.mariadb.jdbc.Driver"), // DatabaseSystem.MYSQL, List.of("com.mysql.cj.jdbc.Driver"), // DatabaseSystem.MONGODB, List.of("com.mongodb.jdbc.MongoDriver"), // + DatabaseSystem.MSSQL, List.of("com.microsoft.sqlserver.jdbc.SQLServerDriver"), // DatabaseSystem.DERBY, List.of("org.apache.derby.jdbc.ClientDriver", "org.apache.derby.jdbc.EmbeddedDriver")// ); @@ -89,15 +91,6 @@ private static Optional determineSystemByJdbcUrl(String jdbcUrl) .map(Map.Entry::getKey); } - private static DatabaseSystem determineSystemByDriverClass(String driverClass) { - return DB_DRIVERS.entrySet() - .stream() - .filter(entry -> usedOneOfDrivers(driverClass, entry.getValue())) - .findFirst() - .map(Map.Entry::getKey) - .orElse(DatabaseSystem.UNKNOWN); - } - private static boolean isJdbcUrlStartWithString(String jdbcUrl, String prefix) { if (StringUtils.isBlank(jdbcUrl)) { LOGGER.warn("Received blank JDBC URL [{}]", jdbcUrl, jdbcUrl); @@ -108,6 +101,15 @@ private static boolean isJdbcUrlStartWithString(String jdbcUrl, String prefix) { .startsWith(prefix); } + private static DatabaseSystem determineSystemByDriverClass(String driverClass) { + return DB_DRIVERS.entrySet() + .stream() + .filter(entry -> usedOneOfDrivers(driverClass, entry.getValue())) + .findFirst() + .map(Map.Entry::getKey) + .orElse(DatabaseSystem.UNKNOWN); + } + private static boolean usedOneOfDrivers(String driverClass, List drivers) { String trimmedDriverClassName = safelyTrim(driverClass); return drivers.stream() diff --git a/components/core/core-database/src/test/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminerTest.java b/components/core/core-database/src/test/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminerTest.java index 021a1b07ccc..2a77a1ac105 100644 --- a/components/core/core-database/src/test/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminerTest.java +++ b/components/core/core-database/src/test/java/org/eclipse/dirigible/components/database/DatabaseSystemDeterminerTest.java @@ -22,6 +22,28 @@ class DatabaseSystemDeterminerTest { @InjectMocks private DatabaseSystemDeterminer databaseSystemDeterminer; + @Test + void testDetermineMSSQL_withUrl() { + testDetermine_withUrl("jdbc:sqlserver://localhost:1433;databaseName=master", DatabaseSystem.MSSQL); + } + + private void testDetermine_withUrl(String jdbcUrl, DatabaseSystem expectedType) { + DatabaseSystem result = DatabaseSystemDeterminer.determine(jdbcUrl, null); + + assertEquals(expectedType, result); + } + + @Test + void testDetermineMSSQL_withDriverUrl() { + testDetermine_withDriver("com.microsoft.sqlserver.jdbc.SQLServerDriver", DatabaseSystem.MSSQL); + } + + private void testDetermine_withDriver(String driverClass, DatabaseSystem expectedType) { + DatabaseSystem result = DatabaseSystemDeterminer.determine(null, driverClass); + + assertEquals(expectedType, result); + } + @Test void testDetermineDerby_withUrl() { testDetermine_withUrl("jdbc:derby://localhost:1527/databaseName;create=true", DatabaseSystem.DERBY); @@ -117,16 +139,4 @@ void testDetermineUnknown_withDriver() { testDetermine_withDriver("com.unknown.Driver", DatabaseSystem.UNKNOWN); } - private void testDetermine_withUrl(String jdbcUrl, DatabaseSystem expectedType) { - DatabaseSystem result = DatabaseSystemDeterminer.determine(jdbcUrl, null); - - assertEquals(expectedType, result); - } - - private void testDetermine_withDriver(String driverClass, DatabaseSystem expectedType) { - DatabaseSystem result = DatabaseSystemDeterminer.determine(null, driverClass); - - assertEquals(expectedType, result); - } - } diff --git a/components/data/data-core/pom.xml b/components/data/data-core/pom.xml index 8c3a882e2c3..50f238bc7e1 100644 --- a/components/data/data-core/pom.xml +++ b/components/data/data-core/pom.xml @@ -46,7 +46,11 @@ org.eclipse.dirigible dirigible-database-sql-postgres - + + org.eclipse.dirigible + dirigible-database-sql-mssql + + com.github.jsqlparser diff --git a/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvProcessor.java b/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvProcessor.java index e01e552fb59..ca246baf0ad 100644 --- a/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvProcessor.java +++ b/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvProcessor.java @@ -9,21 +9,6 @@ */ package org.eclipse.dirigible.components.data.csvim.processor; -import java.io.ByteArrayInputStream; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Types; -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.Base64; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.stream.Collectors; - import org.apache.commons.csv.CSVRecord; import org.apache.commons.lang3.StringUtils; import org.eclipse.dirigible.commons.api.helpers.DateTimeUtils; @@ -44,6 +29,21 @@ import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; +import java.io.ByteArrayInputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Base64; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.stream.Collectors; + /** * The Class CsvProcessor. */ @@ -407,6 +407,9 @@ protected void setValue(PreparedStatement preparedStatement, int paramIdx, Strin } else if (Types.DOUBLE == DataTypeUtils.getSqlTypeByDataType(dataType)) { value = numberize(value, locale); preparedStatement.setDouble(paramIdx, parseDouble(value)); + } else if (Types.FLOAT == DataTypeUtils.getSqlTypeByDataType(dataType)) { + value = numberize(value, locale); + preparedStatement.setFloat(paramIdx, parseFloat(value)); } else if (Types.BOOLEAN == DataTypeUtils.getSqlTypeByDataType(dataType) || Types.BIT == DataTypeUtils.getSqlTypeByDataType(dataType)) { preparedStatement.setBoolean(paramIdx, parseBoolean(value)); diff --git a/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvimProcessor.java b/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvimProcessor.java index fa81025093e..e9325e4bf20 100644 --- a/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvimProcessor.java +++ b/components/data/data-csvim/src/main/java/org/eclipse/dirigible/components/data/csvim/processor/CsvimProcessor.java @@ -9,21 +9,6 @@ */ package org.eclipse.dirigible.components.data.csvim.processor; -import static org.eclipse.dirigible.components.api.platform.RepositoryFacade.getResource; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; - import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; @@ -52,6 +37,21 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static org.eclipse.dirigible.components.api.platform.RepositoryFacade.getResource; + /** * The Class CsvimProcessor. */ @@ -154,6 +154,7 @@ public void process(CsvFile csvFile, InputStream content, String dataSourceName) if (defaultDataSource) { if (("PUBLIC".equals(fileSchema) && dataSource.isOfType(DatabaseSystem.H2))// || ("public".equals(fileSchema) && dataSource.isOfType(DatabaseSystem.POSTGRESQL))// + || ("dbo".equals(fileSchema) && dataSource.isOfType(DatabaseSystem.MSSQL))// || (dataSourceName == null && "public".equalsIgnoreCase(fileSchema))) { // 1. needed for the multitenancy logic - change the schema to the default db schema for this tenant // 2. when datasource is not specified but the import is for default schema - import into defaultdb diff --git a/components/data/data-management/pom.xml b/components/data/data-management/pom.xml index 0704fa9fc05..2f2fb11023e 100644 --- a/components/data/data-management/pom.xml +++ b/components/data/data-management/pom.xml @@ -64,6 +64,10 @@ org.eclipse.dirigible dirigible-database-sql-postgres + + org.eclipse.dirigible + dirigible-database-sql-mssql + diff --git a/components/data/data-sources/src/main/java/org/eclipse/dirigible/components/data/sources/provisioning/DefaultDataSourceProvisioning.java b/components/data/data-sources/src/main/java/org/eclipse/dirigible/components/data/sources/provisioning/DefaultDataSourceProvisioning.java index a13ba49493d..04c5bf2149d 100644 --- a/components/data/data-sources/src/main/java/org/eclipse/dirigible/components/data/sources/provisioning/DefaultDataSourceProvisioning.java +++ b/components/data/data-sources/src/main/java/org/eclipse/dirigible/components/data/sources/provisioning/DefaultDataSourceProvisioning.java @@ -20,6 +20,8 @@ import org.eclipse.dirigible.components.data.sources.manager.DataSourcesManager; import org.eclipse.dirigible.components.data.sources.manager.TenantDataSourceNameManager; import org.eclipse.dirigible.components.data.sources.service.DataSourceService; +import org.eclipse.dirigible.components.database.DatabaseSystem; +import org.eclipse.dirigible.components.database.DirigibleDataSource; import org.eclipse.dirigible.database.sql.SqlFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -81,11 +83,16 @@ public void execute(Tenant tenant) throws TenantProvisioningException { String schema = createSchema(tenant, userId); LOGGER.info("Created schema [{}] for tenant [{}] and user [{}]", schema, tenant, userId); + DirigibleDataSource defaultDataSource = dataSourcesManager.getDefaultDataSource(); + if (defaultDataSource.isOfType(DatabaseSystem.MSSQL)) { + assignUserDefaultSchemaInMSSQL(userId, schema); + grantUserPermissionsInMSSQL(userId); + } + DataSource dataSource = registerDataSource(tenant, userId, password, schema); LOGGER.info("Registered data source [{}] for tenant [{}]", dataSource, tenant); LOGGER.info("Default DataSource for tenant [{}] has been registered.", tenant); - } /** @@ -131,17 +138,18 @@ private void createUser(Tenant tenant, String userId, String password) { * @throws TenantProvisioningException the tenant provisioning exception */ private String createSchema(Tenant tenant, String userId) throws TenantProvisioningException { - javax.sql.DataSource dataSource = dataSourcesManager.getDefaultDataSource(); + DirigibleDataSource dataSource = dataSourcesManager.getDefaultDataSource(); try (Connection connection = dataSource.getConnection()) { String schemaName = getSchemaName(tenant); - String sql = SqlFactory.getNative(connection) - .create() - .schema(schemaName) - .authorization(userId) - .build(); + String createSchemaSql = SqlFactory.getNative(connection) + .create() + .schema(schemaName) + .authorization(userId) + .build(); - try (PreparedStatement prepareStatement = connection.prepareStatement(sql)) { + try (PreparedStatement prepareStatement = connection.prepareStatement(createSchemaSql)) { prepareStatement.execute(); + LOGGER.debug("Created schema [{}] with owner [{}]", schemaName, userId); } return schemaName; @@ -161,6 +169,52 @@ private String getSchemaName(Tenant tenant) { .toUpperCase(); } + private void assignUserDefaultSchemaInMSSQL(String userId, String schemaName) { + DirigibleDataSource dataSource = dataSourcesManager.getDefaultDataSource(); + + // ALTER USER WITH DEFAULT_SCHEMA = + String alterUserSql = """ + DECLARE @sql nvarchar(max); + SET @sql = N'ALTER USER ' + QUOTENAME(?) + + N' WITH DEFAULT_SCHEMA = ' + QUOTENAME(?); + EXEC sp_executesql @sql; + """; + + try (Connection connection = dataSource.getConnection(); PreparedStatement ps = connection.prepareStatement(alterUserSql)) { + ps.setString(1, userId); + ps.setString(2, schemaName); + ps.execute(); + + LOGGER.info("Set default schema [{}] for user [{}]", schemaName, userId); + } catch (SQLException ex) { + throw new TenantProvisioningException("Failed to set default schema [" + schemaName + "] for user " + userId, ex); + } + } + + private void grantUserPermissionsInMSSQL(String userId) { + DirigibleDataSource dataSource = dataSourcesManager.getDefaultDataSource(); + + // GRANT CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE TYPE TO + String grantSql = """ + DECLARE @sql nvarchar(max); + + SET @sql = + N'GRANT CREATE TABLE, CREATE VIEW, CREATE PROCEDURE, CREATE TYPE TO ' + + QUOTENAME(?); + + EXEC sp_executesql @sql; + """; + + try (Connection connection = dataSource.getConnection(); PreparedStatement ps = connection.prepareStatement(grantSql)) { + ps.setString(1, userId); + ps.execute(); + + LOGGER.info("Granted permissions to user [{}] with sql [{}]", userId, grantSql); + } catch (SQLException ex) { + throw new TenantProvisioningException("Failed to grant permissions to user " + userId, ex); + } + } + /** * Register data source. * diff --git a/components/engine/engine-cms-internal/src/main/java/org/eclipse/dirigible/components/engine/cms/internal/CmsProviderInternalFactory.java b/components/engine/engine-cms-internal/src/main/java/org/eclipse/dirigible/components/engine/cms/internal/CmsProviderInternalFactory.java index 8e284db5e34..2c6ab94dc23 100644 --- a/components/engine/engine-cms-internal/src/main/java/org/eclipse/dirigible/components/engine/cms/internal/CmsProviderInternalFactory.java +++ b/components/engine/engine-cms-internal/src/main/java/org/eclipse/dirigible/components/engine/cms/internal/CmsProviderInternalFactory.java @@ -33,9 +33,6 @@ @Component("cms-provider-internal") class CmsProviderInternalFactory implements CmsProviderFactory, DisposableBean { - /** The Constant DIRIGIBLE_CMS_INTERNAL_ROOT_FOLDER. */ - private static final String DIRIGIBLE_CMS_INTERNAL_ROOT_FOLDER = "DIRIGIBLE_CMS_INTERNAL_ROOT_FOLDER"; - /** The Constant PROVIDERS. */ private static final Map PROVIDERS = new HashMap(); diff --git a/components/group/group-database/pom.xml b/components/group/group-database/pom.xml index 54142d1abd1..669c42602ba 100644 --- a/components/group/group-database/pom.xml +++ b/components/group/group-database/pom.xml @@ -15,7 +15,7 @@ pom - + org.eclipse.dirigible dirigible-components-data-core @@ -65,6 +65,10 @@ org.eclipse.dirigible dirigible-database-sql-postgres + + org.eclipse.dirigible + dirigible-database-sql-mssql + org.eclipse.dirigible dirigible-database-sql-mysql diff --git a/components/ui/view-databases/src/main/resources/META-INF/dirigible/view-databases/dialogs/database-dialog.js b/components/ui/view-databases/src/main/resources/META-INF/dirigible/view-databases/dialogs/database-dialog.js index a0d4a352434..b5bce6394d6 100644 --- a/components/ui/view-databases/src/main/resources/META-INF/dirigible/view-databases/dialogs/database-dialog.js +++ b/components/ui/view-databases/src/main/resources/META-INF/dirigible/view-databases/dialogs/database-dialog.js @@ -31,6 +31,7 @@ dbdialog.controller('DBDialogController', ($scope, ViewParameters, Dialogs) => { "org.postgresql.Driver": "jdbc:postgresql://:/", "com.mysql.cj.jdbc.Driver": "jdbc:mysql://:/", "org.mariadb.jdbc.Driver": "jdbc:mariadb://:/", + "com.microsoft.sqlserver.jdbc.SQLServerDriver": "jdbc:sqlserver://:;databaseName=;encrypt=true;trustServerCertificate=true", "com.sap.db.jdbc.Driver": "jdbc:sap://:/?encrypt=true&validateCertificate=false", "net.snowflake.client.jdbc.SnowflakeDriver": "jdbc:snowflake://.snowflakecomputing.com", "org.eclipse.dirigible.mongodb.jdbc.Driver": "jdbc:mongodb://:/", @@ -41,6 +42,7 @@ dbdialog.controller('DBDialogController', ($scope, ViewParameters, Dialogs) => { 'org.postgresql.Driver': '', 'com.mysql.cj.jdbc.Driver': '', 'org.mariadb.jdbc.Driver': '', + 'com.microsoft.sqlserver.jdbc.SQLServerDriver': '', 'com.sap.db.jdbc.Driver': '', 'net.snowflake.client.jdbc.SnowflakeDriver': 'db=,schema=', 'org.eclipse.dirigible.mongodb.jdbc.Driver': '', @@ -51,6 +53,7 @@ dbdialog.controller('DBDialogController', ($scope, ViewParameters, Dialogs) => { { text: 'PostgreSQL - org.postgresql.Driver', value: 'org.postgresql.Driver' }, { text: 'MySQL - com.mysql.cj.jdbc.Driver', value: 'com.mysql.cj.jdbc.Driver' }, { text: 'MariaDB - org.mariadb.jdbc.Driver', value: 'org.mariadb.jdbc.Driver' }, + { text: 'MSSQL - com.microsoft.sqlserver.jdbc.SQLServerDriver', value: 'com.microsoft.sqlserver.jdbc.SQLServerDriver' }, { text: 'SAP HANA - com.sap.db.jdbc.Driver', value: 'com.sap.db.jdbc.Driver' }, { text: 'Snowflake - net.snowflake.client.jdbc.SnowflakeDriver', value: 'net.snowflake.client.jdbc.SnowflakeDriver' }, { text: 'MongoDB - org.eclipse.dirigible.mongodb.jdbc.Driver', value: 'org.eclipse.dirigible.mongodb.jdbc.Driver' } diff --git a/modules/database/database-sql-mssql/about.html b/modules/database/database-sql-mssql/about.html new file mode 100644 index 00000000000..bcb03d59e0f --- /dev/null +++ b/modules/database/database-sql-mssql/about.html @@ -0,0 +1,29 @@ + + + + + +About + + +

About This Content

+ +

April 25, 2020

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 2.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v20.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + diff --git a/modules/database/database-sql-mssql/pom.xml b/modules/database/database-sql-mssql/pom.xml new file mode 100644 index 00000000000..c65ec16bf20 --- /dev/null +++ b/modules/database/database-sql-mssql/pom.xml @@ -0,0 +1,42 @@ + + 4.0.0 + + + org.eclipse.dirigible + dirigible-database-parent + 13.0.0-SNAPSHOT + ../pom.xml + + + Database - SQL Builders - MSSQL + dirigible-database-sql-mssql + jar + + + ${project.artifactId} + + + + + org.eclipse.dirigible + dirigible-commons-config + + + org.eclipse.dirigible + dirigible-database-sql + + + com.microsoft.sqlserver + mssql-jdbc + 13.2.1.jre11 + + + + + ../../../licensing-header.txt + ../../../ + + + diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateBranchingBuilder.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateBranchingBuilder.java new file mode 100644 index 00000000000..0032c06c045 --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateBranchingBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.builders.CreateBranchingBuilder; + +/** + * The MSSQL Create Branching Builder. + */ +public class MSSQLCreateBranchingBuilder extends CreateBranchingBuilder { + + /** + * Instantiates a new MSSQL create branching builder. + * + * @param dialect the dialect + */ + public MSSQLCreateBranchingBuilder(ISqlDialect dialect) { + super(dialect); + } + + /** + * View. + * + * @param view the view + * @return the mssql create view builder + */ + @Override + public MSSQLCreateViewBuilder view(String view) { + return new MSSQLCreateViewBuilder(this.getDialect(), view); + } + + @Override + public MSSQLCreateUserBuilder user(String userId, String password) { + return new MSSQLCreateUserBuilder(getDialect(), userId, password); + } + +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateUserBuilder.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateUserBuilder.java new file mode 100644 index 00000000000..2ff3f9256e5 --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateUserBuilder.java @@ -0,0 +1,22 @@ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.builders.user.CreateUserBuilder; + +public class MSSQLCreateUserBuilder extends CreateUserBuilder { + + public MSSQLCreateUserBuilder(ISqlDialect dialect, String userId, String password) { + super(dialect, userId, password); + } + + @Override + protected String generateCreateUserStatement(String user, String pass) { + // create server login + return "CREATE LOGIN " + getEscapeSymbol() + user + getEscapeSymbol() + SPACE + "WITH PASSWORD =" + getPasswordEscapeSymbol() + pass + + getPasswordEscapeSymbol() + "; " + + // create user mapped to the login + + "CREATE USER " + getEscapeSymbol() + user + getEscapeSymbol() + SPACE + "FOR LOGIN " + getEscapeSymbol() + user + + getEscapeSymbol(); + } +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateViewBuilder.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateViewBuilder.java new file mode 100644 index 00000000000..eb309baecde --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLCreateViewBuilder.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.builders.view.CreateViewBuilder; + +/** + * The MSSQL Create View Builder. + */ +public class MSSQLCreateViewBuilder extends CreateViewBuilder { + + /** The values. */ + private String values = null; + + /** + * Instantiates a new MSSQL create view builder. + * + * @param dialect the dialect + * @param view the view + */ + public MSSQLCreateViewBuilder(ISqlDialect dialect, String view) { + super(dialect, view); + } + + /** + * Column. + * + * @param name the name + * @return the mssql create view builder + */ + @Override + public MSSQLCreateViewBuilder column(String name) { + super.getColumns().add(name); + return this; + } + + /** + * As select. + * + * @param select the select + * @return the mssql create view builder + */ + @Override + public MSSQLCreateViewBuilder asSelect(String select) { + + if (this.values != null) { + throw new IllegalStateException("Create VIEW can use either AS SELECT or AS VALUES, but not both."); + } + setSelect(select); + return this; + } + + /** + * As values. + * + * @param values the values + * @return the MSSQL create view builder + */ + public MSSQLCreateViewBuilder asValues(String values) { + if (getSelect() != null) { + throw new IllegalStateException("Create VIEW can use either AS SELECT or AS VALUES, but not both."); + } + this.values = values; + return this; + } + + /** + * Generate. + * + * @return the string + */ + @Override + public String generate() { + + StringBuilder sql = new StringBuilder(); + + // CREATE + generateCreate(sql); + + // VIEW + generateView(sql); + + // COLUMNS + generateColumns(sql); + + // SELECT or VALUES + if (getSelect() != null) { + generateAsSelect(sql); + } else if (this.values != null) { + generateAsValues(sql); + } else { + throw new IllegalStateException("Create VIEW must use either AS SELECT or AS VALUES."); + } + + return sql.toString(); + } + + /** + * Generate as values. + * + * @param sql the sql + */ + protected void generateAsValues(StringBuilder sql) { + sql.append(SPACE) + .append(KEYWORD_AS) + .append(SPACE); + + String[] differentValues = this.values.split(","); + for (int idx = 0; idx < differentValues.length; idx++) { + sql.append(KEYWORD_SELECT) + .append(SPACE) + .append(differentValues[idx]) + .append(SPACE); + if ((idx + 1) != differentValues.length) { + sql.append("UNION ALL") + .append(SPACE); + } + } + } + +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLNextValueSequenceBuilder.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLNextValueSequenceBuilder.java new file mode 100644 index 00000000000..ab31f8ab70b --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLNextValueSequenceBuilder.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.builders.sequence.NextValueSequenceBuilder; + +/** + * The MSSQL Next Value Sequence Builder. + */ +public class MSSQLNextValueSequenceBuilder extends NextValueSequenceBuilder { + + /** The Constant PATTERN_SELECT_NEXT_VAL_SEQUENCE. */ + private static final String PATTERN_SELECT_NEXT_VAL_SEQUENCE = "SELECT NEXT VALUE FOR %s"; + + /** + * Instantiates a new MSSQL next value sequence builder. + * + * @param dialect the dialect + * @param sequence the sequence + */ + public MSSQLNextValueSequenceBuilder(ISqlDialect dialect, String sequence) { + super(dialect, sequence); + } + + /** + * Generate. + * + * @return the string + */ + @Override + public String generate() { + String sequenceName = encapsulate(this.getSequence(), true); + + return String.format(PATTERN_SELECT_NEXT_VAL_SEQUENCE, sequenceName); + } +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSelectBuilder.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSelectBuilder.java new file mode 100644 index 00000000000..e42bf05952e --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSelectBuilder.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.builders.records.SelectBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MSSQLSelectBuilder extends SelectBuilder { + + private static final Logger LOGGER = LoggerFactory.getLogger(MSSQLSelectBuilder.class); + + public MSSQLSelectBuilder(ISqlDialect dialect) { + super(dialect); + } + + @Override + public SelectBuilder outerJoin(String table, String on, String alias) { + LOGGER.warn("In MSSQL, the term OUTER JOIN is incomplete. It will be treated as FULL OUTER JOIN"); + return genericJoin(KEYWORD_FULL_OUTER, table, on, alias); + } + + @Override + public String generate() { + StringBuilder sql = new StringBuilder(); + + // SELECT + generateSelect(sql); + + // ADD LIMIT IF MISSING OFFSET + if (this.getOffset() == -1 && this.getLimit() > -1) { + sql.append(SPACE) + .append(KEYWORD_TOP) + .append(SPACE) + .append(this.getLimit()); + } + + // DISTINCT + generateDistinct(sql); + + // COLUMNS + generateColumns(sql); + + // TABLES + generateTables(sql); + + // JOINS + generateJoins(sql); + + // WHERE + generateWhere(sql, getWheres()); + + // GROUP BY + generateGroupBy(sql); + + // HAVING + generateHaving(sql); + + // ORDER BY + generateOrderBy(sql, getOrders()); + + // LIMIT AND OFFSET + generateLimitAndOffset(sql, getLimit(), getOffset()); + + // UNION + generateUnion(sql); + + // FOR UPDATE + generateForUpdate(sql); + + return sql.toString(); + } + + @Override + protected void generateLimitAndOffset(StringBuilder sql, int limit, int offset) { + if (limit > -1 && offset > -1) { + if (getOrders().isEmpty()) { + sql.append(SPACE) + .append(KEYWORD_ORDER_BY) + .append(SPACE) + .append("(SELECT NULL)"); + } + sql.append(SPACE) + .append(KEYWORD_OFFSET) + .append(SPACE) + .append(offset) + .append(SPACE) + .append("ROWS FETCH NEXT") + .append(SPACE) + .append(limit) + .append(SPACE) + .append("ROWS ONLY"); + } + } + +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialect.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialect.java new file mode 100644 index 00000000000..a383d900b7b --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialect.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.DataType; +import org.eclipse.dirigible.database.sql.builders.AlterBranchingBuilder; +import org.eclipse.dirigible.database.sql.builders.DropBranchingBuilder; +import org.eclipse.dirigible.database.sql.builders.records.DeleteBuilder; +import org.eclipse.dirigible.database.sql.builders.records.InsertBuilder; +import org.eclipse.dirigible.database.sql.builders.records.UpdateBuilder; +import org.eclipse.dirigible.database.sql.builders.sequence.LastValueIdentityBuilder; +import org.eclipse.dirigible.database.sql.dialects.DefaultSqlDialect; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * The MSSQL SQL Dialect. + */ +public class MSSQLSqlDialect extends + DefaultSqlDialect { + + public static final String FUNCTION_CURRENT_DATE = "CAST(GETDATE() AS DATE)"; //$NON-NLS-1$ + public static final String FUNCTION_CURRENT_TIME = "CAST(GETDATE() AS TIME)"; //$NON-NLS-1$ + public static final String FUNCTION_CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP"; //$NON-NLS-1$ + + public static final Set FUNCTIONS; // functions + keywords + + static { + FUNCTIONS = Collections.synchronizedSet(new HashSet(Arrays.asList( + // --- MATH & STATS --- + "abs", "acos", "asin", "atan", "atan2", "ceiling", "cos", "cot", "degrees", "exp", "floor", "log", "log10", "pi", "power", + "radians", "rand", "round", "sign", "sin", "sqrt", "square", "tan", + + // --- STRING MANIPULATION (MSSQL Specifics) --- + "ascii", "char", "charindex", "concat", "concat_ws", "difference", "format", "left", "len", "lower", "ltrim", "nchar", + "patindex", "quotename", "replace", "replicate", "reverse", "right", "rtrim", "soundex", "space", "str", "stuff", + "substring", "translate", "trim", "upper", "unicode", + + // --- DATE & TIME --- + "dateadd", "datediff", "datepart", "datename", "day", "month", "year", "getdate", "getutcdate", "sysdatetime", + "sysdatetimeoffset", "isdate", "eomonth", "switchoffset", "todatetimeoffset", + + // --- DATA TYPE & CONVERSION --- + "cast", "convert", "parse", "try_cast", "try_convert", "try_parse", "isnumeric", "isjson", + + // --- NULL HANDLING & LOGIC --- + "coalesce", "choose", "iif", "isnull", "nullif", + + // --- AGGREGATES --- + "avg", "count", "count_big", "max", "min", "sum", "stdev", "stdevp", "var", "varp", "string_agg", "checksum_agg", + + // --- SYSTEM & METADATA --- + "newid", "newsequentialid", "rowcount", "compress", "decompress", "host_id", "host_name", "suser_sname", "user_name", + "db_name", "object_id"))); + + FUNCTIONS.addAll(RESERVED_KEYWORDS); + } + + /** + * Creates the. + * + * @return the mssql create branching builder + */ + @Override + public MSSQLCreateBranchingBuilder create() { + return new MSSQLCreateBranchingBuilder(this); + } + + /** + * Nextval. + * + * @param sequence the sequence + * @return the mssql next value sequence builder + */ + @Override + public MSSQLNextValueSequenceBuilder nextval(String sequence) { + return new MSSQLNextValueSequenceBuilder(this, sequence); + } + + /** + * Function current date. + * + * @return the string + */ + @Override + public String functionCurrentDate() { + return FUNCTION_CURRENT_DATE; + } + + @Override + public String getAutoincrementArgument() { + return "IDENTITY(1,1)"; + } + + /** + * Function current time. + * + * @return the string + */ + @Override + public String functionCurrentTime() { + return FUNCTION_CURRENT_TIME; + } + + /** + * Function current timestamp. + * + * @return the string + */ + @Override + public String functionCurrentTimestamp() { + return FUNCTION_CURRENT_TIMESTAMP; + } + + @Override + public String getDataTypeName(DataType dataType) { + return switch (dataType) { + // --- String & Large Text --- + case CHARACTER_LARGE_OBJECT, CLOB, TEXT -> "NVARCHAR(MAX)"; + case NVARCHAR -> "NVARCHAR"; + case VARCHAR -> "VARCHAR"; + + // --- Binary & Blobs --- + case BLOB, BINARY_LARGE_OBJECT, BYTEA, VARBINARY, BINARY_VARYING -> "VARBINARY(MAX)"; + + // --- Boolean --- + case BOOLEAN, BOOL -> "BIT"; + + // --- Numbers --- + case DOUBLE, DOUBLE_PRECISION -> "FLOAT"; + case REAL -> "REAL"; // 4-byte float in MSSQL + + // --- Integers --- + case INT8, BIGINT -> "BIGINT"; + case INT, INT4, INTEGER -> "INT"; + case INT2, SMALLINT -> "SMALLINT"; + case TINYINT, BYTE -> "TINYINT"; // Note: MSSQL TINYINT is 0-255 + + // --- Date & Time --- + case TIMESTAMP, DATETIME -> "DATETIME2"; // DATETIME2 is preferred over DATETIME + + default -> super.getDataTypeName(dataType); + }; + } + + /** + * Gets the functions names. + * + * @return the functions names + */ + @Override + public Set getFunctionsNames() { + return FUNCTIONS; + } + + @Override + public boolean existsSchema(Connection connection, String schema) throws SQLException { + // sys.schemas is the primary metadata view in MSSQL + String sql = "SELECT * FROM sys.schemas WHERE name = ?"; + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, schema); + try (ResultSet resultSet = statement.executeQuery()) { + return resultSet.next(); + } + } + } + + @Override + public boolean existsTable(Connection connection, String table) throws SQLException { + String sql = """ + SELECT 1 FROM sys.tables t + JOIN sys.schemas s ON t.schema_id = s.schema_id + WHERE t.name = ? AND s.name = ?"""; + + // MSSQL defaults to 'dbo' if no schema is specified in the connection + String schema = connection.getSchema(); + if (schema == null) { + schema = "dbo"; + } + + try (PreparedStatement statement = connection.prepareStatement(sql)) { + statement.setString(1, table); + statement.setString(2, schema); + try (ResultSet resultSet = statement.executeQuery()) { + return resultSet.next(); + } + } + } + + @Override + public MSSQLSelectBuilder select() { + return new MSSQLSelectBuilder(this); + } +} diff --git a/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialectProvider.java b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialectProvider.java new file mode 100644 index 00000000000..82d811c2c85 --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/java/org/eclipse/dirigible/database/sql/dialects/mssql/MSSQLSqlDialectProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.components.database.DatabaseSystem; +import org.eclipse.dirigible.database.sql.ISqlDialect; +import org.eclipse.dirigible.database.sql.ISqlDialectProvider; + +/** + * The Class MSSQLSqlDialectProvider. + */ +public class MSSQLSqlDialectProvider implements ISqlDialectProvider { + + @Override + public DatabaseSystem getDatabaseSystem() { + return DatabaseSystem.MSSQL; + } + + /** + * Gets the dialect. + * + * @return the dialect + */ + @Override + public ISqlDialect getDialect() { + return new MSSQLSqlDialect(); + } + +} diff --git a/modules/database/database-sql-mssql/src/main/resources/META-INF/services/org.eclipse.dirigible.database.sql.ISqlDialectProvider b/modules/database/database-sql-mssql/src/main/resources/META-INF/services/org.eclipse.dirigible.database.sql.ISqlDialectProvider new file mode 100644 index 00000000000..ffe828e09d6 --- /dev/null +++ b/modules/database/database-sql-mssql/src/main/resources/META-INF/services/org.eclipse.dirigible.database.sql.ISqlDialectProvider @@ -0,0 +1 @@ +org.eclipse.dirigible.database.sql.dialects.mssql.MSSQLSqlDialectProvider diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateTableTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateTableTest.java new file mode 100644 index 00000000000..388fce46c52 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateTableTest.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.DataType; +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class CreateTableTest. + */ +public class CreateTableTest { + + /** + * Creates the table generic. + */ + @Test + public void createTableGeneric() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .create() + .table("CUSTOMERS") + .column("ID", DataType.INTEGER, true, false, false) + .column("FIRST_NAME", DataType.VARCHAR, false, false, true, "(20)") + .column("LAST_NAME", DataType.VARCHAR, false, true, false, "(30)") + .build(); + + assertNotNull(sql); + assertEquals( + "CREATE TABLE \"CUSTOMERS\" ( \"ID\" INT NOT NULL PRIMARY KEY , \"FIRST_NAME\" VARCHAR (20) NOT NULL UNIQUE , \"LAST_NAME\" VARCHAR (30) )", + sql); + } + + /** + * Creates the table type safe. + */ + @Test + public void createTableTypeSafe() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .create() + .table("CUSTOMERS") + .columnInteger("ID", true, false, false) + .columnVarchar("FIRST_NAME", 20, false, true, true) + .columnVarchar("LAST_NAME", 30, false, true, false) + .build(); + + assertNotNull(sql); + assertEquals( + "CREATE TABLE \"CUSTOMERS\" ( \"ID\" INT NOT NULL PRIMARY KEY , \"FIRST_NAME\" VARCHAR (20) UNIQUE , \"LAST_NAME\" VARCHAR (30) )", + sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateViewTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateViewTest.java new file mode 100644 index 00000000000..ef09dac63cb --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/CreateViewTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class CreateViewTest. + */ +public class CreateViewTest extends CreateTableTest { + + /** + * Creates the view as select. + */ + @Test + public void createViewAsSelect() { + createTableGeneric(); + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .create() + .view("CUSTOMERS_VIEW") + .column("ID") + .column("FIRST_NAME") + .column("LAST_NAME") + .asSelect(SqlFactory.getDefault() + .select() + .column("*") + .from("CUSTOMERS") + .build()) + .build(); + + assertNotNull(sql); + assertEquals("CREATE VIEW \"CUSTOMERS_VIEW\" ( \"ID\" , \"FIRST_NAME\" , \"LAST_NAME\" ) AS SELECT * FROM \"CUSTOMERS\"", sql); + } + + /** + * Creates the view as values. + */ + @Test + public void createViewAsValues() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .create() + .view("STATES") + .column("STATE") + .asValues("'STARTED', 'STOPPED', 'FAILED', 'IN PROCESS'") + .build(); + + assertNotNull(sql); + assertEquals( + "CREATE VIEW \"STATES\" ( \"STATE\" ) AS SELECT 'STARTED' UNION ALL SELECT 'STOPPED' UNION ALL SELECT 'FAILED' UNION ALL SELECT 'IN PROCESS' ", + sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DeleteTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DeleteTest.java new file mode 100644 index 00000000000..c5af34e6956 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DeleteTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class DeleteTest. + */ +public class DeleteTest { + + /** + * Delete simple. + */ + @Test + public void deleteSimple() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .delete() + .from("CUSTOMERS") + .build(); + + assertNotNull(sql); + assertEquals("DELETE FROM \"CUSTOMERS\"", sql); + } + + /** + * Delete where. + */ + @Test + public void deleteWhere() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .delete() + .from("CUSTOMERS") + .where("AGE > ?") + .where("COMPANY = 'SAP'") + .build(); + + assertNotNull(sql); + assertEquals("DELETE FROM \"CUSTOMERS\" WHERE (\"AGE\" > ?) AND (\"COMPANY\" = 'SAP')", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropTableTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropTableTest.java new file mode 100644 index 00000000000..dfd257db25a --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropTableTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class DropTableTest. + */ +public class DropTableTest { + + /** + * Drop table. + */ + @Test + public void dropTable() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .drop() + .table("CUSTOMERS") + .build(); + + assertNotNull(sql); + assertEquals("DROP TABLE \"CUSTOMERS\"", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropViewTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropViewTest.java new file mode 100644 index 00000000000..4fba72d9917 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/DropViewTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class DropViewTest. + */ +public class DropViewTest { + + /** + * Drop view. + */ + @Test + public void dropView() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .drop() + .view("CUSTOMERS_VIEW") + .build(); + + assertNotNull(sql); + assertEquals("DROP VIEW \"CUSTOMERS_VIEW\"", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/InsertTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/InsertTest.java new file mode 100644 index 00000000000..16289b5a337 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/InsertTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class InsertTest. + */ +public class InsertTest { + + /** + * Insert simple. + */ + @Test + public void insertSimple() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .insert() + .into("CUSTOMERS") + .column("FIRST_NAME") + .column("LAST_NAME") + .build(); + + assertNotNull(sql); + assertEquals("INSERT INTO \"CUSTOMERS\" (\"FIRST_NAME\", \"LAST_NAME\") VALUES (?, ?)", sql); + } + + /** + * Insert values. + */ + @Test + public void insertValues() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .insert() + .into("CUSTOMERS") + .column("FIRST_NAME") + .column("LAST_NAME") + .value("?") + .value("'Smith'") + .build(); + + assertNotNull(sql); + assertEquals("INSERT INTO \"CUSTOMERS\" (\"FIRST_NAME\", \"LAST_NAME\") VALUES (?, 'Smith')", sql); + } + + /** + * Insert select. + */ + @Test + public void insertSelect() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .insert() + .into("CUSTOMERS") + .column("FIRST_NAME") + .column("LAST_NAME") + .select(SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("SUPPLIERS") + .build()) + .build(); + + assertNotNull(sql); + assertEquals("INSERT INTO \"CUSTOMERS\" (\"FIRST_NAME\", \"LAST_NAME\") SELECT * FROM \"SUPPLIERS\"", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SelectTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SelectTest.java new file mode 100644 index 00000000000..466a2451586 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SelectTest.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class SelectTest. + */ +public class SelectTest { + + /** + * Select star. + */ + @Test + public void selectStar() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\"", sql); + } + + /** + * Select columns from table. + */ + @Test + public void selectColumnsFromTable() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .build(); + + assertNotNull(sql); + assertEquals("SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\"", sql); + } + + /** + * Select columns from table aliases. + */ + @Test + public void selectColumnsFromTableAliases() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("c.FIRST_NAME") + .column("c.LAST_NAME") + .from("CUSTOMERS", "c") + .build(); + + assertNotNull(sql); + assertEquals("SELECT \"c\".\"FIRST_NAME\", \"c\".\"LAST_NAME\" FROM \"CUSTOMERS\" AS \"c\"", sql); + } + + /** + * Select columns from table join. + */ + @Test + public void selectColumnsFromTableJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .join("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" INNER JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select columns from table inner join. + */ + @Test + public void selectColumnsFromTableInnerJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .innerJoin("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" INNER JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select columns from table outer join. + */ + @Test + public void selectColumnsFromTableOuterJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .outerJoin("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + System.out.println(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" FULL OUTER JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select columns from table left join. + */ + @Test + public void selectColumnsFromTableLeftJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .leftJoin("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" LEFT JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select columns from table right join. + */ + @Test + public void selectColumnsFromTableRightJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .rightJoin("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" RIGHT JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select columns from table full join. + */ + @Test + public void selectColumnsFromTableFullJoin() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .fullJoin("ADDRESSES", "CUSTOMERS.ADDRESS_ID=ADDRESSES.ADDRESS_ID") + .build(); + + assertNotNull(sql); + assertEquals( + "SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" FULL JOIN \"ADDRESSES\" ON \"CUSTOMERS\".\"ADDRESS_ID\"=\"ADDRESSES\".\"ADDRESS_ID\"", + sql); + } + + /** + * Select distinct columns from table. + */ + @Test + public void selectDistinctColumnsFromTable() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .distinct() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .build(); + + assertNotNull(sql); + assertEquals("SELECT DISTINCT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\"", sql); + } + + /** + * Select columns from table order by and desc. + */ + @Test + public void selectColumnsFromTableOrderByAndDesc() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .order("FIRST_NAME") + .order("LAST_NAME", false) + .build(); + + assertNotNull(sql); + assertEquals("SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" ORDER BY \"FIRST_NAME\" ASC, \"LAST_NAME\" DESC", sql); + } + + /** + * Select columns from table group by. + */ + @Test + public void selectColumnsFromTableGroupBy() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("FIRST_NAME") + .column("LAST_NAME") + .from("CUSTOMERS") + .group("FIRST_NAME") + .build(); + + assertNotNull(sql); + assertEquals("SELECT \"FIRST_NAME\", \"LAST_NAME\" FROM \"CUSTOMERS\" GROUP BY \"FIRST_NAME\"", sql); + } + + /** + * Select where simple. + */ + @Test + public void selectWhereSimple() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .where("PRICE > ?") + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" WHERE (\"PRICE\" > ?)", sql); + } + + /** + * Select where and. + */ + @Test + public void selectWhereAnd() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .where("PRICE > ?") + .where("AMOUNT < ?") + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" WHERE (\"PRICE\" > ?) AND (\"AMOUNT\" < ?)", sql); + } + + /** + * Select where or. + */ + @Test + public void selectWhereOr() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .where("PRICE > ? OR AMOUNT < ?") + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" WHERE (\"PRICE\" > ? OR \"AMOUNT\" < ?)", sql); + } + + /** + * Select where expr. + */ + @Test + public void selectWhereExpr() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .where(SqlFactory.getNative(new MSSQLSqlDialect()) + .expression() + .and("PRICE > ?") + .or("AMOUNT < ?") + .build()) + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" WHERE (\"PRICE\" > ? OR \"AMOUNT\" < ?)", sql); + } + + /** + * Select limit. + */ + @Test + public void selectLimit() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .limit(10) + .build(); + + assertNotNull(sql); + assertEquals("SELECT TOP 10 * FROM \"CUSTOMERS\"", sql); + } + + /** + * Select limit offset. + */ + @Test + public void selectLimitOffsetAndOrderBy() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .order("CUSTOMER_ID") + .limit(10) + .offset(20) + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" ORDER BY \"CUSTOMER_ID\" ASC OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY", sql); + } + + @Test + public void selectLimitOffsetAndMissingOrderBy() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("*") + .from("CUSTOMERS") + .limit(10) + .offset(20) + .build(); + + assertNotNull(sql); + assertEquals("SELECT * FROM \"CUSTOMERS\" ORDER BY (SELECT NULL) OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY", sql); + } + + /** + * Select having. + */ + @Test + public void selectHaving() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("COUNT(FIRST_NAME)") + .column("COUNTRY") + .from("CUSTOMERS") + .group("COUNTRY") + .having("COUNT(FIRST_NAME) > 5") + .build(); + + assertNotNull(sql); + assertEquals("SELECT COUNT(\"FIRST_NAME\"), \"COUNTRY\" FROM \"CUSTOMERS\" GROUP BY \"COUNTRY\" HAVING COUNT(\"FIRST_NAME\") > 5", + sql); + } + + /** + * Select union. + */ + @Test + public void selectUnion() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("COUNTRY") + .from("CUSTOMERS") + .union(SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("COUNTRY") + .from("SUPPLIERS") + .build()) + .build(); + + assertNotNull(sql); + assertEquals("SELECT \"COUNTRY\" FROM \"CUSTOMERS\" UNION SELECT \"COUNTRY\" FROM \"SUPPLIERS\"", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SequenceTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SequenceTest.java new file mode 100644 index 00000000000..4119045dc91 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/SequenceTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class SequenceTest. + */ +public class SequenceTest { + + /** + * Creates the sequence. + */ + @Test + public void createSequence() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .create() + .sequence("CUSTOMERS_SEQUENCE") + .build(); + + assertNotNull(sql); + assertEquals("CREATE SEQUENCE \"CUSTOMERS_SEQUENCE\"", sql); + } + + /** + * Alter sequence. + */ + @Test + public void alterSequence() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .alter() + .sequence("CUSTOMERS_SEQUENCE") + .build(); + + assertNotNull(sql); + assertEquals("ALTER SEQUENCE \"CUSTOMERS_SEQUENCE\"", sql); + } + + /** + * Drop sequnce. + */ + @Test + public void dropSequnce() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .drop() + .sequence("CUSTOMERS_SEQUENCE") + .build(); + + assertNotNull(sql); + assertEquals("DROP SEQUENCE \"CUSTOMERS_SEQUENCE\"", sql); + } + + /** + * Nextval sequnce. + */ + @Test + public void nextvalSequnce() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .nextval("CUSTOMERS_SEQUENCE") + .build(); + + assertNotNull(sql); + assertEquals("SELECT NEXT VALUE FOR \"CUSTOMERS_SEQUENCE\"", sql); + } + +} diff --git a/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/UpdateTest.java b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/UpdateTest.java new file mode 100644 index 00000000000..7b0569536a4 --- /dev/null +++ b/modules/database/database-sql-mssql/src/test/java/org/eclipse/dirigible/database/sql/dialects/mssql/UpdateTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.database.sql.dialects.mssql; + +import org.eclipse.dirigible.database.sql.SqlFactory; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * The Class UpdateTest. + */ +public class UpdateTest { + + /** + * Update simple. + */ + @Test + public void updateSimple() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .update() + .table("CUSTOMERS") + .set("FIRST_NAME", "'John'") + .build(); + + assertNotNull(sql); + assertEquals("UPDATE \"CUSTOMERS\" SET \"FIRST_NAME\" = 'John'", sql); + } + + /** + * Update values. + */ + @Test + public void updateValues() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .update() + .table("CUSTOMERS") + .set("FIRST_NAME", "'John'") + .set("LAST_NAME", "'Smith'") + .build(); + + assertNotNull(sql); + assertEquals("UPDATE \"CUSTOMERS\" SET \"FIRST_NAME\" = 'John', \"LAST_NAME\" = 'Smith'", sql); + } + + /** + * Update where. + */ + @Test + public void updateWhere() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .update() + .table("CUSTOMERS") + .set("FIRST_NAME", "'John'") + .set("LAST_NAME", "'Smith'") + .where("AGE > ?") + .where("COMPANY = 'SAP'") + .build(); + + assertNotNull(sql); + assertEquals( + "UPDATE \"CUSTOMERS\" SET \"FIRST_NAME\" = 'John', \"LAST_NAME\" = 'Smith' WHERE (\"AGE\" > ?) AND (\"COMPANY\" = 'SAP')", + sql); + } + + /** + * Update where select. + */ + @Test + public void updateWhereSelect() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .update() + .table("CUSTOMERS") + .set("FIRST_NAME", "'John'") + .set("SALARY", "(" + SqlFactory.getNative(new MSSQLSqlDialect()) + .select() + .column("MAX(SALARY)") + .from("BENEFITS") + .build() + + ")") + .where("COMPANY = 'SAP'") + .build(); + + assertNotNull(sql); + assertEquals( + "UPDATE \"CUSTOMERS\" SET \"FIRST_NAME\" = 'John', \"SALARY\" = (SELECT MAX(\"SALARY\") FROM \"BENEFITS\") WHERE (\"COMPANY\" = 'SAP')", + sql); + } + + /** + * Update where expr. + */ + @Test + public void updateWhereExpr() { + String sql = SqlFactory.getNative(new MSSQLSqlDialect()) + .update() + .table("CUSTOMERS") + .set("FIRST_NAME", "'John'") + .set("LAST_NAME", "'Smith'") + .where(SqlFactory.getNative(new MSSQLSqlDialect()) + .expression() + .and("PRICE > ?") + .or("AMOUNT < ?") + .and("COMPANY = 'SAP'") + .build()) + .build(); + + assertNotNull(sql); + assertEquals( + "UPDATE \"CUSTOMERS\" SET \"FIRST_NAME\" = 'John', \"LAST_NAME\" = 'Smith' WHERE (\"PRICE\" > ? OR \"AMOUNT\" < ? AND \"COMPANY\" = 'SAP')", + sql); + } + +} diff --git a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/DataTypeUtils.java b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/DataTypeUtils.java index 2bd16ac18bd..7b13754df69 100644 --- a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/DataTypeUtils.java +++ b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/DataTypeUtils.java @@ -9,8 +9,6 @@ */ package org.eclipse.dirigible.database.sql; -import static java.text.MessageFormat.format; - import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; @@ -21,6 +19,8 @@ import java.util.Map; import java.util.Map.Entry; +import static java.text.MessageFormat.format; + /** * The Class DataTypeUtils. */ @@ -170,6 +170,8 @@ public class DataTypeUtils { STRING_TO_DATABASE_TYPE.put("TIMESTAMP WITHOUT TIME ZONE", Types.TIMESTAMP); STRING_TO_DATABASE_TYPE.put("DATETIME", Types.TIMESTAMP); + STRING_TO_DATABASE_TYPE.put("DATETIME2", Types.TIMESTAMP); // MSSQL + // ints STRING_TO_DATABASE_TYPE.put(BIT, Types.BIT); STRING_TO_DATABASE_TYPE.put(SMALLINT, Types.SMALLINT); diff --git a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/ISqlKeywords.java b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/ISqlKeywords.java index 55dec23bb31..48853955ee7 100644 --- a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/ISqlKeywords.java +++ b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/ISqlKeywords.java @@ -48,6 +48,8 @@ public interface ISqlKeywords { */ String KEYWORD_OUTER = "OUTER"; //$NON-NLS-1$ + String KEYWORD_FULL_OUTER = "FULL OUTER"; //$NON-NLS-1$ + /** * The Constant KEYWORD_LEFT. */ @@ -168,6 +170,8 @@ public interface ISqlKeywords { */ String KEYWORD_TABLE = "TABLE"; //$NON-NLS-1$ + String KEYWORD_TOP = "TOP"; + /** * The Constant KEYWORD_PRIMARY. */ diff --git a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/dialects/DefaultSqlDialect.java b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/dialects/DefaultSqlDialect.java index 2f49c2121dc..71ab29eab0b 100644 --- a/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/dialects/DefaultSqlDialect.java +++ b/modules/database/database-sql/src/main/java/org/eclipse/dirigible/database/sql/dialects/DefaultSqlDialect.java @@ -9,30 +9,7 @@ */ package org.eclipse.dirigible.database.sql.dialects; -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -import javax.sql.DataSource; - -import org.eclipse.dirigible.database.sql.DataType; -import org.eclipse.dirigible.database.sql.DatabaseArtifactTypes; -import org.eclipse.dirigible.database.sql.DatabaseType; -import org.eclipse.dirigible.database.sql.ISqlDialect; -import org.eclipse.dirigible.database.sql.ISqlKeywords; -import org.eclipse.dirigible.database.sql.SqlException; +import org.eclipse.dirigible.database.sql.*; import org.eclipse.dirigible.database.sql.builders.AlterBranchingBuilder; import org.eclipse.dirigible.database.sql.builders.CreateBranchingBuilder; import org.eclipse.dirigible.database.sql.builders.DropBranchingBuilder; @@ -46,6 +23,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.sql.DataSource; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.sql.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + /** * The Default SQL Dialect. * @@ -62,6 +51,9 @@ public class DefaultSqlDialect