From 302c173210ed0858792075db0dc1946fe426628f Mon Sep 17 00:00:00 2001 From: Stefan Bischof Date: Tue, 14 Jul 2026 20:01:55 +0200 Subject: [PATCH] feat: adopt the SQL model, dialect framework and JDBC modules Bring the SQL vocabulary (org.eclipse.daanse.sql.model), the dialect framework (org.eclipse.daanse.sql.dialect.*) and the JDBC modules (org.eclipse.daanse.sql.jdbc.*: api, record, metadata, impl, importer/csv) into this repo from org.eclipse.daanse.jdbc.db. This repo now owns the whole SQL + JDBC stack as one acyclic reactor and depends on no jdbc.db artifact. That removes the cross-repo cycle which deadlocked CI: jdbc.db needed sql.model at compile scope while the dialect tests here needed jdbc.db at test scope, so neither repo could build first and each resolved a stale snapshot of the other. Also declare the jdbc record dependency in dialect/db/derby, which previously compiled only through a transitive pull via impl. Signed-off-by: Stefan Bischof --- deparser/api/pom.xml | 2 +- .../sql/deparser/api/DialectDeparser.java | 2 +- deparser/jsqlparser/pom.xml | 4 +- .../jsqlparser/BasicDialectDeparser.java | 2 +- .../BasicDialectExpressionDeParser.java | 4 +- .../BasicDialectSelectDeParser.java | 4 +- .../BasicDialectStatementDeParser.java | 2 +- .../BaiscDialectExpressionDeParserTest.java | 2 +- .../DialectStatementDeParserTest.java | 2 +- .../jsqlparser/MockDialectHelper.java | 4 +- dialect/api/pom.xml | 36 + .../dialect/api/DaanseDialectConstants.java | 25 + .../daanse/sql/dialect/api/Dialect.java | 203 ++ .../sql/dialect/api/DialectException.java | 36 + .../sql/dialect/api/DialectFactory.java | 34 + .../sql/dialect/api/DialectInitData.java | 196 ++ .../daanse/sql/dialect/api/DialectName.java | 35 + .../sql/dialect/api/DialectVersion.java | 101 + .../dialect/api/IdentifierCaseFolding.java | 66 + .../dialect/api/IdentifierQuotingPolicy.java | 34 + .../api/capability/AggregateCapabilities.java | 71 + .../api/capability/DdlCapabilities.java | 47 + .../DialectCapabilitiesProvider.java | 235 +++ .../api/capability/JoinCapabilities.java | 45 + .../api/capability/OrderByCapabilities.java | 59 + .../WindowFunctionCapabilities.java | 55 + .../dialect/api/capability/package-info.java | 18 + .../api/generator/AggregationGenerator.java | 154 ++ .../dialect/api/generator/CastGenerator.java | 29 + .../dialect/api/generator/CteGenerator.java | 51 + .../dialect/api/generator/DdlGenerator.java | 705 +++++++ .../api/generator/FunctionGenerator.java | 160 ++ .../dialect/api/generator/HintGenerator.java | 49 + .../api/generator/IdentifierQuoter.java | 110 ++ .../dialect/api/generator/KnownFunction.java | 100 + .../dialect/api/generator/LiteralQuoter.java | 75 + .../dialect/api/generator/MergeGenerator.java | 58 + .../api/generator/OrderByGenerator.java | 58 + .../api/generator/PaginationGenerator.java | 64 + .../ParameterPlaceholderGenerator.java | 27 + .../dialect/api/generator/RegexGenerator.java | 53 + .../api/generator/ReturningGenerator.java | 23 + .../dialect/api/generator/SqlGenerator.java | 29 + .../dialect/api/generator/StatementHint.java | 36 + .../dialect/api/generator/package-info.java | 17 + .../daanse/sql/dialect/api/package-info.java | 18 + .../sql/dialect/api/type/QuoteStyle.java | 71 + .../sql/dialect/api/type/TypeMapper.java | 43 + .../sql/dialect/api/type/package-info.java | 18 + dialect/db/clickhouse/pom.xml | 50 + .../db/clickhouse/ClickHouseDialect.java | 144 ++ .../clickhouse/ClickHouseDialectFactory.java | 34 + .../dialect/db/clickhouse/package-info.java | 18 + .../ClickHouseDialectQuotingPolicyTest.java | 72 + .../db/clickhouse/ClickHouseDialectTest.java | 169 ++ .../clickhouse/integration/ServiceTest.java | 36 + dialect/db/common/pom.xml | 42 + .../db/common/AbstractDialectFactory.java | 45 + .../db/common/AbstractJdbcDialect.java | 585 ++++++ .../sql/dialect/db/common/AnsiDialect.java | 48 + .../sql/dialect/db/common/DialectUtil.java | 99 + .../db/common/JdbcCapabilityFlags.java | 130 ++ .../db/common/JdbcCapabilityRecords.java | 71 + .../sql/dialect/db/common/JdbcDdlEmitter.java | 60 + .../db/common/JdbcIdentifierQuoter.java | 146 ++ .../db/common/JdbcInlineDataGenerator.java | 201 ++ .../dialect/db/common/JdbcLiteralQuoter.java | 113 ++ .../sql/dialect/db/common/JdbcTypeMapper.java | 60 + .../sql/dialect/db/common/package-info.java | 18 + .../db/common/AbstractJdbcDialectTest.java | 119 ++ .../dialect/db/common/DialectUtilTest.java | 122 ++ .../common/IdentifierQuotingPolicyTest.java | 241 +++ .../db/common/JdbcDialectTypeMapTest.java | 50 + .../KnownFunctionDefaultSpellingTest.java | 95 + dialect/db/derby/pom.xml | 80 + .../sql/dialect/db/derby/DerbyDialect.java | 148 ++ .../dialect/db/derby/DerbyDialectFactory.java | 34 + .../sql/dialect/db/derby/package-info.java | 18 + .../derby/DerbyDialectQuotingPolicyTest.java | 72 + .../dialect/db/derby/DerbyDialectTest.java | 75 + .../db/derby/integration/ServiceTest.java | 36 + .../derby/sqlgen/DerbyDdlRoundTripTest.java | 239 +++ dialect/db/duckdb/pom.xml | 50 + .../sql/dialect/db/duckdb/DuckDbDialect.java | 206 +++ .../db/duckdb/DuckDbDialectFactory.java | 37 + .../sql/dialect/db/duckdb/package-info.java | 16 + .../duckdb/DuckDbDialectCapabilitiesTest.java | 24 + dialect/db/h2/pom.xml | 55 + .../daanse/sql/dialect/db/h2/H2Dialect.java | 151 ++ .../sql/dialect/db/h2/H2DialectFactory.java | 34 + .../sql/dialect/db/h2/package-info.java | 17 + .../db/h2/H2DialectQuotingPolicyTest.java | 72 + .../sql/dialect/db/h2/H2DialectTest.java | 272 +++ .../db/h2/integration/ServiceTest.java | 36 + dialect/db/mariadb/pom.xml | 112 ++ .../dialect/db/mariadb/MariaDBDialect.java | 121 ++ .../db/mariadb/MariaDBDialectFactory.java | 34 + .../sql/dialect/db/mariadb/package-info.java | 18 + .../MariaDBGeneratorsRoundTripTest.java | 98 + .../MariaDBInitMetaInfoLatencyTest.java | 183 ++ .../db/mariadb/MariaDBKnownFunctionTest.java | 33 + .../db/mariadb/MariaDBQuotingPolicyTest.java | 59 + .../MariaDBVersionCapabilitiesTest.java | 55 + .../db/mariadb/integration/ServiceTest.java | 37 + .../sqlgen/MariaDBDdlRoundTripTest.java | 262 +++ .../sqlgen/MariaDbAlterRenameOfflineTest.java | 57 + dialect/db/mssqlserver/pom.xml | 111 ++ .../MicrosoftSqlServerDialect.java | 649 +++++++ .../MicrosoftSqlServerDialectFactory.java | 34 + .../dialect/db/mssqlserver/package-info.java | 18 + .../MicrosoftSqlServerDialectTest.java | 97 + ...osoftSqlServerGeneratorsRoundTripTest.java | 99 + .../MicrosoftSqlServerGeneratorsTest.java | 136 ++ ...osoftSqlServerInitMetaInfoLatencyTest.java | 193 ++ .../MicrosoftSqlServerQuotingPolicyTest.java | 83 + .../mssqlserver/integration/ServiceTest.java | 36 + .../sqlgen/MsSqlServerDdlRoundTripTest.java | 260 +++ .../sqlgen/MssqlAlterRenameOfflineTest.java | 120 ++ dialect/db/mysql/pom.xml | 100 + .../sql/dialect/db/mysql/MySqlDialect.java | 614 +++++++ .../dialect/db/mysql/MySqlDialectFactory.java | 44 + .../sql/dialect/db/mysql/package-info.java | 17 + .../dialect/db/mysql/MySqlDialect3Test.java | 121 ++ .../mysql/MySqlGeneratorsRoundTripTest.java | 104 ++ .../dialect/db/mysql/MySqlGeneratorsTest.java | 120 ++ .../mysql/MySqlInitMetaInfoLatencyTest.java | 184 ++ .../db/mysql/MySqlQuotingPolicyTest.java | 81 + .../db/mysql/integration/OSGiServiceTest.java | 30 + .../sqlgen/MySqlAlterRenameOfflineTest.java | 82 + .../mysql/sqlgen/MySqlDdlRoundTripTest.java | 259 +++ dialect/db/oracle/pom.xml | 121 ++ .../sql/dialect/db/oracle/OracleDialect.java | 622 +++++++ .../db/oracle/OracleDialectFactory.java | 34 + .../sql/dialect/db/oracle/package-info.java | 18 + .../sql/dialect/db/oracle/AdditionalTest.java | 114 ++ .../dialect/db/oracle/OracleDialectTest.java | 71 + .../oracle/OracleGeneratorsRoundTripTest.java | 105 ++ .../db/oracle/OracleGeneratorsTest.java | 96 + .../oracle/OracleInitMetaInfoLatencyTest.java | 188 ++ .../db/oracle/OracleQuotingPolicyTest.java | 91 + .../db/oracle/integration/ServiceTest.java | 36 + .../sqlgen/OracleAlterRenameOfflineTest.java | 79 + .../oracle/sqlgen/OracleDdlRoundTripTest.java | 252 +++ dialect/db/pom.xml | 41 + dialect/db/postgresql/pom.xml | 115 ++ .../db/postgresql/PostgreSqlDialect.java | 381 ++++ .../postgresql/PostgreSqlDialectFactory.java | 34 + .../dialect/db/postgresql/package-info.java | 17 + .../db/postgresql/PostgreSqlDialectTest.java | 75 + .../PostgreSqlGeneratorsRoundTripTest.java | 99 + .../postgresql/PostgreSqlGeneratorsTest.java | 65 + .../PostgreSqlInitMetaInfoLatencyTest.java | 185 ++ .../PostgreSqlQuotingPolicyTest.java | 74 + .../postgresql/integration/ServiceTest.java | 36 + .../PostgreSqlAlterRenameOfflineTest.java | 87 + .../sqlgen/PostgreSqlDdlRoundTripTest.java | 263 +++ dialect/db/sqlite/pom.xml | 64 + .../sql/dialect/db/sqlite/SqliteDialect.java | 244 +++ .../db/sqlite/SqliteDialectFactory.java | 35 + .../sql/dialect/db/sqlite/package-info.java | 18 + .../SqliteDialectQuotingPolicyTest.java | 72 + .../dialect/db/sqlite/SqliteDialectTest.java | 92 + .../sqlite/SqliteGeneratorsRoundTripTest.java | 87 + .../db/sqlite/integration/ServiceTest.java | 36 + dialect/db/test-support/pom.xml | 46 + .../db/testsupport/GeneratorTestSupport.java | 62 + .../db/testsupport/RoundTripAssertions.java | 102 ++ .../dialect/db/testsupport/package-info.java | 12 + dialect/pom.xml | 65 + .../01-inventory-and-scope.md | 110 ++ .../02-dependency-and-layering.md | 119 ++ .../03-package-rename-map.md | 96 + .../04-migration-procedure.md | 113 ++ .../dialect-migration/05-consumer-rewiring.md | 102 ++ docs/dialect-migration/06-build-and-java21.md | 118 ++ .../07-anomalies-and-risks.md | 90 + docs/dialect-migration/08-verification.md | 87 + .../09-minimal-split-analysis.md | 214 +++ docs/dialect-migration/README.md | 144 ++ .../minimal-split/01-role-separation.md | 132 ++ .../minimal-split/02-target-structure.md | 116 ++ .../minimal-split/03-migration-procedure.md | 96 + .../minimal-split/04-consumer-rewiring.md | 96 + .../minimal-split/05-per-version-hardening.md | 83 + .../06-build-and-verification.md | 108 ++ .../minimal-split/IMPLEMENTATION-GUIDE.md | 712 ++++++++ .../dialect-migration/minimal-split/README.md | 140 ++ .../ru/01-inventory-and-scope.md | 110 ++ .../ru/02-dependency-and-layering.md | 118 ++ .../ru/03-package-rename-map.md | 94 + .../ru/04-migration-procedure.md | 112 ++ .../ru/05-consumer-rewiring.md | 102 ++ .../ru/06-build-and-java21.md | 117 ++ .../ru/07-anomalies-and-risks.md | 89 + docs/dialect-migration/ru/08-verification.md | 87 + .../ru/09-minimal-split-analysis.md | 215 +++ docs/dialect-migration/ru/README.md | 143 ++ .../ru/minimal-split/01-role-separation.md | 129 ++ .../ru/minimal-split/02-target-structure.md | 115 ++ .../minimal-split/03-migration-procedure.md | 100 + .../ru/minimal-split/04-consumer-rewiring.md | 97 + .../minimal-split/05-per-version-hardening.md | 82 + .../06-build-and-verification.md | 109 ++ .../ru/minimal-split/IMPLEMENTATION-GUIDE.md | 712 ++++++++ .../ru/minimal-split/README.md | 143 ++ .../daanse/sql/guard/api/SqlGuardFactory.java | 2 +- guard/jsqltranspiler/pom.xml | 4 +- .../jsqltranspiler/DeparserColumResolver.java | 2 +- .../jsqltranspiler/TranspilerSqlGuard.java | 2 +- .../TranspilerSqlGuardFactory.java | 2 +- .../integration/SqlGuardTest.java | 4 +- guard/jsqltranspiler/test.bndrun | 9 +- guard/pom.xml | 2 +- jdbc/api/pom.xml | 36 + .../daanse/sql/jdbc/api/DatabaseService.java | 22 + .../daanse/sql/jdbc/api/MetaDataQueries.java | 479 +++++ .../daanse/sql/jdbc/api/MetadataProvider.java | 394 ++++ .../sql/jdbc/api/MetadataProviderFactory.java | 33 + .../daanse/sql/jdbc/api/SnapshotBuilder.java | 63 + .../sql/jdbc/api/meta/DatabaseInfo.java | 28 + .../sql/jdbc/api/meta/IdentifierInfo.java | 30 + .../daanse/sql/jdbc/api/meta/IndexInfo.java | 25 + .../sql/jdbc/api/meta/IndexInfoItem.java | 66 + .../daanse/sql/jdbc/api/meta/MetaInfo.java | 29 + .../sql/jdbc/api/meta/StructureInfo.java | 98 + .../daanse/sql/jdbc/api/meta/TypeInfo.java | 86 + .../sql/jdbc/api/meta/package-info.java | 17 + .../daanse/sql/jdbc/api/package-info.java | 17 + .../jdbc/api/schema/BestRowIdentifier.java | 95 + .../sql/jdbc/api/schema/CheckConstraint.java | 25 + .../sql/jdbc/api/schema/ColumnPrivilege.java | 36 + .../sql/jdbc/api/schema/Constraint.java | 17 + .../sql/jdbc/api/schema/DropImportedKey.java | 23 + .../daanse/sql/jdbc/api/schema/Function.java | 70 + .../sql/jdbc/api/schema/FunctionColumn.java | 95 + .../jdbc/api/schema/FunctionReference.java | 26 + .../sql/jdbc/api/schema/ImportedKey.java | 91 + .../sql/jdbc/api/schema/MaterializedView.java | 42 + .../daanse/sql/jdbc/api/schema/Partition.java | 46 + .../sql/jdbc/api/schema/PartitionMethod.java | 51 + .../daanse/sql/jdbc/api/schema/Procedure.java | 70 + .../sql/jdbc/api/schema/ProcedureColumn.java | 95 + .../jdbc/api/schema/ProcedureReference.java | 26 + .../sql/jdbc/api/schema/PseudoColumn.java | 50 + .../sql/jdbc/api/schema/SchemaObject.java | 18 + .../daanse/sql/jdbc/api/schema/Sequence.java | 48 + .../jdbc/api/schema/SequenceReference.java | 27 + .../sql/jdbc/api/schema/SuperTable.java | 25 + .../daanse/sql/jdbc/api/schema/SuperType.java | 33 + .../sql/jdbc/api/schema/TableDefinition.java | 28 + .../sql/jdbc/api/schema/TableMetaData.java | 32 + .../sql/jdbc/api/schema/TablePrivilege.java | 35 + .../sql/jdbc/api/schema/UniqueConstraint.java | 29 + .../sql/jdbc/api/schema/UserDefinedType.java | 42 + .../api/schema/UserDefinedTypeReference.java | 27 + .../sql/jdbc/api/schema/VersionColumn.java | 67 + .../sql/jdbc/api/schema/ViewDefinition.java | 30 + .../sql/jdbc/api/schema/package-info.java | 17 + jdbc/impl/pom.xml | 99 + .../sql/jdbc/impl/CachingDatabaseService.java | 75 + .../sql/jdbc/impl/DatabaseServiceImpl.java | 1625 +++++++++++++++++ .../daanse/sql/jdbc/impl/package-info.java | 17 + .../jdbc/impl/CachingDatabaseServiceTest.java | 118 ++ .../sql/jdbc/impl/CoreTestAuditTrigger.java | 43 + .../jdbc/impl/DatabaseServiceImplH2Test.java | 277 +++ .../impl/DatabaseServiceImplMocksTest.java | 103 ++ .../DatabaseServiceImplProviderH2Test.java | 240 +++ .../DatabaseServiceJdbcWrappersH2Test.java | 145 ++ ...DatabaseServiceProviderFallbackH2Test.java | 253 +++ .../jdbc/impl/DialectInitDataLatencyTest.java | 56 + .../impl/DialectInitMetaInfoLatencyTest.java | 130 ++ .../jdbc/impl/integration/ServiceTest.java | 35 + .../jdbc/impl/sqlgen/DialectDdlH2Test.java | 159 ++ .../impl/sqlgen/DialectDdlOfflineTest.java | 398 ++++ .../sqlgen/FocusedInterfaceDefaultsTest.java | 238 +++ .../jdbc/impl/sqlgen/H2DdlRoundTripTest.java | 310 ++++ .../sqlgen/QuotingPolicyAcrossDdlTest.java | 195 ++ jdbc/impl/src/test/resources/logback-test.xml | 19 + jdbc/impl/test.bndrun | 84 + jdbc/importer/csv/pom.xml | 89 + .../sql/jdbc/importer/csv/api/Constants.java | 32 + .../jdbc/importer/csv/api/package-info.java | 17 + .../importer/csv/impl/CsvDataImporter.java | 468 +++++ .../csv/impl/CsvDataImporterConfig.java | 80 + .../csv/impl/CsvDataImporterException.java | 27 + .../impl/integration/CsvDataLoaderTest.java | 184 ++ .../src/test/resources/csv/schema1/test1.csv | 4 + .../csv/src/test/resources/csv/test.csv | 4 + .../csv/src/test/resources/logback-test.xml | 19 + jdbc/importer/pom.xml | 31 + jdbc/metadata/pom.xml | 102 ++ .../sql/jdbc/metadata/H2MetadataProvider.java | 904 +++++++++ .../metadata/H2MetadataProviderFactory.java | 40 + .../metadata/MariaDbMetadataProvider.java | 935 ++++++++++ .../MariaDbMetadataProviderFactory.java | 40 + .../sql/jdbc/metadata/MetadataProviders.java | 54 + .../MicrosoftSqlServerMetadataProvider.java | 1090 +++++++++++ ...osoftSqlServerMetadataProviderFactory.java | 40 + .../jdbc/metadata/MySqlMetadataProvider.java | 886 +++++++++ .../MySqlMetadataProviderFactory.java | 40 + .../jdbc/metadata/OracleMetadataProvider.java | 1501 +++++++++++++++ .../OracleMetadataProviderFactory.java | 40 + .../metadata/PostgreSqlMetadataProvider.java | 1014 ++++++++++ .../PostgreSqlMetadataProviderFactory.java | 40 + .../sql/jdbc/metadata/package-info.java | 23 + .../jdbc/metadata/H2MetadataProviderTest.java | 649 +++++++ .../metadata/MariaDBMetadataProviderTest.java | 712 ++++++++ .../jdbc/metadata/MsSqlPartitionsTest.java | 101 + .../sql/jdbc/metadata/PgPartitionsTest.java | 135 ++ .../sql/jdbc/metadata/TestAuditTrigger.java | 43 + jdbc/pom.xml | 68 + jdbc/record/pom.xml | 40 + .../jdbc/record/meta/DatabaseInfoRecord.java | 24 + .../record/meta/IdentifierInfoRecord.java | 23 + .../sql/jdbc/record/meta/MetaInfoRecord.java | 28 + .../jdbc/record/meta/StructureInfoRecord.java | 53 + .../sql/jdbc/record/meta/TypeInfoRecord.java | 26 + .../sql/jdbc/record/meta/package-info.java | 17 + .../schema/BestRowIdentifierRecord.java | 30 + .../record/schema/CheckConstraintRecord.java | 24 + .../record/schema/ColumnDefinitionRecord.java | 22 + .../record/schema/ColumnMetaDataRecord.java | 35 + .../record/schema/ColumnPrivilegeRecord.java | 27 + .../record/schema/DropImportedKeyRecord.java | 21 + .../record/schema/FunctionColumnRecord.java | 35 + .../jdbc/record/schema/FunctionRecord.java | 32 + .../jdbc/record/schema/ImportedKeyRecord.java | 31 + .../record/schema/IndexInfoItemRecord.java | 32 + .../jdbc/record/schema/IndexInfoRecord.java | 24 + .../record/schema/MaterializedViewRecord.java | 28 + .../jdbc/record/schema/PartitionRecord.java | 33 + .../jdbc/record/schema/PrimaryKeyRecord.java | 28 + .../record/schema/ProcedureColumnRecord.java | 35 + .../jdbc/record/schema/ProcedureRecord.java | 32 + .../record/schema/PseudoColumnRecord.java | 33 + .../jdbc/record/schema/SequenceRecord.java | 31 + .../jdbc/record/schema/SuperTableRecord.java | 22 + .../jdbc/record/schema/SuperTypeRecord.java | 26 + .../record/schema/TableDefinitionRecord.java | 27 + .../record/schema/TableMetaDataRecord.java | 30 + .../record/schema/TablePrivilegeRecord.java | 27 + .../sql/jdbc/record/schema/TriggerRecord.java | 31 + .../record/schema/UniqueConstraintRecord.java | 27 + .../record/schema/UserDefinedTypeRecord.java | 28 + .../record/schema/VersionColumnRecord.java | 29 + .../record/schema/ViewDefinitionRecord.java | 26 + .../sql/jdbc/record/schema/package-info.java | 17 + model/pom.xml | 50 + .../sql/model/schema/CatalogReference.java | 17 + .../sql/model/schema/ColumnDefinition.java | 25 + .../sql/model/schema/ColumnMetaData.java | 113 ++ .../sql/model/schema/ColumnReference.java | 24 + .../daanse/sql/model/schema/Named.java | 19 + .../daanse/sql/model/schema/PrimaryKey.java | 28 + .../sql/model/schema/SchemaReference.java | 24 + .../sql/model/schema/TableReference.java | 42 + .../daanse/sql/model/schema/Trigger.java | 80 + .../sql/model/schema/TriggerReference.java | 17 + .../daanse/sql/model/schema/package-info.java | 12 + .../daanse/sql/model/sql/BitOperation.java | 38 + .../daanse/sql/model/sql/NullsOrder.java | 24 + .../daanse/sql/model/sql/OrderedColumn.java | 69 + .../daanse/sql/model/sql/SortDirection.java | 24 + .../daanse/sql/model/sql/package-info.java | 17 + .../sql/model/type/BestFitColumnType.java | 39 + .../daanse/sql/model/type/Datatype.java | 214 +++ .../daanse/sql/model/type/package-info.java | 18 + pom.xml | 3 + statement/api/pom.xml | 2 +- .../statement/api/DeleteStatementBuilder.java | 2 +- .../daanse/sql/statement/api/Expressions.java | 16 +- .../daanse/sql/statement/api/From.java | 4 +- .../statement/api/InsertStatementBuilder.java | 2 +- .../statement/api/SelectStatementBuilder.java | 4 +- .../statement/api/UpdateStatementBuilder.java | 2 +- .../api/expression/SqlExpression.java | 10 +- .../statement/api/model/DeleteStatement.java | 4 +- .../sql/statement/api/model/FromClause.java | 4 +- .../statement/api/model/InsertStatement.java | 4 +- .../sql/statement/api/model/Projection.java | 2 +- .../statement/api/model/SelectStatement.java | 2 +- .../sql/statement/api/model/SortSpec.java | 6 +- .../statement/api/model/UpdateStatement.java | 4 +- .../statement/api/render/BoundParameter.java | 2 +- .../sql/statement/api/render/RenderedSql.java | 2 +- statement/demo/pom.xml | 6 +- .../sql/statement/demo/ResultReaderDemo.java | 8 +- .../statement/demo/StatementBuilderDemo.java | 6 +- .../sql/statement/demo/AggregateTest.java | 6 +- .../statement/demo/CollationOrderByTest.java | 4 +- .../statement/demo/ConstantPredicateTest.java | 6 +- .../daanse/sql/statement/demo/CteTest.java | 6 +- .../demo/DataSourceExecutorTest.java | 6 +- .../daanse/sql/statement/demo/DmlTest.java | 6 +- .../statement/demo/ExistsPredicateTest.java | 6 +- .../statement/demo/ExtraAggregateTest.java | 8 +- .../sql/statement/demo/FooterCommentTest.java | 6 +- .../sql/statement/demo/FromInlineTest.java | 8 +- .../sql/statement/demo/FromSetTest.java | 8 +- .../sql/statement/demo/FromVariantTest.java | 8 +- .../demo/InlineCommentHardeningTest.java | 6 +- .../sql/statement/demo/KnownCallTest.java | 8 +- .../statement/demo/OrdinalOrderByTest.java | 4 +- .../demo/OuterJoinRenderingTest.java | 6 +- .../statement/demo/ParameterAndBatchTest.java | 6 +- .../sql/statement/demo/RecursiveCteTest.java | 8 +- .../statement/demo/RendererHardeningTest.java | 12 +- .../statement/demo/ScalarSubqueryTest.java | 6 +- .../demo/SqlCommentRenderingTest.java | 6 +- .../statement/demo/StarProjectionTest.java | 4 +- .../demo/StatementBuilderDemoTest.java | 6 +- .../sql/statement/demo/StatementHintTest.java | 10 +- .../sql/statement/demo/TableRefTest.java | 14 +- statement/impl/pom.xml | 6 +- .../sql/statement/exec/ColumnAccessors.java | 2 +- .../daanse/sql/statement/exec/JdbcRow.java | 2 +- .../statement/render/DialectSqlRenderer.java | 14 +- .../DistinctCountSubqueryRewriteTest.java | 6 +- .../statement/render/FromRawAliasingTest.java | 2 +- .../render/InTupleRenderingTest.java | 10 +- 420 files changed, 40811 insertions(+), 182 deletions(-) create mode 100644 dialect/api/pom.xml create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DaanseDialectConstants.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/Dialect.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectException.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectFactory.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectInitData.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectName.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectVersion.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierCaseFolding.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierQuotingPolicy.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/AggregateCapabilities.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/JoinCapabilities.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/OrderByCapabilities.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/WindowFunctionCapabilities.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/package-info.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CastGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CteGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/HintGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/IdentifierQuoter.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/KnownFunction.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/LiteralQuoter.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/MergeGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/OrderByGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ParameterPlaceholderGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/RegexGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ReturningGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/SqlGenerator.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/StatementHint.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/package-info.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/package-info.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/QuoteStyle.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/TypeMapper.java create mode 100644 dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/package-info.java create mode 100644 dialect/db/clickhouse/pom.xml create mode 100644 dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java create mode 100644 dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectFactory.java create mode 100644 dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/package-info.java create mode 100644 dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectQuotingPolicyTest.java create mode 100644 dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java create mode 100644 dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/integration/ServiceTest.java create mode 100644 dialect/db/common/pom.xml create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractDialectFactory.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AnsiDialect.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtil.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityRecords.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDdlEmitter.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcIdentifierQuoter.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcInlineDataGenerator.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcLiteralQuoter.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcTypeMapper.java create mode 100644 dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/package-info.java create mode 100644 dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialectTest.java create mode 100644 dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtilTest.java create mode 100644 dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/IdentifierQuotingPolicyTest.java create mode 100644 dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDialectTypeMapTest.java create mode 100644 dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/KnownFunctionDefaultSpellingTest.java create mode 100644 dialect/db/derby/pom.xml create mode 100644 dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java create mode 100644 dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectFactory.java create mode 100644 dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/package-info.java create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectQuotingPolicyTest.java create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/integration/ServiceTest.java create mode 100644 dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyDdlRoundTripTest.java create mode 100644 dialect/db/duckdb/pom.xml create mode 100644 dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java create mode 100644 dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectFactory.java create mode 100644 dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/package-info.java create mode 100644 dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectCapabilitiesTest.java create mode 100644 dialect/db/h2/pom.xml create mode 100644 dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java create mode 100644 dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectFactory.java create mode 100644 dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/package-info.java create mode 100644 dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectQuotingPolicyTest.java create mode 100644 dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java create mode 100644 dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/integration/ServiceTest.java create mode 100644 dialect/db/mariadb/pom.xml create mode 100644 dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java create mode 100644 dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialectFactory.java create mode 100644 dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/package-info.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBInitMetaInfoLatencyTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBKnownFunctionTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBQuotingPolicyTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBVersionCapabilitiesTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/integration/ServiceTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDBDdlRoundTripTest.java create mode 100644 dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java create mode 100644 dialect/db/mssqlserver/pom.xml create mode 100644 dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java create mode 100644 dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectFactory.java create mode 100644 dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/package-info.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerInitMetaInfoLatencyTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/integration/ServiceTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MsSqlServerDdlRoundTripTest.java create mode 100644 dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlAlterRenameOfflineTest.java create mode 100644 dialect/db/mysql/pom.xml create mode 100644 dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java create mode 100644 dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialectFactory.java create mode 100644 dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/package-info.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlInitMetaInfoLatencyTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlQuotingPolicyTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/integration/OSGiServiceTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java create mode 100644 dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlDdlRoundTripTest.java create mode 100644 dialect/db/oracle/pom.xml create mode 100644 dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java create mode 100644 dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectFactory.java create mode 100644 dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/package-info.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/AdditionalTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleInitMetaInfoLatencyTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleQuotingPolicyTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/integration/ServiceTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java create mode 100644 dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleDdlRoundTripTest.java create mode 100644 dialect/db/pom.xml create mode 100644 dialect/db/postgresql/pom.xml create mode 100644 dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java create mode 100644 dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectFactory.java create mode 100644 dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/package-info.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlInitMetaInfoLatencyTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlQuotingPolicyTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/integration/ServiceTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java create mode 100644 dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlDdlRoundTripTest.java create mode 100644 dialect/db/sqlite/pom.xml create mode 100644 dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java create mode 100644 dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectFactory.java create mode 100644 dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/package-info.java create mode 100644 dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectQuotingPolicyTest.java create mode 100644 dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java create mode 100644 dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java create mode 100644 dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/integration/ServiceTest.java create mode 100644 dialect/db/test-support/pom.xml create mode 100644 dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java create mode 100644 dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java create mode 100644 dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/package-info.java create mode 100644 dialect/pom.xml create mode 100644 docs/dialect-migration/01-inventory-and-scope.md create mode 100644 docs/dialect-migration/02-dependency-and-layering.md create mode 100644 docs/dialect-migration/03-package-rename-map.md create mode 100644 docs/dialect-migration/04-migration-procedure.md create mode 100644 docs/dialect-migration/05-consumer-rewiring.md create mode 100644 docs/dialect-migration/06-build-and-java21.md create mode 100644 docs/dialect-migration/07-anomalies-and-risks.md create mode 100644 docs/dialect-migration/08-verification.md create mode 100644 docs/dialect-migration/09-minimal-split-analysis.md create mode 100644 docs/dialect-migration/README.md create mode 100644 docs/dialect-migration/minimal-split/01-role-separation.md create mode 100644 docs/dialect-migration/minimal-split/02-target-structure.md create mode 100644 docs/dialect-migration/minimal-split/03-migration-procedure.md create mode 100644 docs/dialect-migration/minimal-split/04-consumer-rewiring.md create mode 100644 docs/dialect-migration/minimal-split/05-per-version-hardening.md create mode 100644 docs/dialect-migration/minimal-split/06-build-and-verification.md create mode 100644 docs/dialect-migration/minimal-split/IMPLEMENTATION-GUIDE.md create mode 100644 docs/dialect-migration/minimal-split/README.md create mode 100644 docs/dialect-migration/ru/01-inventory-and-scope.md create mode 100644 docs/dialect-migration/ru/02-dependency-and-layering.md create mode 100644 docs/dialect-migration/ru/03-package-rename-map.md create mode 100644 docs/dialect-migration/ru/04-migration-procedure.md create mode 100644 docs/dialect-migration/ru/05-consumer-rewiring.md create mode 100644 docs/dialect-migration/ru/06-build-and-java21.md create mode 100644 docs/dialect-migration/ru/07-anomalies-and-risks.md create mode 100644 docs/dialect-migration/ru/08-verification.md create mode 100644 docs/dialect-migration/ru/09-minimal-split-analysis.md create mode 100644 docs/dialect-migration/ru/README.md create mode 100644 docs/dialect-migration/ru/minimal-split/01-role-separation.md create mode 100644 docs/dialect-migration/ru/minimal-split/02-target-structure.md create mode 100644 docs/dialect-migration/ru/minimal-split/03-migration-procedure.md create mode 100644 docs/dialect-migration/ru/minimal-split/04-consumer-rewiring.md create mode 100644 docs/dialect-migration/ru/minimal-split/05-per-version-hardening.md create mode 100644 docs/dialect-migration/ru/minimal-split/06-build-and-verification.md create mode 100644 docs/dialect-migration/ru/minimal-split/IMPLEMENTATION-GUIDE.md create mode 100644 docs/dialect-migration/ru/minimal-split/README.md create mode 100644 jdbc/api/pom.xml create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/DatabaseService.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetaDataQueries.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProviderFactory.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/SnapshotBuilder.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/DatabaseInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IdentifierInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfoItem.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/MetaInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/TypeInfo.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/package-info.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/package-info.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/BestRowIdentifier.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/CheckConstraint.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ColumnPrivilege.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Constraint.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/DropImportedKey.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Function.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionColumn.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionReference.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ImportedKey.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/MaterializedView.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Partition.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PartitionMethod.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Procedure.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureColumn.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureReference.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PseudoColumn.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Sequence.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SequenceReference.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperTable.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperType.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableDefinition.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableMetaData.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TablePrivilege.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UniqueConstraint.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedType.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedTypeReference.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/VersionColumn.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ViewDefinition.java create mode 100644 jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/package-info.java create mode 100644 jdbc/impl/pom.xml create mode 100644 jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseService.java create mode 100644 jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java create mode 100644 jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/package-info.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseServiceTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CoreTestAuditTrigger.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplH2Test.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplMocksTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplProviderH2Test.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceJdbcWrappersH2Test.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceProviderFallbackH2Test.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitDataLatencyTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitMetaInfoLatencyTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/integration/ServiceTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlH2Test.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/FocusedInterfaceDefaultsTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/H2DdlRoundTripTest.java create mode 100644 jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/QuotingPolicyAcrossDdlTest.java create mode 100644 jdbc/impl/src/test/resources/logback-test.xml create mode 100644 jdbc/impl/test.bndrun create mode 100644 jdbc/importer/csv/pom.xml create mode 100644 jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/Constants.java create mode 100644 jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/package-info.java create mode 100644 jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java create mode 100644 jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterConfig.java create mode 100644 jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterException.java create mode 100644 jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/integration/CsvDataLoaderTest.java create mode 100644 jdbc/importer/csv/src/test/resources/csv/schema1/test1.csv create mode 100644 jdbc/importer/csv/src/test/resources/csv/test.csv create mode 100644 jdbc/importer/csv/src/test/resources/logback-test.xml create mode 100644 jdbc/importer/pom.xml create mode 100644 jdbc/metadata/pom.xml create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MetadataProviders.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProviderFactory.java create mode 100644 jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/package-info.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDBMetadataProviderTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlPartitionsTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/PgPartitionsTest.java create mode 100644 jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/TestAuditTrigger.java create mode 100644 jdbc/pom.xml create mode 100644 jdbc/record/pom.xml create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/DatabaseInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/IdentifierInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/MetaInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/TypeInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/package-info.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/BestRowIdentifierRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/CheckConstraintRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnDefinitionRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnMetaDataRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnPrivilegeRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/DropImportedKeyRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionColumnRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ImportedKeyRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoItemRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/MaterializedViewRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PartitionRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PrimaryKeyRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureColumnRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PseudoColumnRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SequenceRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTableRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTypeRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableDefinitionRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableMetaDataRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TablePrivilegeRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UniqueConstraintRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UserDefinedTypeRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/VersionColumnRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ViewDefinitionRecord.java create mode 100644 jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/package-info.java create mode 100644 model/pom.xml create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/CatalogReference.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnDefinition.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnMetaData.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnReference.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/Named.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/PrimaryKey.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/SchemaReference.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/TableReference.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/TriggerReference.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/schema/package-info.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/sql/BitOperation.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/sql/NullsOrder.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/sql/OrderedColumn.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/sql/SortDirection.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/sql/package-info.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/type/BestFitColumnType.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/type/Datatype.java create mode 100644 model/src/main/java/org/eclipse/daanse/sql/model/type/package-info.java diff --git a/deparser/api/pom.xml b/deparser/api/pom.xml index 7c1e185..78822f5 100644 --- a/deparser/api/pom.xml +++ b/deparser/api/pom.xml @@ -27,7 +27,7 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.api + org.eclipse.daanse.sql.dialect.api 0.0.1-SNAPSHOT diff --git a/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java index 9dcea0e..4d66d15 100644 --- a/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java +++ b/deparser/api/src/main/java/org/eclipse/daanse/sql/deparser/api/DialectDeparser.java @@ -13,7 +13,7 @@ */ package org.eclipse.daanse.sql.deparser.api; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; /** * Dialect-aware SQL deparsers. diff --git a/deparser/jsqlparser/pom.xml b/deparser/jsqlparser/pom.xml index aef19a3..a2d7536 100644 --- a/deparser/jsqlparser/pom.xml +++ b/deparser/jsqlparser/pom.xml @@ -32,12 +32,12 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.api + org.eclipse.daanse.sql.dialect.api 0.0.1-SNAPSHOT org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.common + org.eclipse.daanse.sql.dialect.db.common 0.0.1-SNAPSHOT diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java index 77cd152..db8b6b4 100644 --- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectDeparser.java @@ -13,7 +13,7 @@ */ package org.eclipse.daanse.sql.deparser.jsqlparser; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.ServiceScope; diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java index 2c2f7ba..44c3f08 100644 --- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectExpressionDeParser.java @@ -17,8 +17,8 @@ import java.util.Deque; import java.util.Set; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; import net.sf.jsqlparser.expression.DateValue; import net.sf.jsqlparser.expression.DoubleValue; diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java index ee801b4..02f5b1d 100644 --- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectSelectDeParser.java @@ -16,8 +16,8 @@ import java.util.HashSet; import java.util.Set; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.ExpressionVisitor; diff --git a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java index b9c0514..c87e514 100644 --- a/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java +++ b/deparser/jsqlparser/src/main/java/org/eclipse/daanse/sql/deparser/jsqlparser/BasicDialectStatementDeParser.java @@ -13,7 +13,7 @@ */ package org.eclipse.daanse.sql.deparser.jsqlparser; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import net.sf.jsqlparser.util.deparser.StatementDeParser; diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java index 65a2a54..cc254e9 100644 --- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/BaiscDialectExpressionDeParserTest.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.expression.DateValue; diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java index b28f02f..48c98c0 100644 --- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/DialectStatementDeParserTest.java @@ -15,7 +15,7 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.junit.jupiter.api.Test; import net.sf.jsqlparser.JSQLParserException; diff --git a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java index 9645547..e18328f 100644 --- a/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java +++ b/deparser/jsqlparser/src/test/java/org/eclipse/daanse/sql/deparser/jsqlparser/MockDialectHelper.java @@ -19,8 +19,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; public class MockDialectHelper { diff --git a/dialect/api/pom.xml b/dialect/api/pom.xml new file mode 100644 index 0000000..23728db --- /dev/null +++ b/dialect/api/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect + ${revision} + + org.eclipse.daanse.sql.dialect.api + Eclipse Daanse JDBC DB Dialect API + API definitions for database dialect support in Daanse JDBC. + Provides interfaces and contracts for implementing database-specific SQL + dialects, query optimization, and database feature handling across different + database systems. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + 0.0.1-SNAPSHOT + + + diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DaanseDialectConstants.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DaanseDialectConstants.java new file mode 100644 index 0000000..7d405d6 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DaanseDialectConstants.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api; + +public final class DaanseDialectConstants { + + private DaanseDialectConstants() { + // Utility class, no instantiation + } + + public static final String PREFIX = "org.eclipse.daanse"; + + public static final String DIALECT_NAME_PROPERTY = PREFIX + ".dialect.name"; +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/Dialect.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/Dialect.java new file mode 100644 index 0000000..7811c3e --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/Dialect.java @@ -0,0 +1,203 @@ +/* + * This software is subject to the terms of the Eclipse Public License v1.0 + * Agreement, available at the following URL: + * http://www.eclipse.org/legal/epl-v10.html. + * You must accept the terms of that agreement to use this software. + * + * Copyright (c) 2002-2019 Hitachi Vantara and others. + * Copyright (C) 2021 Sergei Semenkov + * All rights reserved. + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ + +package org.eclipse.daanse.sql.dialect.api; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.eclipse.daanse.sql.dialect.api.capability.DialectCapabilitiesProvider; +import org.eclipse.daanse.sql.dialect.api.generator.AggregationGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.CastGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.CteGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.FunctionGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.HintGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.IdentifierQuoter; +import org.eclipse.daanse.sql.dialect.api.generator.LiteralQuoter; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.OrderByGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.ParameterPlaceholderGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.RegexGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.SqlGenerator; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.api.type.TypeMapper; + +public interface Dialect + extends IdentifierQuoter, LiteralQuoter, DialectCapabilitiesProvider, TypeMapper { + + /** + * Whether double values need an explicit {@code "E0"} exponent (LucidDB quirk). + */ + boolean needsExponent(Object value, String valueString); + + /** + * Whether the given (type, concurrency) combination is supported on this + * engine. + */ + boolean supportsResultSetConcurrency(int type, int concurrency); + + /** + * Picks the most appropriate {@link BestFitColumnType} for a result-set column. + */ + BestFitColumnType getType(ResultSetMetaData metadata, int columnIndex) throws SQLException; + + /** @return dialect name (lowercase by convention) */ + String name(); + + /** @return the engine's case-folding rule for unquoted identifiers */ + default IdentifierCaseFolding caseFolding() { + return IdentifierCaseFolding.UPPER; + } + + default IdentifierQuotingPolicy quotingPolicy() { + return IdentifierQuotingPolicy.ALWAYS; + } + + // -------------------- focused-interface accessors -------------------- + // + // The cross-cutting interfaces ({@link IdentifierQuoter}, {@link + // LiteralQuoter}, + // {@link DialectCapabilitiesProvider}, {@link TypeMapper}) + // are inherited directly — call them on the dialect. The four "builder" + // facets below are accessed only via getters. Concrete dialects implement + // those interfaces and return {@code this} from each getter. + + /** + * Inline-dataset SQL emission ({@code generateInline} — consumed by the statement renderer's + * {@code FromInline} spelling). + */ + SqlGenerator sqlGenerator(); + + /** + * All DDL emission — both string-based primitives ({@code createSchema}, + * {@code dropTable}, {@code clearTable}, {@code dropSchema}) and + * descriptor-based builders ({@code createTable}, {@code createIndex}, + * {@code dropConstraint}, …). + */ + DdlGenerator ddlGenerator(); + + /** ORDER BY clause + NULL ordering builders. */ + OrderByGenerator orderByGenerator(); + + /** Regex match/flag emission. */ + RegexGenerator regexGenerator(); + + /** Aggregate and window function builders. */ + AggregationGenerator aggregationGenerator(); + + /** + * SQL scalar-function wrappers ({@code UPPER}, {@code CASE/IIF}, count + * adornments). + */ + FunctionGenerator functionGenerator(); + + /** + * Dialect-specific query hint emission (e.g. {@code USE INDEX}, + * {@code WITH (NOLOCK)}). + */ + HintGenerator hintGenerator(); + + /** + * Pagination ({@code LIMIT/OFFSET} vs. {@code FETCH NEXT … ROWS ONLY} vs. + * {@code TOP n}). + */ + default PaginationGenerator paginationGenerator() { + return new PaginationGenerator() { + }; + } + + /** + * Prepared-statement parameter placeholders ({@code ?}, {@code $n}, + * {@code @pn}). + */ + default ParameterPlaceholderGenerator parameterPlaceholderGenerator() { + return new ParameterPlaceholderGenerator() { + }; + } + + /** Common-table-expression ({@code WITH [RECURSIVE] …}) emission. */ + default CteGenerator cteGenerator() { + return new CteGenerator() { + }; + } + + /** + * Upsert / merge emission ({@code MERGE INTO}, {@code ON CONFLICT}, + * {@code ON DUPLICATE KEY UPDATE}). + */ + default MergeGenerator mergeGenerator() { + return new MergeGenerator() { + }; + } + + /** + * Type-cast emission ({@code CAST(x AS T)}, {@code TRY_CAST}, + * {@code SAFE_CAST}). + */ + default CastGenerator castGenerator() { + return new CastGenerator() { + }; + } + + /** {@code RETURNING}/{@code OUTPUT} clause emission for DML. */ + default ReturningGenerator returningGenerator() { + return new ReturningGenerator() { + }; + } + + /** + * The spelling of the duplicate-eliminating set-union operator. ANSI SQL and + * virtually every DBMS accept the bare {@code union}; + */ + default String unionDistinctKeyword() { + return "union"; + } + + /** + * Appends {@code value} to {@code buf} as a literal quoted for {@code datatype} — routing each + * type category to the matching {@code quote*Literal} rule. Lives here (not on the neutral + * {@link Datatype} type model) so the type model carries no dialect dependency. + * + * @param datatype the column type whose category selects the quoting rule + * @param buf destination buffer (the literal is appended) + * @param value raw literal text in the canonical SQL form for this type + */ + default void quoteLiteral(Datatype datatype, StringBuilder buf, String value) { + switch (datatype) { + case VARCHAR, UUID, JSON, XML, INTERVAL, ARRAY, STRUCT, BINARY -> quoteStringLiteral(buf, value); + case NUMERIC, INTEGER, DECIMAL, FLOAT, REAL, BIGINT, SMALLINT, DOUBLE -> quoteNumericLiteral(buf, value); + case BOOLEAN -> quoteBooleanLiteral(buf, value); + case DATE -> quoteDateLiteral(buf, value); + case TIME -> quoteTimeLiteral(buf, value); + case TIMESTAMP -> quoteTimestampLiteral(buf, value); + } + } + +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectException.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectException.java new file mode 100644 index 0000000..ad0d0e1 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena, Stefan Bischof - initial + * + */ +package org.eclipse.daanse.sql.dialect.api; + +import java.sql.SQLException; + +@SuppressWarnings("serial") +public class DialectException extends RuntimeException { + + /** + * Wrap a {@link SQLException} from JDBC metadata access with a contextual + * message. + */ + public DialectException(String msg, SQLException e) { + super(msg, e); + } + + /** + * Wrap any underlying exception so callers see a single dialect-typed + * throwable. + */ + public DialectException(Exception e) { + super(e); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectFactory.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectFactory.java new file mode 100644 index 0000000..fe8f0c0 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ + +package org.eclipse.daanse.sql.dialect.api; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +public interface DialectFactory { + + /** Build a dialect from a pre-captured snapshot — the canonical entry point. */ + Dialect createDialect(DialectInitData init); + + default Dialect createDialect(Connection connection) throws SQLException { + return createDialect(DialectInitData.fromConnection(connection)); + } + + default Dialect createDialect(DataSource dataSource) throws SQLException { + return createDialect(DialectInitData.fromDataSource(dataSource)); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectInitData.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectInitData.java new file mode 100644 index 0000000..1ac24cc --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectInitData.java @@ -0,0 +1,196 @@ +/* + * 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.api; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.sql.DataSource; + +/** + * @param quoteIdentifierString e.g. {@code "}, {@code `}, {@code [} + * @param productName {@link DatabaseMetaData#getDatabaseProductName()} + * @param productVersion {@link DatabaseMetaData#getDatabaseProductVersion()} + * @param databaseMajorVersion {@link DatabaseMetaData#getDatabaseMajorVersion()} + * @param databaseMinorVersion {@link DatabaseMetaData#getDatabaseMinorVersion()} + * @param supportedResultSetStyles encoded as {@code List} pairs of + * {@code (type, concurrency)} + * @param readOnly {@link DatabaseMetaData#isReadOnly()} + * @param maxColumnNameLength {@link DatabaseMetaData#getMaxColumnNameLength()} + * @param sqlKeywordsLower engine-specific reserved words from + * {@link DatabaseMetaData#getSQLKeywords()}, + * lower-cased + * @param quotingPolicy initial policy; defaults to + * {@link IdentifierQuotingPolicy#ALWAYS} when + * unset + */ +public record DialectInitData(String quoteIdentifierString, String productName, String productVersion, + int databaseMajorVersion, int databaseMinorVersion, Set> supportedResultSetStyles, + boolean readOnly, int maxColumnNameLength, Set sqlKeywordsLower, + IdentifierQuotingPolicy quotingPolicy) { + + /** + * Defensive copies — record components stay immutable even if callers mutate + * the input set. + */ + public DialectInitData { + supportedResultSetStyles = supportedResultSetStyles == null ? Set.of() : Set.copyOf(supportedResultSetStyles); + sqlKeywordsLower = sqlKeywordsLower == null ? Set.of() : Set.copyOf(sqlKeywordsLower); + if (quotingPolicy == null) { + quotingPolicy = IdentifierQuotingPolicy.ALWAYS; + } + } + + public static DialectInitData ansiDefaults() { + return new DialectInitData("\"", "", "", 0, 0, Set.of(), true, 0, Set.of(), IdentifierQuotingPolicy.ALWAYS); + } + + public static DialectInitData fromConnection(Connection connection) throws SQLException { + if (connection == null) { + return ansiDefaults(); + } + DatabaseMetaData md = connection.getMetaData(); + + // null means "driver doesn't support identifier quoting" — emitIdentifier + // honours this by emitting verbatim regardless of policy. We only fall + // back to ANSI double-quote when the driver throws or returns blank. + String quote = null; + try { + String q = md.getIdentifierQuoteString(); + if (q != null && !q.isEmpty() && !" ".equals(q)) + quote = q; + } catch (SQLException ignore) { + quote = "\""; + } + + String productName = ""; + try { + productName = nullToEmpty(md.getDatabaseProductName()); + } catch (SQLException ignore) { + /* keep default */ } + + String productVersion = ""; + try { + productVersion = nullToEmpty(md.getDatabaseProductVersion()); + } catch (SQLException ignore) { + /* keep default */ } + + int major = 0; + try { + major = md.getDatabaseMajorVersion(); + } catch (SQLException | UnsupportedOperationException ignore) { + /* keep 0 */ } + + int minor = 0; + try { + minor = md.getDatabaseMinorVersion(); + } catch (SQLException | UnsupportedOperationException ignore) { + /* keep 0 */ } + + boolean readOnly = false; + try { + readOnly = md.isReadOnly(); + } catch (SQLException ignore) { + /* keep default */ } + + int maxColLen = 0; + try { + maxColLen = md.getMaxColumnNameLength(); + } catch (SQLException ignore) { + /* keep 0 */ } + + Set keywords = new HashSet<>(); + try { + String csv = md.getSQLKeywords(); + if (csv != null) { + for (String kw : csv.split(",")) { + String t = kw.trim().toLowerCase(Locale.ROOT); + if (!t.isEmpty()) + keywords.add(t); + } + } + } catch (SQLException ignore) { + /* leave empty */ } + + Set> styles = new HashSet<>(); + for (int t : new int[] { ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, + ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int c : new int[] { ResultSet.CONCUR_READ_ONLY, ResultSet.CONCUR_UPDATABLE }) { + try { + if (md.supportsResultSetConcurrency(t, c)) + styles.add(List.of(t, c)); + } catch (SQLException ignore) { + /* skip */ } + } + } + + return new DialectInitData(quote, productName, productVersion, major, minor, styles, readOnly, maxColLen, + keywords, IdentifierQuotingPolicy.ALWAYS); + } + + public static DialectInitData fromDataSource(DataSource dataSource) throws SQLException { + if (dataSource == null) { + return ansiDefaults(); + } + try (Connection c = dataSource.getConnection()) { + return fromConnection(c); + } + } + + private static String nullToEmpty(String s) { + return s == null ? "" : s; + } + + /** + * {@link DialectVersion} view of {@link #databaseMajorVersion} / + * {@link #databaseMinorVersion}. + */ + public DialectVersion version() { + return new DialectVersion(databaseMajorVersion, databaseMinorVersion); + } + + /** Returns a copy with a different {@link IdentifierQuotingPolicy}. */ + public DialectInitData withQuotingPolicy(IdentifierQuotingPolicy policy) { + return new DialectInitData(quoteIdentifierString, productName, productVersion, databaseMajorVersion, + databaseMinorVersion, supportedResultSetStyles, readOnly, maxColumnNameLength, sqlKeywordsLower, + policy); + } + + /** + * Returns a copy with a different identifier quote string (e.g. {@code `} for + * MySQL). + */ + public DialectInitData withQuoteIdentifierString(String quote) { + return new DialectInitData(quote, productName, productVersion, databaseMajorVersion, databaseMinorVersion, + supportedResultSetStyles, readOnly, maxColumnNameLength, sqlKeywordsLower, quotingPolicy); + } + + /** Returns a copy with a different reserved-word set. */ + public DialectInitData withSqlKeywordsLower(Set keywords) { + return new DialectInitData(quoteIdentifierString, productName, productVersion, databaseMajorVersion, + databaseMinorVersion, supportedResultSetStyles, readOnly, maxColumnNameLength, keywords, quotingPolicy); + } + + /** + * @param major database major version (≥ 0; 0 means "unknown") + * @param minor database minor version (≥ 0) + * @return a new {@code DialectInitData} with the specified version + */ + public DialectInitData withVersion(int major, int minor) { + return new DialectInitData(quoteIdentifierString, productName, productVersion, major, minor, + supportedResultSetStyles, readOnly, maxColumnNameLength, sqlKeywordsLower, quotingPolicy); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectName.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectName.java new file mode 100644 index 0000000..d889455 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectName.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.osgi.service.component.annotations.ComponentPropertyType; + +@ComponentPropertyType +@Retention(RetentionPolicy.CLASS) +@Target({ TYPE, METHOD, FIELD }) +public @interface DialectName { + + String value(); + + String PREFIX_ = DaanseDialectConstants.PREFIX + "."; + +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectVersion.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectVersion.java new file mode 100644 index 0000000..a448692 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/DialectVersion.java @@ -0,0 +1,101 @@ +/* + * 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.api; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Comparator; +import java.util.Objects; + +public record DialectVersion(int major, int minor, int patch) implements Comparable { + + private static final Comparator ORDER = Comparator.comparingInt(DialectVersion::major) + .thenComparingInt(DialectVersion::minor).thenComparingInt(DialectVersion::patch); + + /** Sentinel used when the connection's driver reports no usable version. */ + public static final DialectVersion UNKNOWN = new DialectVersion(0, 0, 0); + + public DialectVersion { + if (major < 0 || minor < 0 || patch < 0) { + throw new IllegalArgumentException( + "DialectVersion components must be non-negative: " + major + "." + minor + "." + patch); + } + } + + /** + * Convenience: a version with patch=0. Use when the patch level isn't tracked. + */ + public DialectVersion(int major, int minor) { + this(major, minor, 0); + } + + public static DialectVersion of(Connection connection) throws SQLException { + Objects.requireNonNull(connection, "connection"); + DatabaseMetaData md = connection.getMetaData(); + int maj; + int min; + try { + maj = md.getDatabaseMajorVersion(); + min = md.getDatabaseMinorVersion(); + } catch (SQLException | UnsupportedOperationException e) { + return UNKNOWN; + } + if (maj < 0 || min < 0) { + return UNKNOWN; + } + int patch = parsePatch(md.getDatabaseProductVersion()); + return new DialectVersion(maj, min, patch); + } + + /** Returns true iff {@code this >= other}. */ + public boolean atLeast(DialectVersion other) { + return ORDER.compare(this, other) >= 0; + } + + /** Returns true iff {@code this >= (major.minor.0)}. */ + public boolean atLeast(int major, int minor) { + return atLeast(new DialectVersion(major, minor, 0)); + } + + /** @return {@code true} if UNKNOWN or {@code this >= (major.minor.0)} */ + public boolean isUnknownOrAtLeast(int major, int minor) { + return this.equals(UNKNOWN) || atLeast(major, minor); + } + + @Override + public int compareTo(DialectVersion o) { + return ORDER.compare(this, o); + } + + private static int parsePatch(String productVersion) { + if (productVersion == null) { + return 0; + } + // Match the third dotted integer (`14.5.2` → `2`, `9.4.26-server` → `26`). + int dot1 = productVersion.indexOf('.'); + if (dot1 < 0) + return 0; + int dot2 = productVersion.indexOf('.', dot1 + 1); + if (dot2 < 0) + return 0; + int end = dot2 + 1; + while (end < productVersion.length() && Character.isDigit(productVersion.charAt(end))) { + end++; + } + if (end == dot2 + 1) + return 0; + try { + return Integer.parseInt(productVersion.substring(dot2 + 1, end)); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierCaseFolding.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierCaseFolding.java new file mode 100644 index 0000000..1537d6a --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierCaseFolding.java @@ -0,0 +1,66 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.dialect.api; + +import java.util.Locale; + +/** + * How an engine canonicalizes unquoted identifiers when storing and looking + * them up in the catalog. + */ +public enum IdentifierCaseFolding { + + /** + * Unquoted identifiers are folded to upper case (Oracle, Db2, H2-default, + * Snowflake, SAP HANA). + */ + UPPER, + + /** + * Unquoted identifiers are folded to lower case (PostgreSQL, Redshift, + * Greenplum). + */ + LOWER, + + /** + * Unquoted identifiers are stored exactly as written (MySQL with case-sensitive + * collation, SQL Server). + */ + PRESERVE; + + /** + * @param name identifier to fold; may be {@code null} + * @return the input folded according to this policy, or {@code null} if input + * was {@code null} + */ + public String fold(String name) { + if (name == null) { + return null; + } + return switch (this) { + case UPPER -> name.toUpperCase(Locale.ROOT); + case LOWER -> name.toLowerCase(Locale.ROOT); + case PRESERVE -> name; + }; + } + + /** + * @param name identifier to test; may be {@code null} + * @return {@code true} when {@code name} already matches its folded form (i.e. + * quoting it would not change resolution) + */ + public boolean isCanonical(String name) { + return name != null && name.equals(fold(name)); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierQuotingPolicy.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierQuotingPolicy.java new file mode 100644 index 0000000..eb4d371 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/IdentifierQuotingPolicy.java @@ -0,0 +1,34 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.dialect.api; + +/** + * When the SQL generator should wrap identifiers in the engine's quote + * characters. Trades safety against readability of emitted SQL. + */ +public enum IdentifierQuotingPolicy { + + /** Always quote — safe default; preserves case and accepts any character. */ + ALWAYS, + + /** + * Quote only when needed — reserved word, mixed case, or special characters. + */ + WHEN_NEEDED, + + /** + * Never quote — caller guarantees identifiers are safe; smallest emitted SQL. + */ + NEVER +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/AggregateCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/AggregateCapabilities.java new file mode 100644 index 0000000..aec1250 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/AggregateCapabilities.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.capability; + +/** + * @param countDistinct whether the dialect supports + * COUNT(DISTINCT column) + * @param multipleCountDistinct whether the dialect supports multiple + * COUNT(DISTINCT) in the same query + * @param compoundCountDistinct whether the dialect supports + * COUNT(DISTINCT col1, col2) + * @param countDistinctWithOtherAggs whether COUNT(DISTINCT) can be used with + * other aggregates in the same query + * @param innerDistinct whether the dialect allows DISTINCT in + * inner/subqueries + * @param multipleDistinctSqlMeasures whether the dialect supports multiple + * distinct SQL measures + * @param groupingSets whether the dialect supports GROUPING SETS + * @param groupByExpressions whether the dialect supports expressions + * in GROUP BY + * @param selectNotInGroupBy whether the dialect allows SELECT columns + * not in GROUP BY (MySQL-style) + */ +public record AggregateCapabilities(boolean countDistinct, boolean multipleCountDistinct, boolean compoundCountDistinct, + boolean countDistinctWithOtherAggs, boolean innerDistinct, boolean multipleDistinctSqlMeasures, + boolean groupingSets, boolean groupByExpressions, boolean selectNotInGroupBy) { + + /** @return AggregateCapabilities with all features enabled */ + public static AggregateCapabilities full() { + return new AggregateCapabilities(true, // countDistinct + true, // multipleCountDistinct + true, // compoundCountDistinct + true, // countDistinctWithOtherAggs + true, // innerDistinct + true, // multipleDistinctSqlMeasures + true, // groupingSets + true, // groupByExpressions + false // selectNotInGroupBy (usually false for standard SQL) + ); + } + + /** @return AggregateCapabilities with minimal features */ + public static AggregateCapabilities minimal() { + return new AggregateCapabilities(true, // countDistinct + false, // multipleCountDistinct + false, // compoundCountDistinct + false, // countDistinctWithOtherAggs + false, // innerDistinct + false, // multipleDistinctSqlMeasures + false, // groupingSets + false, // groupByExpressions + false // selectNotInGroupBy + ); + } + + /** @return AggregateCapabilities with all features disabled */ + public static AggregateCapabilities none() { + return new AggregateCapabilities(false, false, false, false, false, false, false, false, false); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java new file mode 100644 index 0000000..3d53181 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DdlCapabilities.java @@ -0,0 +1,47 @@ +/* + * 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.api.capability; + +/** + * @param supportsDdl whether DDL is permitted at all + * @param dropTableCascade {@code DROP TABLE … CASCADE} + * @param sequences {@code CREATE/DROP SEQUENCE} + + * {@code NEXT VALUE FOR} + * @param dropIndexRequiresTable {@code DROP INDEX … ON table} + * @param createTableIfNotExists {@code CREATE TABLE IF NOT EXISTS} + * @param createIndexIfNotExists {@code CREATE INDEX IF NOT EXISTS} + * @param dropIndexIfExists {@code DROP INDEX IF EXISTS} + * @param createOrReplaceView {@code CREATE OR REPLACE VIEW} + * @param createOrReplaceTrigger {@code CREATE OR REPLACE TRIGGER} + * @param dropViewIfExists {@code DROP VIEW IF EXISTS} + * @param dropConstraintIfExists {@code ALTER TABLE … DROP CONSTRAINT IF EXISTS} + * @param dropTableIfExists {@code DROP TABLE IF EXISTS} + * @param dropSchemaIfExists {@code DROP SCHEMA IF EXISTS} + * @param requiresDropSchemaRestrict {@code DROP SCHEMA name RESTRICT} required + * @param maxColumnNameLength driver-reported column-name limit + */ +public record DdlCapabilities(boolean supportsDdl, boolean dropTableCascade, boolean sequences, + boolean dropIndexRequiresTable, boolean createTableIfNotExists, boolean createIndexIfNotExists, + boolean dropIndexIfExists, boolean createOrReplaceView, boolean createOrReplaceTrigger, + boolean dropViewIfExists, boolean dropConstraintIfExists, boolean dropTableIfExists, boolean dropSchemaIfExists, + boolean requiresDropSchemaRestrict, int maxColumnNameLength) { + + /** All supported, no special requirements — default for modern engines. */ + public static DdlCapabilities full() { + return new DdlCapabilities(true, true, true, false, true, true, true, true, true, true, true, true, true, false, + 128); + } + + /** Most conservative — DDL allowed but no convenience clauses. */ + public static DdlCapabilities minimal() { + return new DdlCapabilities(true, false, false, true, false, false, false, false, false, false, false, false, + false, true, 30); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java new file mode 100644 index 0000000..068dda1 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/DialectCapabilitiesProvider.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.capability; + +public interface DialectCapabilitiesProvider { + + default AggregateCapabilities getAggregateCapabilities() { + return new AggregateCapabilities(allowsCountDistinct(), allowsMultipleCountDistinct(), + allowsCompoundCountDistinct(), allowsCountDistinctWithOtherAggs(), allowsInnerDistinct(), + allowsMultipleDistinctSqlMeasures(), supportsGroupingSets(), supportsGroupByExpressions(), + allowsSelectNotInGroupBy()); + } + + default boolean allowsSelectNotInGroupBy() { + return false; + } + + default JoinCapabilities getJoinCapabilities() { + return new JoinCapabilities(allowsJoinOn(), allowsFromAlias(), allowsFromQuery(), requiresAliasForFromQuery()); + } + + default OrderByCapabilities getOrderByCapabilities() { + return new OrderByCapabilities(allowsOrderByAlias(), requiresOrderByAlias(), requiresUnionOrderByOrdinal(), + requiresUnionOrderByExprInSelect(), requiresGroupByAlias(), requiresHavingAlias(), supportsNullsLast()); + } + + WindowFunctionCapabilities getWindowFunctionCapabilities(); + + default DdlCapabilities getDdlCapabilities() { + return new DdlCapabilities(supportsDdl(), supportsDropTableCascade(), supportsSequences(), + dropIndexRequiresTable(), supportsCreateTableIfNotExists(), supportsCreateIndexIfNotExists(), + supportsDropIndexIfExists(), supportsCreateOrReplaceView(), supportsCreateOrReplaceTrigger(), + supportsDropViewIfExists(), supportsDropConstraintIfExists(), supportsDropTableIfExists(), + supportsDropSchemaIfExists(), requiresDropSchemaRestrict(), getMaxColumnNameLength()); + } + + int getMaxColumnNameLength(); + + /** @return true if DDL is allowed */ + boolean supportsDdl(); + + /** @return true if {@code DROP TABLE … CASCADE} is recognised */ + default boolean supportsDropTableCascade() { + return true; + } + + /** @return true if SQL sequences are supported */ + default boolean supportsSequences() { + return true; + } + + /** + * @return true if {@code DROP INDEX} must be qualified with the owning table; + * false (default) otherwise + */ + default boolean dropIndexRequiresTable() { + return false; + } + + /** + * @return true if the dialect honours the {@code IF NOT EXISTS} clause on + * {@code CREATE TABLE} + */ + default boolean supportsCreateTableIfNotExists() { + return true; + } + + /** + * @return true if the dialect supports plain b-tree {@code CREATE INDEX} DDL at all. + * ClickHouse for example rejects {@code CREATE INDEX} without a data-skipping + * {@code TYPE} clause (INCORRECT_QUERY 80) — tools generating index DDL must + * skip or specialize there. + */ + default boolean supportsIndexDdl() { + return true; + } + + /** + * @return true if the dialect honours {@code IF NOT EXISTS} on + * {@code CREATE INDEX} + */ + default boolean supportsCreateIndexIfNotExists() { + return true; + } + + /** + * @return true if the dialect honours {@code IF EXISTS} on {@code DROP INDEX} + */ + default boolean supportsDropIndexIfExists() { + return true; + } + + /** + * @return true if the dialect honours the {@code OR REPLACE} keyword on + * {@code CREATE VIEW} + */ + default boolean supportsCreateOrReplaceView() { + return true; + } + + /** + * @return true if the dialect honours {@code OR REPLACE} on + * {@code CREATE TRIGGER} + */ + default boolean supportsCreateOrReplaceTrigger() { + return false; + } + + /** + * @return true if the dialect honours {@code IF EXISTS} on {@code DROP VIEW} + */ + default boolean supportsDropViewIfExists() { + return true; + } + + /** + * @return true if the dialect honours {@code IF EXISTS} on + * {@code DROP CONSTRAINT} + */ + default boolean supportsDropConstraintIfExists() { + return true; + } + + /** + * @return true if the dialect honours {@code IF EXISTS} on {@code DROP TABLE} + */ + default boolean supportsDropTableIfExists() { + return true; + } + + /** + * @return true if the dialect honours {@code IF EXISTS} on {@code DROP SCHEMA} + */ + default boolean supportsDropSchemaIfExists() { + return true; + } + + /** + * @return true if the dialect requires {@code RESTRICT} after + * {@code DROP SCHEMA name} + */ + default boolean requiresDropSchemaRestrict() { + return false; + } + + /** @return true if dialect sharing is allowed */ + boolean allowsDialectSharing(); + + /** @return true if regex is supported */ + boolean allowsRegularExpressionInWhereClause(); + + /** @return true if multi-value IN is supported */ + boolean supportsMultiValueInExpr(); + + /** @return true if unlimited value lists are supported */ + boolean supportsUnlimitedValueList(); + + /** @return true if required */ + boolean requiresDrillthroughMaxRowsInLimit(); + + /** @return true if parallel loading is supported */ + boolean supportsParallelLoading(); + + /** @return true if batch operations are supported */ + boolean supportsBatchOperations(); + + /** @return true if AS is allowed in field aliases */ + boolean allowsFieldAlias(); + + // -------------------- canonical capability flags -------------------- + // + // These flat boolean methods are the source of truth — every dialect + // implements (or inherits) one definitive value per flag. The + // record-based getters above (getAggregateCapabilities, getJoinCapabilities, + // …) are convenience aggregations that bundle the flags by concern; + // callers that only need one bundle ask for that record, callers that + // need a single flag call the flat method directly. + + boolean allowsFromAlias(); + + boolean allowsFromQuery(); + + boolean requiresAliasForFromQuery(); + + boolean allowsJoinOn(); + + boolean allowsCountDistinct(); + + boolean allowsMultipleCountDistinct(); + + boolean allowsCompoundCountDistinct(); + + boolean allowsCountDistinctWithOtherAggs(); + + boolean allowsInnerDistinct(); + + boolean allowsMultipleDistinctSqlMeasures(); + + boolean supportsGroupingSets(); + + boolean supportsGroupByExpressions(); + + boolean allowsOrderByAlias(); + + boolean requiresOrderByAlias(); + + boolean requiresGroupByAlias(); + + boolean requiresHavingAlias(); + + boolean requiresUnionOrderByOrdinal(); + + boolean requiresUnionOrderByExprInSelect(); + + // Aggregation/window-function feature flags + // (supportsPercentileDisc/Cont, supportsListAgg, supportsNthValue, + // supportsNthValueIgnoreNulls) live on AggregationGenerator. They're + // exposed in WindowFunctionCapabilities below via the dialect's + // aggregationGenerator() — see Dialect.getWindowFunctionCapabilities(). + + default boolean supportsNullsLast() { + return true; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/JoinCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/JoinCapabilities.java new file mode 100644 index 0000000..f9bd333 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/JoinCapabilities.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.capability; + +/** + * @param joinOn whether the dialect supports ANSI JOIN...ON + * syntax + * @param asKeyword whether the dialect allows the AS keyword in + * FROM clause aliases + * @param fromQuery whether the dialect allows subqueries in the + * FROM clause + * @param requiresAliasForFromQuery whether subqueries in FROM require an alias + */ +public record JoinCapabilities(boolean joinOn, boolean asKeyword, boolean fromQuery, + boolean requiresAliasForFromQuery) { + + /** @return JoinCapabilities with standard ANSI SQL features */ + public static JoinCapabilities standard() { + return new JoinCapabilities(true, // joinOn + true, // asKeyword + true, // fromQuery + true // requiresAliasForFromQuery + ); + } + + /** @return JoinCapabilities with optional subquery alias */ + public static JoinCapabilities withOptionalAlias() { + return new JoinCapabilities(true, // joinOn + true, // asKeyword + true, // fromQuery + false // requiresAliasForFromQuery + ); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/OrderByCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/OrderByCapabilities.java new file mode 100644 index 0000000..4aaa8a1 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/OrderByCapabilities.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.capability; + +/** + * @param allowsOrderByAlias whether ORDER BY can reference SELECT + * aliases + * @param requiresOrderByAlias whether ORDER BY must use SELECT + * aliases + * @param requiresUnionOrderByOrdinal whether UNION ORDER BY requires + * ordinal position (e.g., ORDER BY 1) + * @param requiresUnionOrderByExprInSelect whether UNION ORDER BY expression + * must be in SELECT clause + * @param requiresGroupByAlias whether GROUP BY must use SELECT + * aliases + * @param requiresHavingAlias whether HAVING must use SELECT + * aliases + * @param supportsNullsLast whether NULLS FIRST/LAST syntax is + * supported + */ +public record OrderByCapabilities(boolean allowsOrderByAlias, boolean requiresOrderByAlias, + boolean requiresUnionOrderByOrdinal, boolean requiresUnionOrderByExprInSelect, boolean requiresGroupByAlias, + boolean requiresHavingAlias, boolean supportsNullsLast) { + + /** @return OrderByCapabilities with standard features */ + public static OrderByCapabilities standard() { + return new OrderByCapabilities(true, // allowsOrderByAlias + false, // requiresOrderByAlias + false, // requiresUnionOrderByOrdinal + false, // requiresUnionOrderByExprInSelect + false, // requiresGroupByAlias + false, // requiresHavingAlias + true // supportsNullsLast + ); + } + + /** @return OrderByCapabilities with minimal features */ + public static OrderByCapabilities minimal() { + return new OrderByCapabilities(false, // allowsOrderByAlias + false, // requiresOrderByAlias + false, // requiresUnionOrderByOrdinal + false, // requiresUnionOrderByExprInSelect + false, // requiresGroupByAlias + false, // requiresHavingAlias + false // supportsNullsLast + ); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/WindowFunctionCapabilities.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/WindowFunctionCapabilities.java new file mode 100644 index 0000000..a1a9d63 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/WindowFunctionCapabilities.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.capability; + +/** + * @param percentileDisc whether the dialect supports PERCENTILE_DISC + * function + * @param percentileCont whether the dialect supports PERCENTILE_CONT + * function + * @param listAgg whether the dialect supports list aggregation + * (LISTAGG, GROUP_CONCAT, STRING_AGG) + * @param nthValue whether the dialect supports NTH_VALUE window + * function + * @param nthValueIgnoreNulls whether the NTH_VALUE function supports IGNORE + * NULLS / RESPECT NULLS syntax + */ +public record WindowFunctionCapabilities(boolean percentileDisc, boolean percentileCont, boolean listAgg, + boolean nthValue, boolean nthValueIgnoreNulls) { + + /** @return WindowFunctionCapabilities with all features enabled */ + public static WindowFunctionCapabilities full() { + return new WindowFunctionCapabilities(true, // percentileDisc + true, // percentileCont + true, // listAgg + true, // nthValue + true // nthValueIgnoreNulls + ); + } + + /** @return WindowFunctionCapabilities with minimal features */ + public static WindowFunctionCapabilities minimal() { + return new WindowFunctionCapabilities(false, // percentileDisc + false, // percentileCont + false, // listAgg + false, // nthValue + false // nthValueIgnoreNulls + ); + } + + /** @return WindowFunctionCapabilities with all features disabled */ + public static WindowFunctionCapabilities none() { + return new WindowFunctionCapabilities(false, false, false, false, false); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/package-info.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/package-info.java new file mode 100644 index 0000000..df99c7a --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/capability/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.api.capability; diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java new file mode 100644 index 0000000..54556d0 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/AggregationGenerator.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import org.eclipse.daanse.sql.model.sql.BitOperation; + +import java.util.List; + +import org.eclipse.daanse.sql.model.sql.OrderedColumn; + +public interface AggregationGenerator extends IdentifierQuoter { + + String DESC = " DESC"; + String COMMA = ", "; + String OPEN_PAREN = "( "; + String CLOSE_PAREN = " )"; + String ORDER_BY = "ORDER BY "; + String WITHIN_GROUP = " WITHIN GROUP ("; + String OVER = "OVER ( "; + String IGNORE_NULLS = " IGNORE NULLS "; + String RESPECT_NULLS = " RESPECT NULLS "; + + // -------------------- list / string aggregation -------------------- + + default java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + return java.util.Optional.empty(); + } + + /** Whether list aggregation is supported by this dialect. */ + default boolean supportsListAgg() { + return false; + } + + // -------------------- NTH_VALUE -------------------- + + default java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional.empty(); + } + + default boolean supportsNthValue() { + return false; + } + + default boolean supportsNthValueIgnoreNulls() { + return false; + } + + // -------------------- PERCENTILE -------------------- + + /** Discrete percentile aggregate; empty when not supported. */ + default java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional.empty(); + } + + /** Continuous (interpolated) percentile aggregate; empty when not supported. */ + default java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional.empty(); + } + + default boolean supportsPercentileDisc() { + return false; + } + + default boolean supportsPercentileCont() { + return false; + } + + // -------------------- bitwise aggregation -------------------- + + default java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + return java.util.Optional.empty(); + } + + /** Whether the given bitwise operation is supported. */ + default boolean supportsBitAggregation(BitOperation operation) { + return false; + } + + // -------------------- shared formatting helpers -------------------- + + default StringBuilder buildPercentileFunction(String functionName, double percentile, boolean desc, + String tableName, String columnName) { + StringBuilder buf = new StringBuilder(64); + buf.append(functionName).append("(").append(percentile).append(")").append(WITHIN_GROUP).append(ORDER_BY); + if (tableName != null) { + quoteIdentifier(buf, tableName, columnName); + } else { + quoteIdentifier(buf, columnName); + } + if (desc) { + buf.append(DESC); + } + buf.append(")"); + return buf; + } + + /** + * @param supportsNullsHandling whether the engine accepts {@code IGNORE NULLS}/ + * {@code RESPECT NULLS} + */ + default StringBuilder buildNthValueFunction(String functionName, CharSequence operand, boolean ignoreNulls, + Integer n, List columns, boolean supportsNullsHandling) { + StringBuilder buf = new StringBuilder(64); + buf.append(functionName); + buf.append(OPEN_PAREN); + buf.append(operand); + buf.append(COMMA); + buf.append(n == null || n < 1 ? 1 : n); + buf.append(CLOSE_PAREN); + if (supportsNullsHandling) { + buf.append(ignoreNulls ? IGNORE_NULLS : RESPECT_NULLS); + } + buf.append(OVER); + if (columns != null && !columns.isEmpty()) { + buf.append(ORDER_BY); + buf.append(buildOrderedColumnsClause(columns)); + } + buf.append(CLOSE_PAREN); + return buf; + } + + default CharSequence buildOrderedColumnsClause(List columns) { + StringBuilder buf = new StringBuilder(64); + if (columns == null) { + return buf; + } + boolean first = true; + for (OrderedColumn c : columns) { + if (!first) { + buf.append(COMMA); + } + if (c.tableName() != null) { + quoteIdentifier(buf, c.tableName(), c.columnName()); + } else { + quoteIdentifier(buf, c.columnName()); + } + c.sortDirection().ifPresent(dir -> buf.append(' ').append(dir.name())); + c.nullsOrder().ifPresent(no -> buf.append(" NULLS ").append(no.name())); + first = false; + } + return buf; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CastGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CastGenerator.java new file mode 100644 index 0000000..3d02d75 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CastGenerator.java @@ -0,0 +1,29 @@ +/* + * 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/ + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import org.eclipse.daanse.sql.model.type.Datatype; + +public interface CastGenerator { + + default String cast(String expr, Datatype targetType) { + return "CAST(" + expr + " AS " + nativeType(targetType) + ")"; + } + + default String nativeType(Datatype targetType) { + return targetType.getValue().toUpperCase(); + } + + default boolean supportsTryCast() { + return false; + } + + default String tryCast(String expr, Datatype targetType) { + return cast(expr, targetType); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CteGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CteGenerator.java new file mode 100644 index 0000000..a2c3e06 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/CteGenerator.java @@ -0,0 +1,51 @@ +/* + * 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/ + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; + +public interface CteGenerator { + + /** A single common-table expression (name + body). */ + record Cte(String name, String body) { + public Cte { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("name must not be blank"); + } + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank"); + } + } + } + + default String withClause(List ctes, boolean recursive) { + if (ctes == null || ctes.isEmpty()) + return ""; + StringBuilder sb = new StringBuilder("WITH "); + if (recursive && supportsRecursiveCte() && emitsRecursiveKeyword()) { + sb.append("RECURSIVE "); + } + boolean first = true; + for (Cte c : ctes) { + if (!first) + sb.append(", "); + sb.append(c.name()).append(" AS (").append(c.body()).append(")"); + first = false; + } + sb.append(' '); + return sb.toString(); + } + + default boolean supportsRecursiveCte() { + return true; + } + + default boolean emitsRecursiveKeyword() { + return true; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java new file mode 100644 index 0000000..e0d7199 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/DdlGenerator.java @@ -0,0 +1,705 @@ +/* + * 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.api.generator; + +import java.sql.JDBCType; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.dialect.api.capability.DialectCapabilitiesProvider; + +public interface DdlGenerator extends IdentifierQuoter, DialectCapabilitiesProvider { + + // -------------------- DDL primitives (string-based) -------------------- + + /** + * @param schemaName schema name (may be null) + * @return SQL statement to clear the table + */ + String clearTable(String schemaName, String tableName); + + /** + * @param schemaName schema name (may be null) + * @param ifExists whether to include IF EXISTS clause + * @return SQL statement to drop the table + */ + String dropTable(String schemaName, String tableName, boolean ifExists); + + /** + * @param ifExists whether to include IF NOT EXISTS clause + * @return SQL statement to create the schema + */ + String createSchema(String schemaName, boolean ifExists); + + /** + * @param ifExists whether to include IF EXISTS clause + * @param cascade whether to include CASCADE clause + * @return SQL statement to drop the schema + */ + String dropSchema(String schemaName, boolean ifExists, boolean cascade); + + // -------------------- DDL — descriptor-based -------------------- + + default String createTable(TableReference table, List columns, PrimaryKey primaryKey, + boolean ifNotExists) { + StringBuilder sb = new StringBuilder(); + boolean useIfNotExists = ifNotExists && supportsCreateTableIfNotExists(); + sb.append(useIfNotExists ? "CREATE TABLE IF NOT EXISTS " : "CREATE TABLE "); + sb.append(qualified(table)); + sb.append(" (\n"); + + boolean first = true; + for (ColumnDefinition cd : columns) { + if (!first) + sb.append(",\n"); + first = false; + sb.append(" ").append(quoteIdentifier(cd.column().name())); + sb.append(' ').append(nativeType(cd.columnMetaData())); + if (cd.columnMetaData().nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + cd.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + } + if (primaryKey != null && !primaryKey.columns().isEmpty()) { + sb.append(",\n PRIMARY KEY ("); + sb.append(String.join(", ", + primaryKey.columns().stream().map(c -> quoteIdentifier(c.name()).toString()).toList())); + sb.append(")"); + } + sb.append("\n)"); + return sb.toString(); + } + + // -------------------- DML -------------------- + + /** {@code INSERT INTO schema.table (col1, …) VALUES (?, …)} — parameterised. */ + default String insertInto(TableReference table, List columns) { + if (columns.isEmpty()) { + throw new IllegalArgumentException("columns must not be empty for INSERT"); + } + StringBuilder sb = new StringBuilder("INSERT INTO "); + sb.append(qualified(table)); + sb.append(" ("); + appendColumnList(sb, columns); + sb.append(") VALUES ("); + for (int i = 0; i < columns.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append('?'); + } + sb.append(")"); + return sb.toString(); + } + + /** {@code SELECT col1, … FROM schema.table}. */ + default String selectFrom(TableReference table, List columns) { + StringBuilder sb = new StringBuilder("SELECT "); + if (columns.isEmpty()) { + sb.append("*"); + } else { + appendColumnList(sb, columns); + } + sb.append(" FROM ").append(qualified(table)); + return sb.toString(); + } + + /** {@code SELECT * FROM schema.table}. */ + default String selectAll(TableReference table) { + return "SELECT * FROM " + qualified(table); + } + + default String update(TableReference table, List setColumns, + List whereColumns) { + if (setColumns.isEmpty()) { + throw new IllegalArgumentException("setColumns must not be empty for UPDATE"); + } + StringBuilder sb = new StringBuilder("UPDATE "); + sb.append(qualified(table)); + sb.append(" SET "); + for (int i = 0; i < setColumns.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(quoteIdentifier(setColumns.get(i).column().name())); + sb.append(" = ?"); + } + appendWhereEqAll(sb, whereColumns); + return sb.toString(); + } + + /** {@code DELETE FROM schema.table WHERE col1 = ? AND …}. */ + default String deleteFrom(TableReference table, List whereColumns) { + StringBuilder sb = new StringBuilder("DELETE FROM "); + sb.append(qualified(table)); + appendWhereEqAll(sb, whereColumns); + return sb.toString(); + } + + // -------------------- DDL — drop / truncate -------------------- + + /** {@code DROP TABLE [IF EXISTS] schema.table}. */ + default String dropTable(TableReference table, boolean ifExists) { + return dropTable(table, ifExists, false); + } + + default String dropTable(TableReference table, boolean ifExists, boolean cascade) { + String schemaName = table.schema().map(SchemaReference::name).orElse(null); + String base = dropTable(schemaName, table.name(), ifExists); + return (cascade && supportsDropTableCascade()) ? base + " CASCADE" : base; + } + + /** Dialect-specific truncate. */ + default String truncate(TableReference table) { + String schemaName = table.schema().map(SchemaReference::name).orElse(null); + return clearTable(schemaName, table.name()); + } + + // -------------------- DDL — ALTER -------------------- + + /** + * {@code ALTER TABLE schema.table ADD COLUMN col TYPE [NOT NULL] [DEFAULT …]}. + */ + default String alterTableAddColumn(TableReference table, ColumnDefinition column) { + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD COLUMN "); + sb.append(quoteIdentifier(column.column().name())); + sb.append(' ').append(nativeType(column.columnMetaData())); + if (column.columnMetaData().nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + column.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + return sb.toString(); + } + + /** {@code ALTER TABLE schema.table DROP COLUMN col}. */ + default String alterTableDropColumn(TableReference table, String columnName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" DROP COLUMN ") + .append(quoteIdentifier(columnName)).toString(); + } + + default String alterColumnType(TableReference table, String columnName, ColumnMetaData newMeta) { + if (newMeta == null) { + throw new IllegalArgumentException("newMeta must not be null for ALTER COLUMN TYPE"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(" TYPE ").append(nativeType(newMeta)).toString(); + } + + default String alterColumnSetNullability(TableReference table, String columnName, boolean nullable) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(nullable ? " DROP NOT NULL" : " SET NOT NULL").toString(); + } + + default String alterColumnSetNullability(TableReference table, String columnName, boolean nullable, + ColumnMetaData currentMeta) { + return alterColumnSetNullability(table, columnName, nullable); + } + + default String alterColumnSetDefault(TableReference table, String columnName, String defaultExpression) { + if (defaultExpression == null || defaultExpression.isBlank()) { + throw new IllegalArgumentException("defaultExpression must not be blank for SET DEFAULT"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(" SET DEFAULT ").append(defaultExpression).toString(); + } + + /** {@code ALTER TABLE schema.table ALTER COLUMN col DROP DEFAULT}. */ + default String alterColumnDropDefault(TableReference table, String columnName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(" DROP DEFAULT").toString(); + } + + default String renameColumn(TableReference table, String oldName, String newName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME COLUMN ") + .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); + } + + default String renameTable(TableReference table, String newName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME TO ") + .append(quoteIdentifier(newName)).toString(); + } + + default String renameIndex(String oldName, String newName, TableReference table) { + return new StringBuilder("ALTER INDEX ").append(quoteIdentifier(oldName)).append(" RENAME TO ") + .append(quoteIdentifier(newName)).toString(); + } + + default String renameConstraint(TableReference table, String oldName, String newName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME CONSTRAINT ") + .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); + } + + // -------------------- ALTER — ColumnReference-friendly overloads + // -------------------- + // + // ColumnReference carries the column name plus an optional parent table, so + // callers that already hold a ColumnReference can skip restating the + // TableReference. {@code column.table()} must be present for these overloads; + // otherwise an IllegalArgumentException is thrown. + + private static TableReference requireTable(ColumnReference column) { + if (column == null || column.table().isEmpty()) { + throw new IllegalArgumentException("ColumnReference must carry a parent TableReference"); + } + return column.table().get(); + } + + /** ALTER COLUMN TYPE driven by a {@link ColumnReference}. */ + default String alterColumnType(ColumnReference column, ColumnMetaData newMeta) { + return alterColumnType(requireTable(column), column.name(), newMeta); + } + + /** ALTER COLUMN nullability driven by a {@link ColumnReference}. */ + default String alterColumnSetNullability(ColumnReference column, boolean nullable) { + return alterColumnSetNullability(requireTable(column), column.name(), nullable); + } + + /** + * ALTER COLUMN nullability (type-aware) driven by a {@link ColumnReference}. + */ + default String alterColumnSetNullability(ColumnReference column, boolean nullable, ColumnMetaData currentMeta) { + return alterColumnSetNullability(requireTable(column), column.name(), nullable, currentMeta); + } + + /** ALTER COLUMN SET DEFAULT driven by a {@link ColumnReference}. */ + default String alterColumnSetDefault(ColumnReference column, String defaultExpression) { + return alterColumnSetDefault(requireTable(column), column.name(), defaultExpression); + } + + /** ALTER COLUMN DROP DEFAULT driven by a {@link ColumnReference}. */ + default String alterColumnDropDefault(ColumnReference column) { + return alterColumnDropDefault(requireTable(column), column.name()); + } + + /** + * RENAME COLUMN driven by a {@link ColumnReference} (the column being renamed). + */ + default String renameColumn(ColumnReference column, String newName) { + return renameColumn(requireTable(column), column.name(), newName); + } + + // -------------------- DDL — indexes -------------------- + + default String createIndex(String indexName, TableReference table, List columnNames, boolean unique, + boolean ifNotExists) { + if (columnNames == null || columnNames.isEmpty()) { + throw new IllegalArgumentException("columnNames must not be empty for CREATE INDEX"); + } + StringBuilder sb = new StringBuilder("CREATE "); + if (unique) + sb.append("UNIQUE "); + sb.append("INDEX "); + if (ifNotExists && supportsCreateIndexIfNotExists()) { + sb.append("IF NOT EXISTS "); + } + sb.append(quoteIdentifier(indexName)); + sb.append(" ON ").append(qualified(table)); + sb.append(" ("); + for (int i = 0; i < columnNames.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(quoteIdentifier(columnNames.get(i))); + } + sb.append(")"); + return sb.toString(); + } + + /** {@code DROP INDEX [IF EXISTS] idx}. SQL-99 form (no {@code ON table}). */ + default String dropIndex(String indexName, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP INDEX "); + if (ifExists && supportsDropIndexIfExists()) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(indexName)); + return sb.toString(); + } + + default String dropIndex(String indexName, TableReference table, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP INDEX "); + if (ifExists && supportsDropIndexIfExists()) + sb.append("IF EXISTS "); + if (!dropIndexRequiresTable() && table != null && table.schema().isPresent()) { + sb.append(quoteIdentifier(table.schema().get().name(), indexName)); + } else { + sb.append(quoteIdentifier(indexName)); + } + if (dropIndexRequiresTable() && table != null) { + sb.append(" ON ").append(qualified(table)); + } + return sb.toString(); + } + + // -------------------- DDL — views -------------------- + + default String createView(TableReference view, String selectSql, boolean orReplace) { + if (selectSql == null || selectSql.isBlank()) { + throw new IllegalArgumentException("selectSql must not be blank for CREATE VIEW"); + } + StringBuilder sb = new StringBuilder("CREATE "); + if (orReplace && supportsCreateOrReplaceView()) + sb.append("OR REPLACE "); + sb.append("VIEW "); + sb.append(qualified(view)); + sb.append(" AS "); + sb.append(selectSql); + return sb.toString(); + } + + /** {@code DROP VIEW [IF EXISTS] schema.view}. */ + default String dropView(TableReference view, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP VIEW "); + if (ifExists && supportsDropViewIfExists()) + sb.append("IF EXISTS "); + sb.append(qualified(view)); + return sb.toString(); + } + + // -------------------- DDL — sequences -------------------- + + record SequenceDefinition(String schemaName, String name, Long startWith, Long incrementBy, Long minValue, + Long maxValue, Boolean cycle, Long cache) { + + public SequenceDefinition { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("sequence name must not be blank"); + } + } + + /** Plain unqualified sequence with default options. */ + public static SequenceDefinition of(String name) { + return new SequenceDefinition(null, name, null, null, null, null, null, null); + } + + /** Schema-qualified sequence with default options. */ + public static SequenceDefinition of(String schemaName, String name) { + return new SequenceDefinition(schemaName, name, null, null, null, null, null, null); + } + } + + default Optional createSequence(SequenceDefinition seq, boolean ifNotExists) { + if (!supportsSequences()) { + return Optional.empty(); + } + StringBuilder sb = new StringBuilder("CREATE SEQUENCE "); + if (ifNotExists) + sb.append("IF NOT EXISTS "); + sb.append(quoteIdentifier(seq.schemaName(), seq.name())); + if (seq.startWith() != null) + sb.append(" START WITH ").append(seq.startWith()); + if (seq.incrementBy() != null) + sb.append(" INCREMENT BY ").append(seq.incrementBy()); + if (seq.minValue() != null) + sb.append(" MINVALUE ").append(seq.minValue()); + if (seq.maxValue() != null) + sb.append(" MAXVALUE ").append(seq.maxValue()); + if (seq.cycle() != null) + sb.append(seq.cycle() ? " CYCLE" : " NO CYCLE"); + if (seq.cache() != null) + sb.append(" CACHE ").append(seq.cache()); + return Optional.of(sb.toString()); + } + + default Optional dropSequence(String schemaName, String name, boolean ifExists) { + if (!supportsSequences()) { + return Optional.empty(); + } + StringBuilder sb = new StringBuilder("DROP SEQUENCE "); + if (ifExists) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(schemaName, name)); + return Optional.of(sb.toString()); + } + + /** + * {@code NEXT VALUE FOR schema.name}. Returns empty on dialects without + * sequences. + */ + default Optional nextValueFor(String schemaName, String name) { + if (!supportsSequences()) { + return Optional.empty(); + } + return Optional.of("NEXT VALUE FOR " + quoteIdentifier(schemaName, name)); + } + + // -------------------- DDL — table-level constraints -------------------- + + /** + * {@code ALTER TABLE schema.table ADD CONSTRAINT name PRIMARY KEY (col1, …)}. + */ + default String addPrimaryKeyConstraint(TableReference table, String constraintName, List columnNames) { + if (columnNames == null || columnNames.isEmpty()) { + throw new IllegalArgumentException("columnNames must not be empty for PRIMARY KEY"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD CONSTRAINT ").append(quoteIdentifier(constraintName)); + sb.append(" PRIMARY KEY ("); + appendQuotedNames(sb, columnNames); + sb.append(")"); + return sb.toString(); + } + + /** {@code ALTER TABLE schema.table ADD CONSTRAINT name UNIQUE (col1, …)}. */ + default String addUniqueConstraint(TableReference table, String constraintName, List columnNames) { + if (columnNames == null || columnNames.isEmpty()) { + throw new IllegalArgumentException("columnNames must not be empty for UNIQUE"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD CONSTRAINT ").append(quoteIdentifier(constraintName)); + sb.append(" UNIQUE ("); + appendQuotedNames(sb, columnNames); + sb.append(")"); + return sb.toString(); + } + + default String addForeignKeyConstraint(TableReference table, String constraintName, List fkColumns, + TableReference referencedTable, List referencedColumns, String onDelete, String onUpdate) { + if (fkColumns == null || fkColumns.isEmpty()) { + throw new IllegalArgumentException("fkColumns must not be empty for FOREIGN KEY"); + } + if (referencedColumns == null || referencedColumns.isEmpty()) { + throw new IllegalArgumentException("referencedColumns must not be empty for FOREIGN KEY"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD CONSTRAINT ").append(quoteIdentifier(constraintName)); + sb.append(" FOREIGN KEY ("); + appendQuotedNames(sb, fkColumns); + sb.append(") REFERENCES "); + sb.append(qualified(referencedTable)); + sb.append(" ("); + appendQuotedNames(sb, referencedColumns); + sb.append(")"); + if (onDelete != null && !onDelete.isBlank()) + sb.append(" ON DELETE ").append(onDelete); + if (onUpdate != null && !onUpdate.isBlank()) + sb.append(" ON UPDATE ").append(onUpdate); + return sb.toString(); + } + + /** {@code ALTER TABLE schema.table ADD CONSTRAINT name CHECK (expression)}. */ + default String addCheckConstraint(TableReference table, String constraintName, String expression) { + if (expression == null || expression.isBlank()) { + throw new IllegalArgumentException("expression must not be blank for CHECK"); + } + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD CONSTRAINT ").append(quoteIdentifier(constraintName)); + sb.append(" CHECK (").append(expression).append(")"); + return sb.toString(); + } + + default String dropConstraint(TableReference table, String constraintName, boolean ifExists) { + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" DROP CONSTRAINT "); + if (ifExists && supportsDropConstraintIfExists()) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(constraintName)); + return sb.toString(); + } + + // -------------------- DDL — triggers -------------------- + + /** Render the SQL keyword for a {@link Trigger.TriggerTiming}. */ + static String triggerTimingKeyword(Trigger.TriggerTiming t) { + return t == Trigger.TriggerTiming.INSTEAD_OF ? "INSTEAD OF" : t.name(); + } + + default String createTrigger(String triggerName, Trigger.TriggerTiming timing, Trigger.TriggerEvent event, + TableReference table, Trigger.TriggerScope scope, String whenCondition, String body) { + return createTrigger(triggerName, timing, event, table, scope, whenCondition, body, false); + } + + default String createTrigger(String triggerName, Trigger.TriggerTiming timing, Trigger.TriggerEvent event, + TableReference table, Trigger.TriggerScope scope, String whenCondition, String body, boolean orReplace) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for CREATE TRIGGER"); + } + StringBuilder sb = new StringBuilder("CREATE "); + if (orReplace && supportsCreateOrReplaceTrigger()) + sb.append("OR REPLACE "); + sb.append("TRIGGER "); + sb.append(quoteIdentifier(triggerName)); + sb.append(' ').append(triggerTimingKeyword(timing)).append(' ').append(event); + sb.append(" ON ").append(qualified(table)); + sb.append(' ').append(scope.forEachClause()); + if (whenCondition != null && !whenCondition.isBlank()) { + sb.append(" WHEN (").append(whenCondition).append(")"); + } + sb.append(' ').append(body); + return sb.toString(); + } + + default Optional createTriggerProcedure(String procedureName, String schemaName, String body) { + return Optional.empty(); + } + + default String createTriggerUsingProcedure(String triggerName, String schemaName, Trigger.TriggerTiming timing, + Trigger.TriggerEvent event, TableReference table, Trigger.TriggerScope scope, String whenCondition, + String procedureName) { + throw new UnsupportedOperationException( + "This dialect does not support separate trigger procedures. Use createTrigger with an inline body."); + } + + default Optional dropProcedure(String procedureName, String schemaName, boolean ifExists) { + return Optional.empty(); + } + + default java.util.List dropTriggerOnTable(String triggerName, TableReference table, boolean ifExists) { + return java.util.List.of(dropTrigger(triggerName, ifExists)); + } + + /** {@code DROP TRIGGER [IF EXISTS] name}. */ + default String dropTrigger(String triggerName, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP TRIGGER "); + if (ifExists) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(triggerName)); + return sb.toString(); + } + + // -------------------- helpers -------------------- + + /** Dialect-quoted, fully-qualified {@code "schema"."table"}. */ + default String qualified(TableReference table) { + String schemaName = table.schema().map(SchemaReference::name).orElse(null); + return quoteIdentifier(schemaName, table.name()); + } + + default String nativeType(ColumnMetaData meta) { + String tn = meta.typeName(); + if (tn != null && !tn.isBlank() && !"UNKNOWN".equalsIgnoreCase(tn)) { + return applyLengthAndScale(tn, meta); + } + return defaultTypeName(meta); + } + + default List columnsOf(TableReference table, List allColumns) { + String sKey = schemaKey(table); + List out = new ArrayList<>(); + for (ColumnDefinition cd : allColumns) { + ColumnReference colRef = cd.column(); + if (colRef.table().isEmpty()) + continue; + TableReference tr = colRef.table().get(); + if (!table.name().equals(tr.name())) + continue; + if (!sKey.equals(schemaKey(tr))) + continue; + out.add(cd); + } + return out; + } + + // -------------------- private helpers -------------------- + + private void appendQuotedNames(StringBuilder sb, List names) { + for (int i = 0; i < names.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(quoteIdentifier(names.get(i))); + } + } + + private void appendWhereEqAll(StringBuilder sb, List whereColumns) { + if (whereColumns == null || whereColumns.isEmpty()) { + return; + } + sb.append(" WHERE "); + for (int i = 0; i < whereColumns.size(); i++) { + if (i > 0) + sb.append(" AND "); + sb.append(quoteIdentifier(whereColumns.get(i).column().name())); + sb.append(" = ?"); + } + } + + private void appendColumnList(StringBuilder sb, List columns) { + for (int i = 0; i < columns.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(quoteIdentifier(columns.get(i).column().name())); + } + } + + private static String applyLengthAndScale(String typeName, ColumnMetaData meta) { + if (typeName.contains("(")) + return typeName; + JDBCType jt = meta.dataType(); + if (jt == null) + return typeName; + OptionalInt size = meta.columnSize(); + OptionalInt scale = meta.decimalDigits(); + return switch (jt) { + case CHAR, VARCHAR, NCHAR, NVARCHAR, LONGVARCHAR, LONGNVARCHAR -> + size.isPresent() ? typeName + "(" + size.getAsInt() + ")" : typeName; + case NUMERIC, DECIMAL -> { + if (size.isPresent() && scale.isPresent()) { + yield typeName + "(" + size.getAsInt() + ", " + scale.getAsInt() + ")"; + } else if (size.isPresent()) { + yield typeName + "(" + size.getAsInt() + ")"; + } else { + yield typeName; + } + } + default -> typeName; + }; + } + + private static String defaultTypeName(ColumnMetaData meta) { + JDBCType jt = meta.dataType(); + OptionalInt size = meta.columnSize(); + OptionalInt scale = meta.decimalDigits(); + return switch (jt) { + case BIT, BOOLEAN -> "BOOLEAN"; + case TINYINT -> "TINYINT"; + case SMALLINT -> "SMALLINT"; + case INTEGER -> "INTEGER"; + case BIGINT -> "BIGINT"; + case FLOAT, REAL -> "REAL"; + case DOUBLE -> "DOUBLE PRECISION"; + case NUMERIC, DECIMAL -> { + if (size.isPresent() && scale.isPresent()) { + yield "DECIMAL(" + size.getAsInt() + ", " + scale.getAsInt() + ")"; + } else if (size.isPresent()) { + yield "DECIMAL(" + size.getAsInt() + ")"; + } else { + yield "DECIMAL"; + } + } + case DATE -> "DATE"; + case TIME, TIME_WITH_TIMEZONE -> "TIME"; + case TIMESTAMP, TIMESTAMP_WITH_TIMEZONE -> "TIMESTAMP"; + case CHAR -> size.isPresent() ? "CHAR(" + size.getAsInt() + ")" : "CHAR(1)"; + case VARCHAR, LONGVARCHAR, NVARCHAR, LONGNVARCHAR, NCHAR -> + size.isPresent() ? "VARCHAR(" + size.getAsInt() + ")" : "VARCHAR(255)"; + case BINARY, VARBINARY, LONGVARBINARY, BLOB -> "VARBINARY"; + case CLOB, NCLOB -> "CLOB"; + case null, default -> "VARCHAR(255)"; + }; + } + + private static String schemaKey(TableReference tbl) { + String s = tbl.schema().map(SchemaReference::name).orElse(""); + String c = tbl.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(""); + return c + "" + s; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java new file mode 100644 index 0000000..e033957 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/FunctionGenerator.java @@ -0,0 +1,160 @@ +/* + * 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.api.generator; + +public interface FunctionGenerator { + + default StringBuilder wrapIntoSqlUpperCaseFunction(CharSequence sqlExpression) { + return new StringBuilder("UPPER(").append(sqlExpression).append(")"); + } + + /** + * @param thenExpression value when condition is true + * @param elseExpression value when condition is false + */ + default StringBuilder wrapIntoSqlIfThenElseFunction(CharSequence condition, CharSequence thenExpression, + CharSequence elseExpression) { + return new StringBuilder("CASE WHEN ").append(condition).append(" THEN ").append(thenExpression) + .append(" ELSE ").append(elseExpression).append(" END"); + } + + + /** + * Wraps an expression in the SQL {@code GROUPING(...)} super-aggregate function, used + * together with {@code GROUP BY GROUPING SETS}/{@code ROLLUP} to detect which grouping + * columns contributed to a given output row. + * + * @param expression the grouping column expression + * @return the {@code GROUPING(expression)} fragment + */ + default StringBuilder generateGrouping(CharSequence expression) { + return new StringBuilder("GROUPING(").append(expression).append(")"); + } + + /** + * Renders a portable {@link KnownFunction} intent in this dialect's spelling. + * + *

+ * The default implementation emits the most portable (ANSI where possible) + * spelling; dialects override only the functions whose spelling differs. + * + * @param function the portable function intent + * @param arguments the already-rendered SQL argument expressions + * @return the rendered function call + * @throws IllegalArgumentException if the argument count does not match the + * function's arity + */ + default StringBuilder generateKnownFunction(KnownFunction function, java.util.List arguments) { + return switch (function) { + case SUBSTRING -> { + checkArity(function, arguments, 2, 3); + StringBuilder sb = new StringBuilder("SUBSTRING(").append(arguments.get(0)).append(", ") + .append(arguments.get(1)); + if (arguments.size() == 3) { + sb.append(", ").append(arguments.get(2)); + } + yield sb.append(")"); + } + case LENGTH -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("CHAR_LENGTH(").append(arguments.get(0)).append(")"); + } + case CONCAT -> { + checkArity(function, arguments, 2, Integer.MAX_VALUE); + StringBuilder sb = new StringBuilder("("); + for (int i = 0; i < arguments.size(); i++) { + if (i > 0) { + sb.append(" || "); + } + sb.append(arguments.get(i)); + } + yield sb.append(")"); + } + case INDEX_OF -> { + checkArity(function, arguments, 2, 2); + yield new StringBuilder("POSITION(").append(arguments.get(0)).append(" IN ").append(arguments.get(1)) + .append(")"); + } + case TRIM -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("TRIM(").append(arguments.get(0)).append(")"); + } + case LTRIM -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("TRIM(LEADING FROM ").append(arguments.get(0)).append(")"); + } + case RTRIM -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("TRIM(TRAILING FROM ").append(arguments.get(0)).append(")"); + } + case YEAR, MONTH, DAY, HOUR, MINUTE, SECOND -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("EXTRACT(").append(function.name()).append(" FROM ").append(arguments.get(0)) + .append(")"); + } + case DATE -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS DATE)"); + } + case TIME -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("CAST(").append(arguments.get(0)).append(" AS TIME)"); + } + case ROUND -> { + checkArity(function, arguments, 1, 2); + StringBuilder sb = new StringBuilder("ROUND(").append(arguments.get(0)); + if (arguments.size() == 2) { + sb.append(", ").append(arguments.get(1)); + } + yield sb.append(")"); + } + case FLOOR, ABS, SQRT -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder(function.name()).append("(").append(arguments.get(0)).append(")"); + } + case CEILING -> { + checkArity(function, arguments, 1, 1); + yield new StringBuilder("CEILING(").append(arguments.get(0)).append(")"); + } + case MOD, POWER -> { + checkArity(function, arguments, 2, 2); + yield new StringBuilder(function.name()).append("(").append(arguments.get(0)).append(", ") + .append(arguments.get(1)).append(")"); + } + case NOW -> { + checkArity(function, arguments, 0, 0); + yield new StringBuilder("CURRENT_TIMESTAMP"); + } + }; + } + + /** + * Validates the argument count for {@link #generateKnownFunction}. + * + * @throws IllegalArgumentException if {@code arguments.size()} is outside + * {@code [min, max]} + */ + private static void checkArity(KnownFunction function, java.util.List arguments, int min, + int max) { + int size = arguments.size(); + if (size < min || size > max) { + String expected; + if (min == max) { + expected = String.valueOf(min); + } else if (max == Integer.MAX_VALUE) { + expected = min + " or more"; + } else { + expected = min + ".." + max; + } + throw new IllegalArgumentException( + function.name() + " expects " + expected + " argument(s), got " + size); + } + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/HintGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/HintGenerator.java new file mode 100644 index 0000000..a3e5d8e --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/HintGenerator.java @@ -0,0 +1,49 @@ +/* + * 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.api.generator; + +import java.util.List; +import java.util.Map; + +public interface HintGenerator { + + /** + * @param buf buffer being built (the hint, if any, is appended here) + * @param hints map of hint name → value (semantics dialect-specific) + */ + default void appendHintsAfterFromClause(StringBuilder buf, Map hints) { + // dialect-specific; default no-op + } + + /** + * The optimizer-hint block placed directly after the {@code SELECT} keyword + * (Oracle/MySQL {@code /*+ ... *}{@code /} style), including a trailing space when + * non-empty. + * + * @param hints the statement-level hint intents + * @return the hint block, or an empty builder when this dialect has no such block + * (the default: the hints are silently ignored) + */ + default StringBuilder selectHint(List hints) { + return new StringBuilder(); + } + + /** + * The trailing statement option clause (SQL Server {@code OPTION (...)} style), + * including a leading space when non-empty. + * + * @param hints the statement-level hint intents + * @return the option clause, or an empty builder when this dialect has no such + * clause (the default: the hints are silently ignored) + */ + default StringBuilder statementOption(List hints) { + return new StringBuilder(); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/IdentifierQuoter.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/IdentifierQuoter.java new file mode 100644 index 0000000..09a2972 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/IdentifierQuoter.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.type.QuoteStyle; + +public interface IdentifierQuoter { + + String quoteIdentifier(CharSequence val); + + /** @param val identifier to quote (must not be null) */ + void quoteIdentifier(String val, StringBuilder buf); + + /** + * @param qual qualifier (schema name); if not null, prepended with separator + */ + String quoteIdentifier(String qual, String name); + + /** @param names list of names to be quoted */ + void quoteIdentifier(StringBuilder buf, String... names); + + String getQuoteIdentifierString(); + + /** + * @param val identifier to quote conditionally + * @return either {@code val} unquoted (as a fresh {@code StringBuilder}) or the + * result of {@link #quoteIdentifier(CharSequence)} + */ + default String quoteIdentifierIfNeeded(CharSequence val) { + return quoteIdentifier(val); + } + + /** + * @param policy desired quoting style for this single call; {@code null} is + * treated as {@link IdentifierQuotingPolicy#ALWAYS} + */ + default String quoteIdentifierWith(CharSequence val, IdentifierQuotingPolicy policy) { + if (val == null) { + return ""; + } + IdentifierQuotingPolicy p = (policy == null) ? IdentifierQuotingPolicy.ALWAYS : policy; + return switch (p) { + case ALWAYS -> quoteIdentifier(val); + case WHEN_NEEDED -> quoteIdentifierIfNeeded(val); + case NEVER -> val.toString(); + }; + } + + default void quoteIdentifierWith(String val, StringBuilder buf, IdentifierQuotingPolicy policy) { + if (val == null) { + return; + } + buf.append(quoteIdentifierWith(val, policy)); + } + + default void quoteIdentifierWith(StringBuilder buf, IdentifierQuotingPolicy policy, String... names) { + int nonNull = 0; + for (String name : names) { + if (name == null) { + continue; + } + if (nonNull > 0) { + buf.append('.'); + } + quoteIdentifierWith(name, buf, policy); + ++nonNull; + } + } + + /** + * @param buf buffer to append to (must not be null) + * @param identifiers identifiers to quote (must not be null; may be empty) + */ + default void appendQuotedCsv(StringBuilder buf, List identifiers) { + boolean first = true; + for (String id : identifiers) { + if (!first) + buf.append(", "); + buf.append(quoteIdentifier(id)); + first = false; + } + } + + default QuoteStyle getQuoteStyle() { + String quote = getQuoteIdentifierString(); + if (quote == null || quote.isEmpty()) { + return QuoteStyle.NONE; + } + return switch (quote) { + case "\"" -> QuoteStyle.DOUBLE_QUOTE; + case "`" -> QuoteStyle.BACKTICK; + case "[" -> QuoteStyle.SQUARE_BRACKET; + default -> QuoteStyle.DOUBLE_QUOTE; + }; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/KnownFunction.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/KnownFunction.java new file mode 100644 index 0000000..4a747fe --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/KnownFunction.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +/** + * Portable well-known SQL function intent. + * + *

+ * Each constant names what the caller wants computed, never how a particular + * database spells it: the concrete spelling (function name, argument order, + * {@code EXTRACT} vs. {@code DATEPART}, ...) is the job of the dialect's + * {@link FunctionGenerator}. + * + *

+ * Upper/lower case folding is deliberately absent from this vocabulary: case + * folding already has dedicated dialect hooks (identifier {@code CaseFold} and + * {@link FunctionGenerator#wrapIntoSqlUpperCaseFunction(CharSequence)}). + */ +public enum KnownFunction { + + /** Substring extraction: {@code SUBSTRING(string, start[, length])}; 2 or 3 arguments. */ + SUBSTRING, + + /** Character length of a string; 1 argument. */ + LENGTH, + + /** String concatenation; 2 or more arguments. */ + CONCAT, + + /** 1-based position of a needle inside a haystack: {@code INDEX_OF(needle, haystack)}; 2 arguments. */ + INDEX_OF, + + /** Strip leading and trailing whitespace; 1 argument. */ + TRIM, + + /** Strip leading whitespace; 1 argument. */ + LTRIM, + + /** Strip trailing whitespace; 1 argument. */ + RTRIM, + + /** Year part of a date/time value; 1 argument. */ + YEAR, + + /** Month part of a date/time value; 1 argument. */ + MONTH, + + /** Day-of-month part of a date/time value; 1 argument. */ + DAY, + + /** Hour part of a time value; 1 argument. */ + HOUR, + + /** Minute part of a time value; 1 argument. */ + MINUTE, + + /** Second part of a time value; 1 argument. */ + SECOND, + + /** Date part of a date/time value; 1 argument. */ + DATE, + + /** Time part of a date/time value; 1 argument. */ + TIME, + + /** Numeric rounding: {@code ROUND(value[, digits])}; 1 or 2 arguments. */ + ROUND, + + /** Largest integer not greater than the value; 1 argument. */ + FLOOR, + + /** Smallest integer not less than the value; 1 argument. */ + CEILING, + + /** Absolute value; 1 argument. */ + ABS, + + /** Remainder of an integer division: {@code MOD(dividend, divisor)}; 2 arguments. */ + MOD, + + /** Exponentiation: {@code POWER(base, exponent)}; 2 arguments. */ + POWER, + + /** Square root; 1 argument. */ + SQRT, + + /** Current date and time; no arguments. */ + NOW +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/LiteralQuoter.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/LiteralQuoter.java new file mode 100644 index 0000000..8fb08b2 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/LiteralQuoter.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import org.eclipse.daanse.sql.model.type.Datatype; + +public interface LiteralQuoter { + + void quoteStringLiteral(StringBuilder buf, String s); + + void quoteNumericLiteral(StringBuilder buf, String value); + + void quoteBooleanLiteral(StringBuilder buf, String value); + + /** @param value literal value in ISO format */ + void quoteDateLiteral(StringBuilder buf, String value); + + /** @param value literal value in ISO format */ + void quoteTimeLiteral(StringBuilder buf, String value); + + /** @param value literal value in ISO format */ + void quoteTimestampLiteral(StringBuilder buf, String value); + + StringBuilder quoteDecimalLiteral(CharSequence value); + + /** @param datatype the SQL datatype of the value */ + void quote(StringBuilder buf, Object value, Datatype datatype); + + default char likeEscapeChar() { + return '\\'; + } + + /** @param value plain-text value to embed verbatim into a LIKE pattern */ + default void quoteLikeLiteral(StringBuilder buf, String value) { + if (value == null) { + quoteStringLiteral(buf, null); + return; + } + char esc = likeEscapeChar(); + StringBuilder escaped = new StringBuilder(value.length() + 8); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%' || c == '_' || c == esc) { + escaped.append(esc); + } + escaped.append(c); + } + quoteStringLiteral(buf, escaped.toString()); + } + + /** @param bytes binary payload (must not be null) */ + default void quoteBinaryLiteral(StringBuilder buf, byte[] bytes) { + if (bytes == null) { + throw new IllegalArgumentException("bytes must not be null"); + } + buf.append("X'"); + for (byte b : bytes) { + int v = b & 0xff; + buf.append(Character.forDigit((v >>> 4) & 0xf, 16)); + buf.append(Character.forDigit(v & 0xf, 16)); + } + buf.append('\''); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/MergeGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/MergeGenerator.java new file mode 100644 index 0000000..eca4614 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/MergeGenerator.java @@ -0,0 +1,58 @@ +/* + * 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/ + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +public interface MergeGenerator { + + /** + * Identifier columns + columns to update on match + columns to insert on miss. + */ + record UpsertSpec(TableReference target, List keyColumns, List insertColumns, + List updateColumns) { + public UpsertSpec { + if (target == null) + throw new IllegalArgumentException("target"); + if (keyColumns == null || keyColumns.isEmpty()) { + throw new IllegalArgumentException("keyColumns must not be empty"); + } + if (insertColumns == null || insertColumns.isEmpty()) { + throw new IllegalArgumentException("insertColumns must not be empty"); + } + // updateColumns may be empty (insert-or-ignore semantics). + } + } + + /** + * @return the upsert SQL, or empty if this dialect doesn't support the + * requested shape (insert-or-ignore vs insert-or-update) + */ + default Optional upsert(UpsertSpec spec, List values) { + return Optional.empty(); + } + + default boolean supportsMerge() { + return false; + } + + /** + * @param values one row of values, ordered like {@code spec.insertColumns()} + * @throws IllegalArgumentException if {@code values} doesn't match the spec + */ + static void requireValuesMatch(UpsertSpec spec, List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException( + "values must match insertColumns: expected " + spec.insertColumns().size() + ", got " + + (values == null ? "null" : String.valueOf(values.size()))); + } + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/OrderByGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/OrderByGenerator.java new file mode 100644 index 0000000..be82637 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/OrderByGenerator.java @@ -0,0 +1,58 @@ +/* + * 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.api.generator; + +import org.eclipse.daanse.sql.model.type.Datatype; + +public interface OrderByGenerator extends LiteralQuoter { + + String CASE_WHEN = "CASE WHEN "; + String DESC = " DESC"; + String ASC = " ASC"; + + /** + * @param nullable whether the expression can be null + * @param ascending true for ASC, false for DESC + * @param collateNullsLast true to put NULLs last, false for NULLs first + */ + default StringBuilder generateOrderItem(CharSequence expr, boolean nullable, boolean ascending, + boolean collateNullsLast) { + if (nullable) { + return generateOrderByNulls(expr, ascending, collateNullsLast); + } + return new StringBuilder(expr).append(ascending ? ASC : DESC); + } + + /** + * @param orderValue sentinel value (quoted via {@link #quote}) + * @param datatype datatype of {@code orderValue} for proper quoting + * @param ascending true for ASC, false for DESC + * @param collateNullsLast true to put the sentinel last, false for first + */ + default StringBuilder generateOrderItemForOrderValue(CharSequence expr, String orderValue, Datatype datatype, + boolean ascending, boolean collateNullsLast) { + StringBuilder sb = new StringBuilder(CASE_WHEN).append(expr).append(" = "); + quote(sb, orderValue, datatype); + sb.append(collateNullsLast ? " THEN 1 ELSE 0 END, " : " THEN 0 ELSE 1 END, ").append(expr); + return sb.append(ascending ? ASC : DESC); + } + + default StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + StringBuilder sb = new StringBuilder(CASE_WHEN).append(expr) + .append(collateNullsLast ? " IS NULL THEN 1 ELSE 0 END, " : " IS NULL THEN 0 ELSE 1 END, ") + .append(expr); + return sb.append(ascending ? ASC : DESC); + } + + default StringBuilder generateOrderByNullsAnsi(CharSequence expr, boolean ascending, boolean collateNullsLast) { + return new StringBuilder(expr).append(ascending ? ASC : DESC) + .append(collateNullsLast ? " NULLS LAST" : " NULLS FIRST"); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java new file mode 100644 index 0000000..d06b774 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/PaginationGenerator.java @@ -0,0 +1,64 @@ +/* + * 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.api.generator; + +import java.util.Optional; +import java.util.OptionalLong; + +public interface PaginationGenerator { + + /** + * Limit fragment that must be placed immediately after {@code SELECT [DISTINCT]}, used by + * dialects that express row limits as a leading clause (e.g. SQL Server / Sybase + * {@code TOP n}) rather than a trailing clause. + *

+ * The default is {@link Optional#empty()}, meaning the dialect uses the trailing + * {@link #paginate(OptionalLong, OptionalLong)} form instead. A dialect must implement + * either this leading form or the trailing form, never both for the + * same query. + * + * @param limit maximum rows to return (must be ≥ 0 if present) + * @param offset rows to skip (must be ≥ 0 if present) + * @return the {@code SELECT}-prefix fragment (without trailing space), or empty when the + * dialect paginates with a trailing clause + */ + default Optional selectPrefix(OptionalLong limit, OptionalLong offset) { + return Optional.empty(); + } + + /** + * @param limit maximum rows to return (must be ≥ 0 if present) + * @param offset rows to skip (must be ≥ 0 if present) + * @return suffix to append after {@code ORDER BY …} (may be empty, never null) + */ + default String paginate(OptionalLong limit, OptionalLong offset) { + StringBuilder sb = new StringBuilder(); + offset.ifPresent(o -> { + if (o < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" OFFSET ").append(o).append(" ROWS"); + }); + limit.ifPresent(l -> { + if (l < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" FETCH NEXT ").append(l).append(" ROWS ONLY"); + }); + return sb.toString(); + } + + default boolean supportsOffset() { + return true; + } + + /** Convenience for callers that always want to paginate. */ + default Optional paginate(long limit) { + return Optional.of(paginate(OptionalLong.of(limit), OptionalLong.empty())); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ParameterPlaceholderGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ParameterPlaceholderGenerator.java new file mode 100644 index 0000000..bec6903 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ParameterPlaceholderGenerator.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +public interface ParameterPlaceholderGenerator { + + /** + * @param n one-based parameter index (must be ≥ 1) + * @return the placeholder marker (never null/empty) + */ + default String placeholder(int n) { + if (n < 1) + throw new IllegalArgumentException("parameter index must be >= 1"); + return "?"; + } + + default boolean placeholdersAreIndexed() { + return false; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/RegexGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/RegexGenerator.java new file mode 100644 index 0000000..3336a56 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/RegexGenerator.java @@ -0,0 +1,53 @@ +/* + * 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.api.generator; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public interface RegexGenerator { + + /** {@code "^(\(\?([a-zA-Z]+)\)).*$"} — Java embedded-flag prefix capture. */ + String FLAGS_REGEXP = "^(\\(\\?([a-zA-Z]+)\\)).*$"; + + /** Compiled form of {@link #FLAGS_REGEXP}. */ + Pattern FLAGS_PATTERN = Pattern.compile(FLAGS_REGEXP); + + /** + * @param source source column or expression to match against + * @param javaRegExp Java regular expression pattern (may contain leading + * {@code (?flags)}) + * @return regex match expression, or empty if not supported + */ + default Optional generateRegularExpression(String source, String javaRegExp) { + return Optional.empty(); + } + + /** + * @param javaRegex Java regex possibly starting with {@code (?flags)} + * @param mapping flag-letter translation table (Java → dialect) + * @param dialectFlags receives the translated dialect flags (appended to) + * @return {@code javaRegex} with the Java flag prefix removed + */ + default String extractEmbeddedFlags(String javaRegex, String[][] mapping, StringBuilder dialectFlags) { + final Matcher flagsMatcher = FLAGS_PATTERN.matcher(javaRegex); + if (flagsMatcher.matches()) { + final String flags = flagsMatcher.group(2); + for (String[] flag : mapping) { + if (flags.contains(flag[0])) { + dialectFlags.append(flag[1]); + } + } + javaRegex = javaRegex.substring(0, flagsMatcher.start(1)) + javaRegex.substring(flagsMatcher.end(1)); + } + return javaRegex; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ReturningGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ReturningGenerator.java new file mode 100644 index 0000000..dabf7ee --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/ReturningGenerator.java @@ -0,0 +1,23 @@ +/* + * 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/ + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; +import java.util.Optional; + +public interface ReturningGenerator { + + default Optional returning(List columns) { + return Optional.empty(); + } + + /** Whether this dialect supports any form of RETURNING/OUTPUT clause. */ + default boolean supportsReturning() { + return false; + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/SqlGenerator.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/SqlGenerator.java new file mode 100644 index 0000000..129fd9c --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/SqlGenerator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; +import java.util.Map; + +import org.eclipse.daanse.sql.model.type.Datatype; + +public interface SqlGenerator { + + /** + * @param columnNames names of the columns in the inline table + * @param valueList list of rows, each row being an array of values + */ + StringBuilder generateInline(List columnNames, List columnTypes, List valueList); + +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/StatementHint.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/StatementHint.java new file mode 100644 index 0000000..54d3ab9 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/StatementHint.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.generator; + +import java.util.List; + +/** + * A statement-level optimizer hint intent — the vocabulary the + * {@link HintGenerator} spells (the hint-level sibling of {@link KnownFunction}). + * + *

+ * A hint expresses what the caller wants the optimizer to do, never how a + * particular database writes it: dialects that have a matching construct spell + * it ({@code /*+ ... *}{@code /} block, {@code OPTION (...)} clause, ...); + * dialects without one silently ignore it. + * + * @param name the hint name (e.g. {@code MAX_EXECUTION_TIME}, {@code RECOMPILE}) + * @param arguments the hint arguments, possibly empty (e.g. {@code ["1000"]}) + */ +public record StatementHint(String name, List arguments) { + + public StatementHint { + arguments = List.copyOf(arguments); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/package-info.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/package-info.java new file mode 100644 index 0000000..1be1eec --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/generator/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.api.generator; diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/package-info.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/package-info.java new file mode 100644 index 0000000..c57f082 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.api; diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/QuoteStyle.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/QuoteStyle.java new file mode 100644 index 0000000..7d0396c --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/QuoteStyle.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.type; + +/** Identifier quote characters supported by JDBC engines. */ +public enum QuoteStyle { + + /** ANSI standard double quotes ({@code "name"}) — Postgres, Oracle, Db2. */ + DOUBLE_QUOTE("\"", "\""), + + /** MySQL/MariaDB-style backticks ({@code `name`}). */ + BACKTICK("`", "`"), + + /** SQL Server / Sybase delimited identifiers ({@code [name]}). */ + SQUARE_BRACKET("[", "]"), + + /** No quoting — emitted SQL exposes the identifier verbatim. */ + NONE("", ""); + + private final String openQuote; + private final String closeQuote; + + QuoteStyle(String openQuote, String closeQuote) { + this.openQuote = openQuote; + this.closeQuote = closeQuote; + } + + /** @return the opening quote character (empty for {@link #NONE}) */ + public String openQuote() { + return openQuote; + } + + /** @return the closing quote character (empty for {@link #NONE}) */ + public String closeQuote() { + return closeQuote; + } + + /** + * @param identifier identifier to wrap; may be {@code null} + * @return identifier wrapped in this style's quotes; the input unchanged when + * this style is {@link #NONE} or {@code identifier} is {@code null} + */ + public String quote(String identifier) { + if (this == NONE || identifier == null) { + return identifier; + } + return openQuote + identifier + closeQuote; + } + + /** + * @param buf the {@link StringBuilder} to append to + * @param identifier identifier to append; {@code null} appends nothing + */ + public void quote(StringBuilder buf, String identifier) { + if (identifier == null) { + return; + } + buf.append(openQuote).append(identifier).append(closeQuote); + } +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/TypeMapper.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/TypeMapper.java new file mode 100644 index 0000000..b107eab --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/TypeMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.api.type; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import org.eclipse.daanse.sql.model.type.BestFitColumnType; + +public interface TypeMapper { + + /** + * @return the best Java type for reading this column + * @throws SQLException if metadata cannot be retrieved + */ + BestFitColumnType getType(ResultSetMetaData metaData, int columnIndex) throws SQLException; + + /** + * @param valueString the string representation of the value + * @return true if an exponent suffix is needed + */ + boolean needsExponent(Object value, String valueString); + + /** + * @param type ResultSet type (e.g., TYPE_FORWARD_ONLY, + * TYPE_SCROLL_INSENSITIVE) + * @param concurrency ResultSet concurrency (e.g., CONCUR_READ_ONLY, + * CONCUR_UPDATABLE) + * @return true if the combination is supported + */ + boolean supportsResultSetConcurrency(int type, int concurrency); +} diff --git a/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/package-info.java b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/package-info.java new file mode 100644 index 0000000..be2cc93 --- /dev/null +++ b/dialect/api/src/main/java/org/eclipse/daanse/sql/dialect/api/type/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.api.type; diff --git a/dialect/db/clickhouse/pom.xml b/dialect/db/clickhouse/pom.xml new file mode 100644 index 0000000..8a66823 --- /dev/null +++ b/dialect/db/clickhouse/pom.xml @@ -0,0 +1,50 @@ + + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.clickhouse + Eclipse Daanse JDBC DB Dialect ClickHouse + ClickHouse database dialect implementation providing + ClickHouse-specific SQL generation, query optimization, and analytical + database feature handling. Supports ClickHouse columnar storage, distributed + queries, and high-performance analytical operations for OLAP workloads. + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + diff --git a/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java new file mode 100644 index 0000000..308368f --- /dev/null +++ b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialect.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.clickhouse; + +import java.util.List; + +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; + +public class ClickHouseDialect extends AbstractJdbcDialect { + + /** ClickHouse rejects plain {@code CREATE INDEX} without a data-skipping {@code TYPE} + * clause (INCORRECT_QUERY 80) — b-tree index DDL is not expressible. */ + @Override + public boolean supportsIndexDdl() { + return false; + } + + private static final String SUPPORTED_PRODUCT_NAME = "CLICKHOUSE"; + + /** JDBC-free constructor for SQL generation. */ + public ClickHouseDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public ClickHouseDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public boolean supportsSequences() { + return false; + } + + @Override + public boolean supportsDropConstraintIfExists() { + return false; + } + + @Override + public boolean supportsCreateOrReplaceView() { + return false; + } + + @Override + public boolean requiresDrillthroughMaxRowsInLimit() { + return true; + } + + /** + * ClickHouse rejects a bare {@code UNION} when the server-side + * {@code union_default_mode} setting is empty (the default): + * {@code Code: 558 DB::Exception: Expected ALL or DISTINCT in SelectWithUnion + * query}. Spell the duplicate-eliminating union explicitly. + */ + @Override + public String unionDistinctKeyword() { + return "union distinct"; + } + + @Override + public void quoteStringLiteral(StringBuilder buf, String s) { + buf.append('\''); + + String s0 = s.replace("\\", "\\\\"); + s0 = s0.replace("'", "\\'"); + buf.append(s0); + + buf.append('\''); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + // Unified BitOperation methods + + @Override + public java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + StringBuilder buf = new StringBuilder(64); + StringBuilder result = switch (operation) { + case AND -> buf.append("groupBitAnd(").append(operand).append(")"); + case OR -> buf.append("groupBitOr(").append(operand).append(")"); + case XOR -> buf.append("groupBitXor(").append(operand).append(")"); + case NAND -> buf.append("NOT(groupBitAnd(").append(operand).append("))"); + case NOR -> buf.append("NOT(groupBitOr(").append(operand).append("))"); + case NXOR -> buf.append("NOT(groupBitXor(").append(operand).append("))"); + }; + return java.util.Optional.of(result.toString()); + } + + @Override + public boolean supportsBitAggregation(BitOperation operation) { + return true; // ClickHouse supports all bit operations + } + + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + StringBuilder buf = new StringBuilder(64); + buf.append("groupArrayArray"); + buf.append("( "); + buf.append(operand); + buf.append(")"); + // groupArrayArray(page_visits) + return java.util.Optional.of((buf).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("nth_value", operand, ignoreNulls, n, columns, false)).toString()); + } + + @Override + public boolean supportsNthValue() { + return true; + } + + @Override + public boolean supportsListAgg() { + return true; + } +} diff --git a/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectFactory.java b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectFactory.java new file mode 100644 index 0000000..90d941f --- /dev/null +++ b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.clickhouse; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("CLICKHOUSE") +public class ClickHouseDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return ClickHouseDialect::new; + } + +} diff --git a/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/package-info.java b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/package-info.java new file mode 100644 index 0000000..3bdf929 --- /dev/null +++ b/dialect/db/clickhouse/src/main/java/org/eclipse/daanse/sql/dialect/db/clickhouse/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.clickhouse; diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectQuotingPolicyTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectQuotingPolicyTest.java new file mode 100644 index 0000000..eefab8f --- /dev/null +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * 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.clickhouse; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class ClickHouseDialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private ClickHouseDialect dialectNever() { + ClickHouseDialect d = new ClickHouseDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("`").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("`").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("`").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("`").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("`").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("`") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java new file mode 100644 index 0000000..8547891 --- /dev/null +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/ClickHouseDialectTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.clickhouse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ClickHouseDialectTest { + + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private ClickHouseDialect dialect; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("ClickHouse"); + when(metaData.getDatabaseProductVersion()).thenReturn("23.0"); + dialect = new ClickHouseDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + } + + @Test + void testGetDialectName() { + assertEquals("clickhouse", dialect.name()); + } + + @Test + void testRequiresDrillthroughMaxRowsInLimit() { + assertTrue(dialect.requiresDrillthroughMaxRowsInLimit()); + } + + @Test + void capability_flags_reflect_clickhouse_constraints() { + // ClickHouse: no FK / sequences, no DROP CONSTRAINT IF EXISTS, no + // CREATE OR REPLACE VIEW. Most other IF EXISTS forms ARE supported. + assertFalse(dialect.supportsSequences()); + assertFalse(dialect.supportsDropConstraintIfExists()); + assertFalse(dialect.supportsCreateOrReplaceView()); + assertTrue(dialect.supportsDropTableIfExists()); + assertTrue(dialect.supportsCreateTableIfNotExists()); + } + + @Nested + @DisplayName("String Literal Quoting Tests") + class StringLiteralQuotingTests { + + @Test + void testQuoteStringLiteral_Simple() { + StringBuilder buf = new StringBuilder(); + dialect.quoteStringLiteral(buf, "test"); + assertEquals("'test'", buf.toString()); + } + + @Test + void testQuoteStringLiteral_WithSingleQuote() { + StringBuilder buf = new StringBuilder(); + dialect.quoteStringLiteral(buf, "don't"); + assertEquals("'don\\'t'", buf.toString()); + } + + @Test + void testQuoteStringLiteral_WithBackslash() { + StringBuilder buf = new StringBuilder(); + dialect.quoteStringLiteral(buf, "path\\to\\file"); + assertEquals("'path\\\\to\\\\file'", buf.toString()); + } + + @Test + void testQuoteStringLiteral_WithBothQuoteAndBackslash() { + StringBuilder buf = new StringBuilder(); + dialect.quoteStringLiteral(buf, "it's a\\path"); + assertEquals("'it\\'s a\\\\path'", buf.toString()); + } + } + + @Nested + @DisplayName("Bit Aggregation Tests") + class BitAggregationTests { + + @ParameterizedTest + @EnumSource(BitOperation.class) + void testSupportsBitAggregation(BitOperation operation) { + assertTrue(dialect.supportsBitAggregation(operation)); + } + + @Test + void testGenerateBitAggregation_AND() { + String result = dialect.generateBitAggregation(BitOperation.AND, "column1").orElseThrow(); + assertEquals("groupBitAnd(column1)", result); + } + + @Test + void testGenerateBitAggregation_OR() { + String result = dialect.generateBitAggregation(BitOperation.OR, "column1").orElseThrow(); + assertEquals("groupBitOr(column1)", result); + } + + @Test + void testGenerateBitAggregation_XOR() { + String result = dialect.generateBitAggregation(BitOperation.XOR, "column1").orElseThrow(); + assertEquals("groupBitXor(column1)", result); + } + + @Test + void testGenerateBitAggregation_NAND() { + String result = dialect.generateBitAggregation(BitOperation.NAND, "column1").orElseThrow(); + assertEquals("NOT(groupBitAnd(column1))", result); + } + + @Test + void testGenerateBitAggregation_NOR() { + String result = dialect.generateBitAggregation(BitOperation.NOR, "column1").orElseThrow(); + assertEquals("NOT(groupBitOr(column1))", result); + } + + @Test + void testGenerateBitAggregation_NXOR() { + String result = dialect.generateBitAggregation(BitOperation.NXOR, "column1").orElseThrow(); + assertEquals("NOT(groupBitXor(column1))", result); + } + } + + @Nested + @DisplayName("Window Function Tests") + class WindowFunctionTests { + + @Test + void testSupportsNthValue() { + assertTrue(dialect.supportsNthValue()); + } + + @Test + void testSupportsListAgg() { + assertTrue(dialect.supportsListAgg()); + } + + @Test + void testGenerateListAgg() { + String result = dialect.generateListAgg("column1", false, null, null, null, null).orElseThrow(); + assertEquals("groupArrayArray( column1)", result); + } + } +} diff --git a/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/integration/ServiceTest.java b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/integration/ServiceTest.java new file mode 100644 index 0000000..251adbf --- /dev/null +++ b/dialect/db/clickhouse/src/test/java/org/eclipse/daanse/sql/dialect/db/clickhouse/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.clickhouse.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.clickhouse.ClickHouseDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(ClickHouseDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/common/pom.xml b/dialect/db/common/pom.xml new file mode 100644 index 0000000..7f57b24 --- /dev/null +++ b/dialect/db/common/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.common + Daanse JDBC DB Dialect Common + Common functionality and base classes for database dialect + implementations. Provides shared utilities, base dialect classes, and common + SQL generation patterns used across different database-specific dialect + implementations. + + + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractDialectFactory.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractDialectFactory.java new file mode 100644 index 0000000..e105671 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractDialectFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import java.util.Locale; +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; + +public abstract class AbstractDialectFactory implements DialectFactory { + + @Override + public Dialect createDialect(DialectInitData init) { + return getConstructorFunction().apply(init); + } + + /** + * @param productSubstring lower-cased substring to look for in + * {@link DialectInitData#productName()} + * @param init captured snapshot + * @return true if the substring is contained in the (case-insensitive) product + * name + */ + protected static boolean isDatabase(String productSubstring, DialectInitData init) { + String name = init.productName(); + if (name == null) + return false; + return name.toLowerCase(Locale.ROOT).contains(productSubstring.toLowerCase(Locale.ROOT)); + } + + public abstract Function getConstructorFunction(); +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java new file mode 100644 index 0000000..3e7b070 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialect.java @@ -0,0 +1,585 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import java.sql.DatabaseMetaData; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.capability.AggregateCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.JoinCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.OrderByCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.WindowFunctionCapabilities; +import org.eclipse.daanse.sql.dialect.api.generator.AggregationGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.FunctionGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.HintGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.OrderByGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.RegexGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.SqlGenerator; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author jhyde + * @since Oct 10, 2008 + */ +public abstract class AbstractJdbcDialect implements Dialect, SqlGenerator, DdlGenerator, OrderByGenerator, + RegexGenerator, AggregationGenerator, FunctionGenerator, HintGenerator { + + @Override + public SqlGenerator sqlGenerator() { + return this; + } + + @Override + public DdlGenerator ddlGenerator() { + return this; + } + + @Override + public OrderByGenerator orderByGenerator() { + return this; + } + + @Override + public RegexGenerator regexGenerator() { + return this; + } + + @Override + public AggregationGenerator aggregationGenerator() { + return this; + } + + @Override + public FunctionGenerator functionGenerator() { + return this; + } + + @Override + public HintGenerator hintGenerator() { + return this; + } + + // SQL keyword constants moved with their consumers: ORDER_BY/WITHIN_GROUP/ + // OVER/IGNORE_NULLS/RESPECT_NULLS/COMMA/OPEN_PAREN/CLOSE_PAREN/DESC live on + // AggregationGenerator; CASE_WHEN/DESC/ASC live on OrderByGenerator. + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractJdbcDialect.class); + + private static final int[] RESULT_SET_TYPE_VALUES = { ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE }; + + private static final int[] CONCURRENCY_VALUES = { ResultSet.CONCUR_READ_ONLY, ResultSet.CONCUR_UPDATABLE }; + private String quoteIdentifierString = ""; + + private String productName = ""; + + protected String productVersion = ""; + + protected org.eclipse.daanse.sql.dialect.api.DialectVersion dialectVersion = org.eclipse.daanse.sql.dialect.api.DialectVersion.UNKNOWN; + + private Set> supportedResultSetTypes = null; + + private boolean readOnly = true; + + private int maxColumnNameLength = 0; + + private IdentifierQuotingPolicy quotingPolicy = IdentifierQuotingPolicy.ALWAYS; + + private Set sqlKeywordsLower = Set.of(); + + private final JdbcCapabilityFlags caps = new JdbcCapabilityFlags(this); + private final JdbcDdlEmitter ddl = new JdbcDdlEmitter(this); + private final JdbcLiteralQuoter literals = new JdbcLiteralQuoter(new JdbcLiteralQuoter.Hooks() { + @Override + public void quoteDateLiteral(StringBuilder buf, java.sql.Date date) { + AbstractJdbcDialect.this.quoteDateLiteral(buf, date); + } + + @Override + public void quoteTimestampLiteral(StringBuilder buf, String value, java.sql.Timestamp timestamp) { + AbstractJdbcDialect.this.quoteTimestampLiteral(buf, value, timestamp); + } + }); + private final JdbcInlineDataGenerator inline = new JdbcInlineDataGenerator(this); + private final JdbcIdentifierQuoter identQuoter = new JdbcIdentifierQuoter(() -> quoteIdentifierString, + () -> quotingPolicy, this::caseFolding, () -> sqlKeywordsLower, this::needsQuoting); + + private final JdbcCapabilityRecords capRecords = new JdbcCapabilityRecords(this); + + // SINGLE_QUOTE_SIZE / DOUBLE_QUOTE_SIZE / TRIVIAL_IDENTIFIER moved to + // JdbcIdentifierQuoter + + // DEFAULT_TYPE_MAP moved to JdbcTypeMapper + + protected void setQuoteIdentifierString(String quote) { + this.quoteIdentifierString = quote; + } + + public AbstractJdbcDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + if (init == null) { + init = org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults(); + } + applyInit(init); + } + + private void applyInit(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + // Pass null through directly: null means "driver doesn't support identifier + // quoting" and emitIdentifier emits verbatim in that case. + this.quoteIdentifierString = init.quoteIdentifierString(); + this.productName = init.productName(); + this.productVersion = init.productVersion(); + this.dialectVersion = init.version(); + this.supportedResultSetTypes = init.supportedResultSetStyles(); + this.readOnly = init.readOnly(); + if (init.maxColumnNameLength() > 0) { + this.maxColumnNameLength = init.maxColumnNameLength(); + } + java.util.Set combined = new HashSet<>(SQL92_RESERVED_LOWER); + combined.addAll(init.sqlKeywordsLower()); + this.sqlKeywordsLower = java.util.Set.copyOf(combined); + if (init.quotingPolicy() != null) { + setQuotingPolicy(init.quotingPolicy()); + } + } + + private static Set deduceSqlKeywords(DatabaseMetaData metaData) { + Set result = new HashSet<>(SQL92_RESERVED_LOWER); + try { + String csv = metaData.getSQLKeywords(); + if (csv != null && !csv.isEmpty()) { + for (String kw : csv.split(",")) { + String trimmed = kw.trim().toLowerCase(java.util.Locale.ROOT); + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + } + } catch (SQLException ignored) { + // Driver doesn't support getSQLKeywords; fall back to SQL-92 baseline. + } + return Set.copyOf(result); + } + + private static final Set SQL92_RESERVED_LOWER = Set.of("all", "and", "any", "as", "asc", "between", "by", + "case", "check", "column", "create", "cross", "current_date", "current_time", "current_timestamp", + "default", "delete", "desc", "distinct", "drop", "else", "end", "escape", "except", "exists", "false", + "for", "foreign", "from", "full", "group", "having", "in", "inner", "insert", "intersect", "into", "is", + "join", "key", "left", "like", "natural", "not", "null", "on", "or", "order", "outer", "primary", + "references", "right", "select", "set", "some", "table", "then", "true", "union", "unique", "update", + "user", "using", "values", "view", "when", "where", "with"); + + @Override + public boolean allowsDialectSharing() { + return caps.allowsDialectSharing(); + } + + protected boolean supportsNullsOrdering() { + return false; + } + + @Override + public String quoteIdentifier(final CharSequence val) { + return identQuoter.quote(val); + } + + @Override + public void quoteIdentifier(final String val, final StringBuilder buf) { + identQuoter.quote(val, buf); + } + + @Override + public void quoteIdentifierWith(final String val, final StringBuilder buf, IdentifierQuotingPolicy policy) { + identQuoter.quoteWith(val, buf, policy); + } + + @Override + public String quoteIdentifierWith(CharSequence val, IdentifierQuotingPolicy policy) { + return identQuoter.quoteWith(val, policy); + } + + @Override + public IdentifierQuotingPolicy quotingPolicy() { + return quotingPolicy; + } + + public void setQuotingPolicy(IdentifierQuotingPolicy policy) { + this.quotingPolicy = (policy == null) ? IdentifierQuotingPolicy.ALWAYS : policy; + } + + @Override + public String quoteIdentifierIfNeeded(CharSequence val) { + return identQuoter.quoteIfNeeded(val); + } + + /** Subclass-overridable hook — default delegates to the helper. */ + protected boolean needsQuoting(String val) { + return identQuoter.defaultNeedsQuoting(val); + } + + @Override + public String quoteIdentifier(final String qual, final String name) { + return identQuoter.quoteQualified(qual, name); + } + + @Override + public void quoteIdentifier(final StringBuilder buf, final String... names) { + identQuoter.quoteParts(buf, names); + } + + @Override + public String getQuoteIdentifierString() { + return quoteIdentifierString; + } + + @Override + public void quoteStringLiteral(StringBuilder buf, String s) { + literals.quoteString(buf, s); + } + + @Override + public void quoteNumericLiteral(StringBuilder buf, String value) { + literals.quoteNumeric(buf, value); + } + + @Override + public StringBuilder quoteDecimalLiteral(CharSequence value) { + return literals.quoteDecimal(value); + } + + @Override + public void quoteBooleanLiteral(StringBuilder buf, String value) { + literals.quoteBoolean(buf, value); + } + + @Override + public void quoteDateLiteral(StringBuilder buf, String value) { + literals.quoteDate(buf, value); + } + + /** Subclass-overridable hook for date-literal emission. */ + protected void quoteDateLiteral(StringBuilder buf, Date date) { + JdbcLiteralQuoter.defaultQuoteDateLiteral(buf, date); + } + + @Override + public void quoteTimeLiteral(StringBuilder buf, String value) { + literals.quoteTime(buf, value); + } + + @Override + public void quoteTimestampLiteral(StringBuilder buf, String value) { + literals.quoteTimestamp(buf, value); + } + + /** Subclass-overridable hook for timestamp-literal emission. */ + protected void quoteTimestampLiteral(StringBuilder buf, String value, Timestamp timestamp) { + JdbcLiteralQuoter.defaultQuoteTimestampLiteral(buf, value, timestamp); + } + + @Override + public boolean requiresAliasForFromQuery() { + return caps.requiresAliasForFromQuery(); + } + + @Override + public boolean allowsFromAlias() { + return caps.allowsFromAlias(); + } + + @Override + public boolean allowsFromQuery() { + return caps.allowsFromQuery(); + } + + @Override + public boolean allowsCompoundCountDistinct() { + return caps.allowsCompoundCountDistinct(); + } + + @Override + public boolean allowsCountDistinct() { + return caps.allowsCountDistinct(); + } + + @Override + public boolean allowsMultipleCountDistinct() { + return caps.allowsMultipleCountDistinct(); + } + + @Override + public boolean allowsMultipleDistinctSqlMeasures() { + return caps.allowsMultipleDistinctSqlMeasures(); + } + + @Override + public boolean allowsCountDistinctWithOtherAggs() { + return caps.allowsCountDistinctWithOtherAggs(); + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return inline.generateInline(columnNames, columnTypes, valueList); + } + + /** @return Expression that returns the given values */ + protected StringBuilder generateInlineGeneric(List columnNames, List columnTypes, + List valueList, String fromClause, boolean cast) { + return inline.generateInlineGeneric(columnNames, columnTypes, valueList, fromClause, cast); + } + + /** @return Expression that returns the given values via SQL-2003 VALUES */ + public StringBuilder generateInlineForAnsi(String alias, List columnNames, List columnTypes, + List valueList, boolean cast) { + return inline.generateInlineForAnsi(alias, columnNames, columnTypes, valueList, cast); + } + + @Override + public boolean needsExponent(Object value, String valueString) { + return false; + } + + @Override + public void quote(StringBuilder buf, Object value, Datatype datatype) { + inline.quote(buf, value, datatype); + } + + @Override + public boolean supportsDdl() { + return !readOnly; + } + + @Override + public boolean supportsGroupByExpressions() { + return caps.supportsGroupByExpressions(); + } + + // allowsSelectNotInGroupBy(): inherits the interface default (false). + // Dialects that allow MySQL-style implicit ANY (Hive, MySQL with non-strict + // SQL_MODE) can override directly. + + @Override + public boolean allowsJoinOn() { + return caps.allowsJoinOn(); + } + + @Override + public boolean supportsGroupingSets() { + return caps.supportsGroupingSets(); + } + + @Override + public boolean supportsUnlimitedValueList() { + return caps.supportsUnlimitedValueList(); + } + + @Override + public boolean requiresGroupByAlias() { + return caps.requiresGroupByAlias(); + } + + @Override + public boolean requiresOrderByAlias() { + return caps.requiresOrderByAlias(); + } + + @Override + public boolean requiresHavingAlias() { + return caps.requiresHavingAlias(); + } + + @Override + public boolean allowsOrderByAlias() { + return caps.allowsOrderByAlias(); + } + + @Override + public boolean requiresUnionOrderByOrdinal() { + return caps.requiresUnionOrderByOrdinal(); + } + + @Override + public boolean requiresUnionOrderByExprInSelect() { + return caps.requiresUnionOrderByExprInSelect(); + } + + @Override + public boolean supportsMultiValueInExpr() { + return caps.supportsMultiValueInExpr(); + } + + @Override + public boolean supportsResultSetConcurrency(int type, int concurrency) { + return supportedResultSetTypes.contains(Arrays.asList(type, concurrency)); + } + + @Override + public String toString() { + return productName; + } + + @Override + public int getMaxColumnNameLength() { + return maxColumnNameLength; + } + + @Override + public boolean allowsRegularExpressionInWhereClause() { + return caps.allowsRegularExpressionInWhereClause(); + } + + @Override + public BestFitColumnType getType(ResultSetMetaData metaData, int columnIndex) throws SQLException { + BestFitColumnType internalType = JdbcTypeMapper.resolveType(metaData, columnIndex); + logTypeInfo(metaData, columnIndex, internalType); + return internalType; + } + + protected void logTypeInfo(ResultSetMetaData metaData, int columnIndex, BestFitColumnType internalType) + throws SQLException { + if (LOGGER.isDebugEnabled()) { + final int columnType = metaData.getColumnType(columnIndex + 1); + final int precision = metaData.getPrecision(columnIndex + 1); + final int scale = metaData.getScale(columnIndex + 1); + final String columnName = metaData.getColumnName(columnIndex + 1); + LOGGER.debug(new StringBuilder("AbstractJdbcDialect.getType ").append("Dialect- ").append(this.name()) + .append(", Column-").append(columnName).append(" is of internal type ").append(internalType) + .append(". JDBC type was ").append(columnType).append(". Column precision=").append(precision) + .append(". Column scale=").append(scale).toString()); + } + } + + @Override + public boolean requiresDrillthroughMaxRowsInLimit() { + return caps.requiresDrillthroughMaxRowsInLimit(); + } + + @Override + public boolean allowsFieldAlias() { + return caps.allowsFieldAlias(); + } + + @Override + public boolean allowsInnerDistinct() { + return caps.allowsInnerDistinct(); + } + + @Override + public String clearTable(String schemaName, String tableName) { + return ddl.clearTable(schemaName, tableName); + } + + @Override + public String dropTable(String schemaName, String tableName, boolean ifExists) { + return ddl.dropTable(schemaName, tableName, ifExists); + } + + @Override + public String createSchema(String schemaName, boolean ifNotExists) { + return ddl.createSchema(schemaName, ifNotExists); + } + + @Override + public String dropSchema(String schemaName, boolean ifExists, boolean cascade) { + return ddl.dropSchema(schemaName, ifExists, cascade); + } + + @Override + public boolean supportsParallelLoading() { + return caps.supportsParallelLoading(); + } + + @Override + public boolean supportsBatchOperations() { + return caps.supportsBatchOperations(); + } + + protected Set> deduceSupportedResultSetStyles(DatabaseMetaData databaseMetaData) { + Set> supports = new HashSet>(); + for (int type : RESULT_SET_TYPE_VALUES) { + for (int concurrency : CONCURRENCY_VALUES) { + try { + if (databaseMetaData.supportsResultSetConcurrency(type, concurrency)) { + String driverName = databaseMetaData.getDriverName(); + if (type != ResultSet.TYPE_FORWARD_ONLY + && driverName.equals("JDBC-ODBC Bridge (odbcjt32.dll)")) { + // In JDK 1.6, the Jdbc-Odbc bridge announces + // that it can handle TYPE_SCROLL_INSENSITIVE + // but it does so by generating a 'COUNT(*)' + // query, and this query is invalid if the query + // contains a single-quote. So, override the + // driver. + continue; + } + supports.add(new ArrayList(Arrays.asList(type, concurrency))); + } + } catch (SQLException e) { + // DB2 throws "com.ibm.db2.jcc.b.SqlException: Unknown type + // or Concurrency" for certain values of type/concurrency. + // No harm in interpreting all such exceptions as 'this + // database does not support this type/concurrency + // combination'. + // Util.discard(e); + } + } + } + return supports; + } + + // Capability record caching for performance optimization + + @Override + public AggregateCapabilities getAggregateCapabilities() { + return capRecords.aggregate(); + } + + @Override + public JoinCapabilities getJoinCapabilities() { + return capRecords.join(); + } + + @Override + public OrderByCapabilities getOrderByCapabilities() { + return capRecords.orderBy(supportsNullsOrdering()); + } + + @Override + public WindowFunctionCapabilities getWindowFunctionCapabilities() { + return capRecords.windowFunction(); + } + +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AnsiDialect.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AnsiDialect.java new file mode 100644 index 0000000..55fef82 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/AnsiDialect.java @@ -0,0 +1,48 @@ +/* + * 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.common; + +import java.sql.Connection; + +public class AnsiDialect extends AbstractJdbcDialect { + + private static final String DIALECT_NAME = "ansi"; + + /** + * Construct with a JDBC connection — derives quoting, max name length, etc. + * from metadata. + */ + public AnsiDialect(Connection connection) { + super(initDataFor(connection)); + } + + /** JDBC-free constructor — uses ANSI double-quote and SQL-99 defaults. */ + public AnsiDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot. Preferred path. */ + public AnsiDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + private static org.eclipse.daanse.sql.dialect.api.DialectInitData initDataFor(Connection c) { + try { + return org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(c); + } catch (java.sql.SQLException e) { + return org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults(); + } + } + + @Override + public String name() { + return DIALECT_NAME; + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtil.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtil.java new file mode 100644 index 0000000..342b3dd --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtil.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import java.util.regex.Pattern; + +public class DialectUtil { + + private static final Pattern UNICODE_CASE_FLAG_IN_JAVA_REG_EXP_PATTERN = Pattern.compile("\\|\\(\\?u\\)"); + private static final String EMPTY = ""; + + public static final String ESCAPE_REGEXP = "(\\\\Q([^\\\\Q]+)\\\\E)"; + + public static final Pattern ESCAPE_PATTERN = Pattern.compile(ESCAPE_REGEXP); + + private DialectUtil() { + // constructor + } + + /** + * @param javaRegExp the regular expression to clean up + * @return the cleaned regular expression + */ + public static String cleanUnicodeAwareCaseFlag(String javaRegExp) { + String cleaned = javaRegExp; + if (cleaned != null && isUnicodeCaseFlagInRegExp(cleaned)) { + cleaned = UNICODE_CASE_FLAG_IN_JAVA_REG_EXP_PATTERN.matcher(cleaned).replaceAll(EMPTY); + } + return cleaned; + } + + private static boolean isUnicodeCaseFlagInRegExp(String javaRegExp) { + return UNICODE_CASE_FLAG_IN_JAVA_REG_EXP_PATTERN.matcher(javaRegExp).find(); + } + + /** + * @param val the value to quote + * @return the quoted string + */ + public static String singleQuoteString(String val) { + StringBuilder buf = new StringBuilder(64); + singleQuoteString(val, buf); + return buf.toString(); + } + + /** + * @param val the value to quote + * @param buf the buffer to append to + */ + public static void singleQuoteString(String val, StringBuilder buf) { + buf.append('\''); + String escaped = val.replace("'", "''"); + buf.append(escaped); + buf.append('\''); + } + + /** + * @param expr the expression to order by + * @param ascending true for ASC, false for DESC + * @param collateNullsLast true if nulls should appear last + * @return the generated ORDER BY expression + */ + public static StringBuilder generateOrderByNullsWithIsnull(CharSequence expr, boolean ascending, + boolean collateNullsLast) { + if (collateNullsLast) { + if (ascending) { + return new StringBuilder("ISNULL(").append(expr).append(") ASC, ").append(expr).append(" ASC"); + } else { + return new StringBuilder(expr).append(" DESC"); + } + } else { + if (ascending) { + return new StringBuilder(expr).append(" ASC"); + } else { + return new StringBuilder("ISNULL(").append(expr).append(") DESC, ").append(expr).append(" DESC"); + } + } + } + +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java new file mode 100644 index 0000000..1f71f72 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityFlags.java @@ -0,0 +1,130 @@ +/* + * 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.common; + +import org.eclipse.daanse.sql.dialect.api.Dialect; + +/** + * Default values for the per-engine boolean capability flags. Methods that + * depend on another flag delegate through the supplied {@link Dialect} so + * subclass overrides are respected. + */ +final class JdbcCapabilityFlags { + + private final Dialect dialect; + + JdbcCapabilityFlags(Dialect dialect) { + this.dialect = dialect; + } + + boolean requiresAliasForFromQuery() { + return false; + } + + boolean allowsFromAlias() { + return true; + } + + boolean allowsFromQuery() { + return true; + } + + boolean allowsCompoundCountDistinct() { + return false; + } + + boolean allowsCountDistinct() { + return true; + } + + boolean allowsMultipleCountDistinct() { + return dialect.allowsCountDistinct(); + } + + boolean allowsMultipleDistinctSqlMeasures() { + return dialect.allowsMultipleCountDistinct(); + } + + boolean allowsCountDistinctWithOtherAggs() { + return dialect.allowsCountDistinct(); + } + + boolean supportsGroupByExpressions() { + return true; + } + + boolean allowsJoinOn() { + return true; + } + + boolean supportsGroupingSets() { + return false; + } + + boolean supportsUnlimitedValueList() { + return false; + } + + boolean requiresGroupByAlias() { + return false; + } + + boolean requiresOrderByAlias() { + return false; + } + + boolean requiresHavingAlias() { + return false; + } + + boolean allowsOrderByAlias() { + return dialect.requiresOrderByAlias(); + } + + boolean requiresUnionOrderByOrdinal() { + return true; + } + + boolean requiresUnionOrderByExprInSelect() { + return true; + } + + boolean supportsMultiValueInExpr() { + return false; + } + + boolean allowsRegularExpressionInWhereClause() { + return false; + } + + boolean requiresDrillthroughMaxRowsInLimit() { + return false; + } + + boolean allowsFieldAlias() { + return true; + } + + boolean allowsInnerDistinct() { + return true; + } + + boolean supportsParallelLoading() { + return true; + } + + boolean supportsBatchOperations() { + return true; + } + + boolean allowsDialectSharing() { + return true; + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityRecords.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityRecords.java new file mode 100644 index 0000000..2e90af9 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcCapabilityRecords.java @@ -0,0 +1,71 @@ +/* + * 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.common; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.capability.AggregateCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.JoinCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.OrderByCapabilities; +import org.eclipse.daanse.sql.dialect.api.capability.WindowFunctionCapabilities; + +/** + * Lazy-caches the four {@code XxxCapabilities} record snapshots derived from + * the dialect's flat boolean flags. Each record is built once on first access + * by reading current flag values from the dialect. + */ +final class JdbcCapabilityRecords { + + private final Dialect dialect; + private AggregateCapabilities aggregate; + private JoinCapabilities join; + private OrderByCapabilities orderBy; + private WindowFunctionCapabilities windowFunction; + + JdbcCapabilityRecords(Dialect dialect) { + this.dialect = dialect; + } + + AggregateCapabilities aggregate() { + if (aggregate == null) { + aggregate = new AggregateCapabilities(dialect.allowsCountDistinct(), dialect.allowsMultipleCountDistinct(), + dialect.allowsCompoundCountDistinct(), dialect.allowsCountDistinctWithOtherAggs(), + dialect.allowsInnerDistinct(), dialect.allowsMultipleDistinctSqlMeasures(), + dialect.supportsGroupingSets(), dialect.supportsGroupByExpressions(), + dialect.allowsSelectNotInGroupBy()); + } + return aggregate; + } + + JoinCapabilities join() { + if (join == null) { + join = new JoinCapabilities(dialect.allowsJoinOn(), dialect.allowsFromAlias(), dialect.allowsFromQuery(), + dialect.requiresAliasForFromQuery()); + } + return join; + } + + OrderByCapabilities orderBy(boolean supportsNullsOrdering) { + if (orderBy == null) { + orderBy = new OrderByCapabilities(dialect.allowsOrderByAlias(), dialect.requiresOrderByAlias(), + dialect.requiresUnionOrderByOrdinal(), dialect.requiresUnionOrderByExprInSelect(), + dialect.requiresGroupByAlias(), dialect.requiresHavingAlias(), supportsNullsOrdering); + } + return orderBy; + } + + WindowFunctionCapabilities windowFunction() { + if (windowFunction == null) { + var agg = dialect.aggregationGenerator(); + windowFunction = new WindowFunctionCapabilities(agg.supportsPercentileDisc(), agg.supportsPercentileCont(), + agg.supportsListAgg(), agg.supportsNthValue(), agg.supportsNthValueIgnoreNulls()); + } + return windowFunction; + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDdlEmitter.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDdlEmitter.java new file mode 100644 index 0000000..139e01b --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDdlEmitter.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import org.eclipse.daanse.sql.dialect.api.Dialect; + +/** + * Default DDL string emission ({@code TRUNCATE}, {@code DROP TABLE}, + * {@code CREATE/DROP SCHEMA}). Calls back to the dialect for identifier quoting + * and feature-detection flags. + */ +final class JdbcDdlEmitter { + + private final Dialect dialect; + + JdbcDdlEmitter(Dialect dialect) { + this.dialect = dialect; + } + + String clearTable(String schemaName, String tableName) { + return new StringBuilder("TRUNCATE TABLE ").append(dialect.quoteIdentifier(schemaName, tableName)).toString(); + } + + String dropTable(String schemaName, String tableName, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP TABLE "); + if (ifExists && dialect.supportsDropTableIfExists()) { + sb.append("IF EXISTS "); + } + return sb.append(dialect.quoteIdentifier(schemaName, tableName)).toString(); + } + + String createSchema(String schemaName, boolean ifNotExists) { + StringBuilder sb = new StringBuilder("CREATE SCHEMA "); + if (ifNotExists) { + sb.append("IF NOT EXISTS "); + } + return sb.append(dialect.quoteIdentifier(schemaName)).toString(); + } + + String dropSchema(String schemaName, boolean ifExists, boolean cascade) { + StringBuilder sb = new StringBuilder("DROP SCHEMA "); + if (ifExists && dialect.supportsDropSchemaIfExists()) { + sb.append("IF EXISTS "); + } + sb.append(dialect.quoteIdentifier(schemaName)); + if (dialect.requiresDropSchemaRestrict()) { + sb.append(" RESTRICT"); + } else if (cascade && dialect.supportsDropTableCascade()) { + sb.append(" CASCADE"); + } + return sb.toString(); + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcIdentifierQuoter.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcIdentifierQuoter.java new file mode 100644 index 0000000..441ebc2 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcIdentifierQuoter.java @@ -0,0 +1,146 @@ +/* + * 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.common; + +import java.util.Locale; +import java.util.Set; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.regex.Pattern; + +import org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; + +/** + * Default identifier-quoting logic. Pure: callers supply + * {@code quoteIdentifierString}, {@code quotingPolicy}, {@code caseFolding}, + * {@code sqlKeywordsLower}, and the protected {@code needsQuoting} hook (for + * subclass overrides). + */ +final class JdbcIdentifierQuoter { + + static final int SINGLE_QUOTE_SIZE = 10; + static final int DOUBLE_QUOTE_SIZE = 2 * SINGLE_QUOTE_SIZE + 1; + static final Pattern TRIVIAL_IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*"); + + private final Supplier quoteIdentifierString; + private final Supplier quotingPolicy; + private final Supplier caseFolding; + private final Supplier> sqlKeywordsLower; + private final Predicate needsQuotingHook; + + JdbcIdentifierQuoter(Supplier quoteIdentifierString, Supplier quotingPolicy, + Supplier caseFolding, Supplier> sqlKeywordsLower, + Predicate needsQuotingHook) { + this.quoteIdentifierString = quoteIdentifierString; + this.quotingPolicy = quotingPolicy; + this.caseFolding = caseFolding; + this.sqlKeywordsLower = sqlKeywordsLower; + this.needsQuotingHook = needsQuotingHook; + } + + String quote(CharSequence val) { + int size = val.length() + SINGLE_QUOTE_SIZE; + StringBuilder buf = new StringBuilder(size); + quote(val.toString(), buf); + return buf.toString(); + } + + void quote(final String val, final StringBuilder buf) { + emit(val, buf, quotingPolicy.get()); + } + + void quoteWith(final String val, final StringBuilder buf, IdentifierQuotingPolicy policy) { + if (val == null) + return; + emit(val, buf, policy == null ? IdentifierQuotingPolicy.ALWAYS : policy); + } + + String quoteWith(CharSequence val, IdentifierQuotingPolicy policy) { + if (val == null) + return ""; + StringBuilder buf = new StringBuilder(val.length() + SINGLE_QUOTE_SIZE); + emit(val.toString(), buf, policy == null ? IdentifierQuotingPolicy.ALWAYS : policy); + return buf.toString(); + } + + String quoteIfNeeded(CharSequence val) { + if (val == null) + return ""; + String s = val.toString(); + if (!needsQuotingHook.test(s)) + return s; + String q = quoteIdentifierString.get(); + if (q == null || (s.startsWith(q) && s.endsWith(q))) + return s; + String escaped = s.replace(q, q + q); + StringBuilder buf = new StringBuilder(s.length() + SINGLE_QUOTE_SIZE); + buf.append(q).append(escaped).append(q); + return buf.toString(); + } + + String quoteQualified(final String qual, final String name) { + int size = name != null ? name.length() + : 0 + ((qual == null) ? SINGLE_QUOTE_SIZE : (qual.length() + DOUBLE_QUOTE_SIZE)); + StringBuilder buf = new StringBuilder(size); + quoteParts(buf, qual, name); + return buf.toString(); + } + + void quoteParts(final StringBuilder buf, final String... names) { + int nonNullNameCount = 0; + for (String name : names) { + if (name == null) + continue; + if (nonNullNameCount > 0) { + buf.append('.'); + } + assert name.length() > 0 : "name should probably be null, not empty"; + quote(name, buf); + ++nonNullNameCount; + } + } + + private void emit(String val, StringBuilder buf, IdentifierQuotingPolicy policy) { + String q = quoteIdentifierString.get(); + if (q == null || (val.startsWith(q) && val.endsWith(q))) { + buf.append(val); + return; + } + switch (policy) { + case NEVER: + buf.append(val); + return; + case WHEN_NEEDED: + if (!needsQuotingHook.test(val)) { + buf.append(val); + return; + } + break; + case ALWAYS: + default: + break; + } + String val2 = val.replace(q, q + q); + buf.append(q).append(val2).append(q); + } + + /** Default {@code needsQuoting} body — used unless the dialect overrides. */ + boolean defaultNeedsQuoting(String val) { + if (val == null || val.isEmpty()) + return true; + if (!TRIVIAL_IDENTIFIER.matcher(val).matches()) + return true; + if (sqlKeywordsLower.get().contains(val.toLowerCase(Locale.ROOT))) + return true; + IdentifierCaseFolding folding = caseFolding.get(); + return folding != IdentifierCaseFolding.PRESERVE && !folding.isCanonical(val); + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcInlineDataGenerator.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcInlineDataGenerator.java new file mode 100644 index 0000000..cded1ac --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcInlineDataGenerator.java @@ -0,0 +1,201 @@ +/* + * 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.common; + +import java.util.List; +import java.util.Map; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.Datatype; + +/** + * Default emission of inline data ({@code SELECT … UNION ALL} and SQL-2003 + * {@code VALUES} forms) plus per-cell {@code quote(buf, value, datatype)}. + * Calls back to the dialect for identifier quoting, alias-allowed flag, and the + * exponent-needed hook. + */ +final class JdbcInlineDataGenerator { + + private final Dialect dialect; + + JdbcInlineDataGenerator(Dialect dialect) { + this.dialect = dialect; + } + + StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineForAnsi("t", columnNames, columnTypes, valueList, false); + } + + StringBuilder generateInlineGeneric(List columnNames, List columnTypes, List valueList, + String fromClause, boolean cast) { + final StringBuilder buf = new StringBuilder(); + int columnCount = columnNames.size(); + assert columnTypes.size() == columnCount; + + Integer[] maxLengths = new Integer[columnCount]; + if (cast) { + fillMaxLengthsArray(maxLengths, columnTypes, valueList); + } + + for (int i = 0; i < valueList.size(); i++) { + if (i > 0) { + buf.append(" union all "); + } + String[] values = valueList.get(i); + buf.append("select "); + formSelectFieldsForInlineGeneric(buf, values, columnTypes, columnNames, maxLengths); + if (fromClause != null) { + buf.append(fromClause); + } + } + return buf; + } + + + StringBuilder generateInlineForAnsi(String alias, List columnNames, List columnTypes, + List valueList, boolean cast) { + final StringBuilder buf = new StringBuilder(); + buf.append("SELECT * FROM (VALUES "); + String[] castTypes = null; + if (cast) { + castTypes = getCastTypes(columnNames, columnTypes, valueList); + } + formSelectFieldsForInlineForAnsi(buf, valueList, columnTypes, castTypes); + buf.append(") AS "); + dialect.quoteIdentifier(alias, buf); + buf.append(" ("); + for (int j = 0; j < columnNames.size(); j++) { + final String columnName = columnNames.get(j); + if (j > 0) { + buf.append(", "); + } + dialect.quoteIdentifier(columnName, buf); + } + buf.append(")"); + return buf; + } + + void quote(StringBuilder buf, Object value, Datatype datatype) { + if (value == null) { + buf.append("null"); + } else { + String valueString = value.toString(); + if (dialect.needsExponent(value, valueString)) { + valueString += "E0"; + } + dialect.quoteLiteral(datatype, buf, valueString); + } + } + + private void formSelectFieldsForInlineGeneric(StringBuilder buf, String[] values, List columnTypes, + List columnNames, Integer[] maxLengths) { + for (int j = 0; j < values.length; j++) { + String value = values[j]; + if (j > 0) { + buf.append(", "); + } + final String columnType = columnTypes.get(j); + final String columnName = columnNames.get(j); + Datatype datatype = Datatype.fromValue(columnType); + final Integer maxLength = maxLengths[j]; + if (maxLength != null) { + buf.append("CAST("); + quote(buf, value, datatype); + buf.append(" AS VARCHAR(").append(maxLength).append("))"); + } else { + quote(buf, value, datatype); + } + if (dialect.allowsFromAlias()) { + buf.append(" as "); + } else { + buf.append(' '); + } + dialect.quoteIdentifier(columnName, buf); + } + } + + private void formSelectFieldsForInlineForAnsi(StringBuilder buf, List valueList, List columnTypes, + String[] castTypes) { + for (int i = 0; i < valueList.size(); i++) { + if (i > 0) { + buf.append(", "); + } + String[] values = valueList.get(i); + buf.append("("); + for (int j = 0; j < values.length; j++) { + String value = values[j]; + if (j > 0) { + buf.append(", "); + } + final String columnType = columnTypes.get(j); + Datatype datatype = Datatype.fromValue(columnType); + if (value == null) { + String sqlType = guessSqlType(columnType, valueList, j); + buf.append("CAST(NULL AS ").append(sqlType).append(")"); + } else if (castTypes != null && castTypes[j] != null) { + buf.append("CAST("); + quote(buf, value, datatype); + buf.append(" AS ").append(castTypes[j]).append(")"); + } else { + quote(buf, value, datatype); + } + } + buf.append(")"); + } + } + + private void fillMaxLengthsArray(final Integer[] maxLengths, final List columnTypes, + final List valueList) { + for (int i = 0; i < columnTypes.size(); i++) { + String columnType = columnTypes.get(i); + Datatype datatype = Datatype.fromValue(columnType); + if (datatype == Datatype.VARCHAR) { + maxLengths[i] = getMaxLen(valueList, i); + } + } + } + + private Integer getMaxLen(List valueList, int i) { + int maxLen = -1; + for (String[] strings : valueList) { + if (strings[i] != null && strings[i].length() > maxLen) { + maxLen = strings[i].length(); + } + } + return maxLen; + } + + private String[] getCastTypes(List columnNames, List columnTypes, List valueList) { + String[] castTypes = new String[columnNames.size()]; + for (int i = 0; i < columnNames.size(); i++) { + String columnType = columnTypes.get(i); + if (Datatype.fromValue(columnType) == Datatype.VARCHAR) { + castTypes[i] = guessSqlType(columnType, valueList, i); + } + } + return castTypes; + } + + private static String guessSqlType(String basicType, List valueList, int column) { + if (Datatype.fromValue(basicType) == Datatype.VARCHAR) { + int maxLen = 1; + for (String[] values : valueList) { + final String value = values[column]; + if (value == null) { + continue; + } + maxLen = Math.max(maxLen, value.length()); + } + return new StringBuilder("VARCHAR(").append(maxLen).append(")").toString(); + } else { + return "INTEGER"; + } + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcLiteralQuoter.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcLiteralQuoter.java new file mode 100644 index 0000000..e2ed4bf --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcLiteralQuoter.java @@ -0,0 +1,113 @@ +/* + * 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.common; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; + +/** + * Default SQL literal quoting for the standard scalar types. Date and timestamp + * delegate to {@link Hooks} for the protected overload that subclasses extend. + */ +final class JdbcLiteralQuoter { + + /** Subclass-overridable protected hooks the dialect provides via callbacks. */ + interface Hooks { + void quoteDateLiteral(StringBuilder buf, Date date); + + void quoteTimestampLiteral(StringBuilder buf, String value, Timestamp timestamp); + } + + private final Hooks hooks; + + JdbcLiteralQuoter(Hooks hooks) { + this.hooks = hooks; + } + + void quoteString(StringBuilder buf, String s) { + DialectUtil.singleQuoteString(s, buf); + } + + void quoteNumeric(StringBuilder buf, String value) { + buf.append(value); + } + + StringBuilder quoteDecimal(CharSequence value) { + return new StringBuilder(value); + } + + void quoteBoolean(StringBuilder buf, String value) { + // Boolean-typed levels are frequently stored physically as 0/1 (e.g. SMALLINT, for + // portability — DuckDB reads such a column via getInt). Accept the numeric spelling and + // emit it verbatim so the predicate matches the integer-backed column, rather than + // rejecting it as an illegal boolean literal. + if (value.equals("0") || value.equals("1")) { + buf.append(value); + return; + } + if (!value.equalsIgnoreCase("TRUE") && !(value.equalsIgnoreCase("FALSE"))) { + throw new NumberFormatException("Illegal BOOLEAN literal: " + value); + } + buf.append(value); + } + + void quoteDate(StringBuilder buf, String value) { + Date date; + try { + date = Date.valueOf(value); + } catch (IllegalArgumentException ex) { + try { + date = new Date(Timestamp.valueOf(value).getTime()); + } catch (IllegalArgumentException ex2) { + throw new NumberFormatException("Illegal DATE literal: " + value); + } + } + hooks.quoteDateLiteral(buf, date); + } + + void quoteTime(StringBuilder buf, String value) { + try { + Time.valueOf(value); + } catch (IllegalArgumentException ex) { + throw new NumberFormatException("Illegal TIME literal: " + value); + } + buf.append("TIME "); + DialectUtil.singleQuoteString(value, buf); + } + + void quoteTimestamp(StringBuilder buf, String value) { + Timestamp timestamp; + try { + timestamp = Timestamp.valueOf(value); + } catch (IllegalArgumentException ex) { + throw new NumberFormatException("Illegal TIMESTAMP literal: " + value); + } + hooks.quoteTimestampLiteral(buf, timestamp.toString(), timestamp); + } + + /** + * Default implementation of the protected {@code quoteDateLiteral(buf, Date)} + * hook. + */ + static void defaultQuoteDateLiteral(StringBuilder buf, Date date) { + buf.append("DATE "); + DialectUtil.singleQuoteString(date.toString(), buf); + } + + /** + * Default implementation of the protected + * {@code quoteTimestampLiteral(buf, value, Timestamp)} hook. + */ + static void defaultQuoteTimestampLiteral(StringBuilder buf, String value, Timestamp timestamp) { + buf.append("TIMESTAMP "); + DialectUtil.singleQuoteString(value, buf); + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcTypeMapper.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcTypeMapper.java new file mode 100644 index 0000000..48bc3e0 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/JdbcTypeMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.daanse.sql.model.type.BestFitColumnType; + +/** + * Default JDBC SQL-type → {@link BestFitColumnType} mapping. NUMERIC/DECIMAL + * adapt to precision/scale; everything else looks up {@link #DEFAULT_TYPE_MAP}. + */ +final class JdbcTypeMapper { + + static final Map DEFAULT_TYPE_MAP; + + static { + Map m = new HashMap<>(); + m.put(Types.SMALLINT, BestFitColumnType.INT); + m.put(Types.INTEGER, BestFitColumnType.INT); + m.put(Types.BOOLEAN, BestFitColumnType.INT); + m.put(Types.DOUBLE, BestFitColumnType.DOUBLE); + m.put(Types.FLOAT, BestFitColumnType.DOUBLE); + m.put(Types.BIGINT, BestFitColumnType.DOUBLE); + DEFAULT_TYPE_MAP = Collections.unmodifiableMap(m); + } + + private JdbcTypeMapper() { + // static helpers only + } + + static BestFitColumnType resolveType(ResultSetMetaData metaData, int columnIndex) throws SQLException { + final int columnType = metaData.getColumnType(columnIndex + 1); + BestFitColumnType internalType = null; + if (columnType != Types.NUMERIC && columnType != Types.DECIMAL) { + internalType = DEFAULT_TYPE_MAP.get(columnType); + } else { + final int precision = metaData.getPrecision(columnIndex + 1); + final int scale = metaData.getScale(columnIndex + 1); + if (scale == 0 && precision <= 9) { + internalType = BestFitColumnType.INT; + } else { + internalType = BestFitColumnType.DOUBLE; + } + } + return internalType == null ? BestFitColumnType.OBJECT : internalType; + } +} diff --git a/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/package-info.java b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/package-info.java new file mode 100644 index 0000000..5831001 --- /dev/null +++ b/dialect/db/common/src/main/java/org/eclipse/daanse/sql/dialect/db/common/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.common; diff --git a/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialectTest.java b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialectTest.java new file mode 100644 index 0000000..5613ae9 --- /dev/null +++ b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/AbstractJdbcDialectTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AbstractJdbcDialectTest { + + private static final String ILLEGAL_BOOLEAN_LITERAL = "illegal for base dialect implemetation boolean literal"; + private static final String ILLEGAL_BOOLEAN_LITERAL_MESSAGE = "Illegal BOOLEAN literal: "; + private static final String BOOLEAN_LITERAL_TRUE = "True"; + private static final String BOOLEAN_LITERAL_FALSE = "False"; + private static final String BOOLEAN_LITERAL_ONE = "1"; + private static final String BOOLEAN_LITERAL_ZERO = "0"; + + private AbstractJdbcDialect jdbcDialect = new AbstractJdbcDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()) { + + @Override + public String name() { + return null; + } + + }; + private static StringBuilder buf; + + @BeforeEach + protected void setUp() throws Exception { + buf = new StringBuilder(); + } + + @Nested + @DisplayName("Regular Expression Tests") + class RegularExpressionTests { + + @Test + void testAllowsRegularExpressionInWhereClause() { + assertFalse(jdbcDialect.allowsRegularExpressionInWhereClause()); + } + + @Test + void testGenerateRegularExpression() { + assertTrue(jdbcDialect.regexGenerator().generateRegularExpression(null, null).isEmpty()); + } + } + + @Nested + @DisplayName("Boolean Literal Quoting Tests") + class BooleanLiteralQuotingTests { + + @Test + void testQuoteBooleanLiteral_True() throws Exception { + assertEquals(0, buf.length()); + jdbcDialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_TRUE); + assertEquals(BOOLEAN_LITERAL_TRUE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_False() throws Exception { + assertEquals(0, buf.length()); + jdbcDialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_FALSE); + assertEquals(BOOLEAN_LITERAL_FALSE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_OneAccepted() throws Exception { + // 0/1 are accepted as BOOLEAN literals since the quoteBoolean fix. + assertEquals(0, buf.length()); + jdbcDialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ONE); + assertEquals(BOOLEAN_LITERAL_ONE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_ZeroAccepted() throws Exception { + // 0/1 are accepted as BOOLEAN literals since the quoteBoolean fix. + assertEquals(0, buf.length()); + jdbcDialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ZERO); + assertEquals(BOOLEAN_LITERAL_ZERO, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_TrowsExceptionOnIllegaLiteral() throws Exception { + assertEquals(0, buf.length()); + try { + jdbcDialect.quoteBooleanLiteral(buf, ILLEGAL_BOOLEAN_LITERAL); + fail("The illegal boolean literal exception should appear BUT it was not."); + } catch (NumberFormatException e) { + assertEquals(ILLEGAL_BOOLEAN_LITERAL_MESSAGE + ILLEGAL_BOOLEAN_LITERAL, e.getMessage()); + } + } + } +} diff --git a/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtilTest.java b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtilTest.java new file mode 100644 index 0000000..b202920 --- /dev/null +++ b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/DialectUtilTest.java @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.regex.Matcher; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class DialectUtilTest { + + @Nested + @DisplayName("Single Quote String Tests") + class SingleQuoteStringTests { + + @Test + void testSingleQuoteString_Simple() { + assertEquals("'foo'", DialectUtil.singleQuoteString("foo")); + } + + @Test + void testSingleQuoteString_WithSingleQuote() { + assertEquals("'don''t'", DialectUtil.singleQuoteString("don't")); + } + + @Test + void testSingleQuoteString_WithMultipleSingleQuotes() { + assertEquals("'it''s a ''test'''", DialectUtil.singleQuoteString("it's a 'test'")); + } + + @Test + void testSingleQuoteString_Empty() { + assertEquals("''", DialectUtil.singleQuoteString("")); + } + + @Test + void testSingleQuoteString_StringBuilder() { + StringBuilder buf = new StringBuilder("SELECT "); + DialectUtil.singleQuoteString("value", buf); + assertEquals("SELECT 'value'", buf.toString()); + } + + @Test + void testSingleQuoteString_StringBuilder_WithQuotes() { + StringBuilder buf = new StringBuilder(); + DialectUtil.singleQuoteString("it's", buf); + assertEquals("'it''s'", buf.toString()); + } + } + + @Nested + @DisplayName("Unicode Case Flag Tests") + class UnicodeCaseFlagTests { + + @Test + void testCleanUnicodeAwareCaseFlag_WithFlag() { + String regex = "(?i)|(?u).*pattern.*"; + String cleaned = DialectUtil.cleanUnicodeAwareCaseFlag(regex); + assertEquals("(?i).*pattern.*", cleaned); + } + + @Test + void testCleanUnicodeAwareCaseFlag_WithoutFlag() { + String regex = "(?i).*pattern.*"; + String cleaned = DialectUtil.cleanUnicodeAwareCaseFlag(regex); + assertEquals("(?i).*pattern.*", cleaned); + } + + @Test + void testCleanUnicodeAwareCaseFlag_Null() { + String cleaned = DialectUtil.cleanUnicodeAwareCaseFlag(null); + assertEquals(null, cleaned); + } + } + + @Nested + @DisplayName("Escape Pattern Tests") + class EscapePatternTests { + + @Test + void testEscapePattern_NotNull() { + assertNotNull(DialectUtil.ESCAPE_PATTERN); + } + + @Test + void testEscapePattern_MatchesQuotedSequence() { + String input = "prefix\\Qescaped content\\Esuffix"; + Matcher matcher = DialectUtil.ESCAPE_PATTERN.matcher(input); + assertTrue(matcher.find()); + assertEquals("\\Qescaped content\\E", matcher.group(1)); + assertEquals("escaped content", matcher.group(2)); + } + + @Test + void testEscapePattern_NoMatch() { + String input = "no escape sequence here"; + Matcher matcher = DialectUtil.ESCAPE_PATTERN.matcher(input); + assertTrue(!matcher.find()); + } + + @Test + void testEscapeRegexp_Constant() { + assertEquals("(\\\\Q([^\\\\Q]+)\\\\E)", DialectUtil.ESCAPE_REGEXP); + } + } +} diff --git a/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/IdentifierQuotingPolicyTest.java b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/IdentifierQuotingPolicyTest.java new file mode 100644 index 0000000..56c6555 --- /dev/null +++ b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/IdentifierQuotingPolicyTest.java @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class IdentifierQuotingPolicyTest { + + /** A dialect we can swap the case-folding rule on per test. */ + static class FoldingDialect extends AbstractJdbcDialect { + private final IdentifierCaseFolding folding; + + FoldingDialect(IdentifierCaseFolding folding) { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + this.folding = folding; + } + + @Override + public IdentifierCaseFolding caseFolding() { + return folding; + } + + @Override + public boolean needsExponent(Object value, String valueString) { + return false; + } + + @Override + public String name() { + return "test"; + } + } + + @Nested + class AlwaysPolicyKeepsHistoricalBehaviour { + FoldingDialect d = new FoldingDialect(IdentifierCaseFolding.UPPER); + + @Test + void identifierIsAlwaysQuoted() { + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("\"EMPLOYEES\""); + } + + @Test + void mixedCaseIsAlwaysQuoted() { + assertThat(d.quoteIdentifier("firstName").toString()).isEqualTo("\"firstName\""); + } + + @Test + void reservedWordIsAlwaysQuoted() { + assertThat(d.quoteIdentifier("ORDER").toString()).isEqualTo("\"ORDER\""); + } + } + + @Nested + class WhenNeededPolicyOnUpperFoldingDialect { + FoldingDialect d; + + WhenNeededPolicyOnUpperFoldingDialect() { + d = new FoldingDialect(IdentifierCaseFolding.UPPER); + d.setQuotingPolicy(IdentifierQuotingPolicy.WHEN_NEEDED); + } + + @Test + void canonicalUpperCaseIsEmittedUnquoted() { + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("EMPLOYEES"); + } + + @Test + void mixedCaseRequiresQuoting() { + assertThat(d.quoteIdentifier("firstName").toString()).isEqualTo("\"firstName\""); + } + + @Test + void reservedWordRequiresQuoting() { + // ORDER is canonical UPPER but it is reserved → must be quoted + assertThat(d.quoteIdentifier("ORDER").toString()).isEqualTo("\"ORDER\""); + } + + @Test + void nonTrivialCharsRequireQuoting() { + assertThat(d.quoteIdentifier("with-dash").toString()).isEqualTo("\"with-dash\""); + } + + @Test + void emptyNameQuoted() { + assertThat(d.quoteIdentifier("").toString()).isEqualTo("\"\""); + } + } + + @Nested + class WhenNeededPolicyOnLowerFoldingDialect { + FoldingDialect d; + + WhenNeededPolicyOnLowerFoldingDialect() { + d = new FoldingDialect(IdentifierCaseFolding.LOWER); + d.setQuotingPolicy(IdentifierQuotingPolicy.WHEN_NEEDED); + } + + @Test + void canonicalLowerCaseIsEmittedUnquoted() { + assertThat(d.quoteIdentifier("employees").toString()).isEqualTo("employees"); + } + + @Test + void upperCaseRequiresQuoting() { + // 'EMPLOYEES' is non-canonical for a LOWER-folding dialect + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("\"EMPLOYEES\""); + } + } + + @Nested + class WhenNeededPolicyOnPreserveFoldingDialect { + FoldingDialect d; + + WhenNeededPolicyOnPreserveFoldingDialect() { + d = new FoldingDialect(IdentifierCaseFolding.PRESERVE); + d.setQuotingPolicy(IdentifierQuotingPolicy.WHEN_NEEDED); + } + + @Test + void mixedCaseIsAlwaysSafeUnquotedOnPreserveDialect() { + assertThat(d.quoteIdentifier("firstName").toString()).isEqualTo("firstName"); + } + + @Test + void reservedWordStillRequiresQuoting() { + assertThat(d.quoteIdentifier("ORDER").toString()).isEqualTo("\"ORDER\""); + } + } + + @Nested + class NeverPolicyEmitsVerbatim { + FoldingDialect d; + + NeverPolicyEmitsVerbatim() { + d = new FoldingDialect(IdentifierCaseFolding.UPPER); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + } + + @Test + void emitsCanonicalNameUnquoted() { + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("EMPLOYEES"); + } + + @Test + void emitsMixedCaseVerbatim_consumerResponsibilityToMatchCatalog() { + assertThat(d.quoteIdentifier("firstName").toString()).isEqualTo("firstName"); + } + + @Test + void emitsReservedWordVerbatim_consumerResponsibilityToAvoid() { + assertThat(d.quoteIdentifier("ORDER").toString()).isEqualTo("ORDER"); + } + } + + @Nested + class PerCallOverrideDoesNotMutateDialect { + FoldingDialect d = new FoldingDialect(IdentifierCaseFolding.UPPER); + + @Test + void alwaysPolicyOnDialect_overrideToWhenNeeded_yieldsUnquoted() { + // dialect default is ALWAYS — would normally quote + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("\"EMPLOYEES\""); + + // per-call override does not change the dialect's policy + assertThat(d.quoteIdentifierWith("EMPLOYEES", IdentifierQuotingPolicy.WHEN_NEEDED).toString()) + .isEqualTo("EMPLOYEES"); + + // dialect default still ALWAYS — next normal call still quotes + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("\"EMPLOYEES\""); + assertThat(d.quotingPolicy()).isEqualTo(IdentifierQuotingPolicy.ALWAYS); + } + + @Test + void perCallNeverEmitsVerbatim() { + assertThat(d.quoteIdentifierWith("firstName", IdentifierQuotingPolicy.NEVER).toString()) + .isEqualTo("firstName"); + } + + @Test + void perCallAlwaysOnANeverPolicyDialect_quotes() { + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + assertThat(d.quoteIdentifier("EMPLOYEES").toString()).isEqualTo("EMPLOYEES"); + assertThat(d.quoteIdentifierWith("EMPLOYEES", IdentifierQuotingPolicy.ALWAYS).toString()) + .isEqualTo("\"EMPLOYEES\""); + // dialect-level state preserved + assertThat(d.quotingPolicy()).isEqualTo(IdentifierQuotingPolicy.NEVER); + } + + @Test + void nullPolicyTreatedAsAlways() { + assertThat(d.quoteIdentifierWith("EMPLOYEES", null).toString()).isEqualTo("\"EMPLOYEES\""); + } + } + + @Nested + class CaseFoldingHelper { + @Test + void upperFoldsToUpper() { + assertThat(IdentifierCaseFolding.UPPER.fold("Foo")).isEqualTo("FOO"); + assertThat(IdentifierCaseFolding.UPPER.isCanonical("FOO")).isTrue(); + assertThat(IdentifierCaseFolding.UPPER.isCanonical("Foo")).isFalse(); + } + + @Test + void lowerFoldsToLower() { + assertThat(IdentifierCaseFolding.LOWER.fold("Foo")).isEqualTo("foo"); + assertThat(IdentifierCaseFolding.LOWER.isCanonical("foo")).isTrue(); + assertThat(IdentifierCaseFolding.LOWER.isCanonical("Foo")).isFalse(); + } + + @Test + void preservePreserves() { + assertThat(IdentifierCaseFolding.PRESERVE.fold("Foo")).isEqualTo("Foo"); + assertThat(IdentifierCaseFolding.PRESERVE.isCanonical("Foo")).isTrue(); + assertThat(IdentifierCaseFolding.PRESERVE.isCanonical("FOO")).isTrue(); + } + + @Test + void nullSafe() { + assertThat(IdentifierCaseFolding.UPPER.fold(null)).isNull(); + assertThat(IdentifierCaseFolding.UPPER.isCanonical(null)).isFalse(); + } + } +} diff --git a/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDialectTypeMapTest.java b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDialectTypeMapTest.java new file mode 100644 index 0000000..77e534e --- /dev/null +++ b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/JdbcDialectTypeMapTest.java @@ -0,0 +1,50 @@ +/* + * 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.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.junit.jupiter.api.Test; + +class JdbcDialectTypeMapTest { + + private final AbstractJdbcDialect dialect = new AbstractJdbcDialect(DialectInitData.ansiDefaults()) { + @Override + public String name() { + return null; + } + }; + + @Test + void testNumericPrecisionFiveScaleZeroMapsToInt() throws SQLException { + ResultSetMetaData rs = mock(ResultSetMetaData.class); + when(rs.getColumnType(1)).thenReturn(Types.NUMERIC); + when(rs.getPrecision(1)).thenReturn(5); + when(rs.getScale(1)).thenReturn(0); + assertEquals(BestFitColumnType.INT, dialect.getType(rs, 0)); + } + + @Test + void testDecimalPrecisionFiveScaleZeroMapsToInt() throws SQLException { + ResultSetMetaData rs = mock(ResultSetMetaData.class); + when(rs.getColumnType(1)).thenReturn(Types.DECIMAL); + when(rs.getPrecision(1)).thenReturn(5); + when(rs.getScale(1)).thenReturn(0); + assertEquals(BestFitColumnType.INT, dialect.getType(rs, 0)); + } +} diff --git a/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/KnownFunctionDefaultSpellingTest.java b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/KnownFunctionDefaultSpellingTest.java new file mode 100644 index 0000000..a6e729b --- /dev/null +++ b/dialect/db/common/src/test/java/org/eclipse/daanse/sql/dialect/db/common/KnownFunctionDefaultSpellingTest.java @@ -0,0 +1,95 @@ +/* + * 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.common; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.daanse.sql.dialect.api.generator.FunctionGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** Table test for the ANSI/most-portable default spellings of {@link KnownFunction}. */ +class KnownFunctionDefaultSpellingTest { + + private final FunctionGenerator generator = new AbstractJdbcDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()) { + + @Override + public String name() { + return null; + } + + }.functionGenerator(); + + static Stream defaultSpellings() { + return Stream.of( // + Arguments.of(KnownFunction.SUBSTRING, List.of("x", "2"), "SUBSTRING(x, 2)"), + Arguments.of(KnownFunction.SUBSTRING, List.of("x", "2", "3"), "SUBSTRING(x, 2, 3)"), + Arguments.of(KnownFunction.LENGTH, List.of("x"), "CHAR_LENGTH(x)"), + Arguments.of(KnownFunction.CONCAT, List.of("a", "b"), "(a || b)"), + Arguments.of(KnownFunction.CONCAT, List.of("a", "b", "c"), "(a || b || c)"), + Arguments.of(KnownFunction.INDEX_OF, List.of("n", "h"), "POSITION(n IN h)"), + Arguments.of(KnownFunction.TRIM, List.of("x"), "TRIM(x)"), + Arguments.of(KnownFunction.LTRIM, List.of("x"), "TRIM(LEADING FROM x)"), + Arguments.of(KnownFunction.RTRIM, List.of("x"), "TRIM(TRAILING FROM x)"), + Arguments.of(KnownFunction.YEAR, List.of("x"), "EXTRACT(YEAR FROM x)"), + Arguments.of(KnownFunction.MONTH, List.of("x"), "EXTRACT(MONTH FROM x)"), + Arguments.of(KnownFunction.DAY, List.of("x"), "EXTRACT(DAY FROM x)"), + Arguments.of(KnownFunction.HOUR, List.of("x"), "EXTRACT(HOUR FROM x)"), + Arguments.of(KnownFunction.MINUTE, List.of("x"), "EXTRACT(MINUTE FROM x)"), + Arguments.of(KnownFunction.SECOND, List.of("x"), "EXTRACT(SECOND FROM x)"), + Arguments.of(KnownFunction.DATE, List.of("x"), "CAST(x AS DATE)"), + Arguments.of(KnownFunction.TIME, List.of("x"), "CAST(x AS TIME)"), + Arguments.of(KnownFunction.ROUND, List.of("x"), "ROUND(x)"), + Arguments.of(KnownFunction.ROUND, List.of("x", "2"), "ROUND(x, 2)"), + Arguments.of(KnownFunction.FLOOR, List.of("x"), "FLOOR(x)"), + Arguments.of(KnownFunction.CEILING, List.of("x"), "CEILING(x)"), + Arguments.of(KnownFunction.ABS, List.of("x"), "ABS(x)"), + Arguments.of(KnownFunction.MOD, List.of("a", "b"), "MOD(a, b)"), + Arguments.of(KnownFunction.POWER, List.of("a", "b"), "POWER(a, b)"), + Arguments.of(KnownFunction.SQRT, List.of("x"), "SQRT(x)"), + Arguments.of(KnownFunction.NOW, List.of(), "CURRENT_TIMESTAMP")); + } + + @ParameterizedTest + @MethodSource("defaultSpellings") + void default_spelling(KnownFunction function, List arguments, String expected) { + assertThat(generator.generateKnownFunction(function, arguments)).hasToString(expected); + } + + static Stream arityViolations() { + return Stream.of( // + Arguments.of(KnownFunction.SUBSTRING, List.of("x")), + Arguments.of(KnownFunction.SUBSTRING, List.of("x", "2", "3", "4")), + Arguments.of(KnownFunction.LENGTH, List.of("x", "y")), + Arguments.of(KnownFunction.CONCAT, List.of("a")), + Arguments.of(KnownFunction.INDEX_OF, List.of("n")), + Arguments.of(KnownFunction.TRIM, List.of()), + Arguments.of(KnownFunction.YEAR, List.of("x", "y")), + Arguments.of(KnownFunction.ROUND, List.of("x", "2", "3")), + Arguments.of(KnownFunction.MOD, List.of("a")), + Arguments.of(KnownFunction.NOW, List.of("x"))); + } + + @ParameterizedTest + @MethodSource("arityViolations") + void arity_violation_names_function_and_expected_arity(KnownFunction function, List arguments) { + assertThatThrownBy(() -> generator.generateKnownFunction(function, arguments)) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining(function.name()) + .hasMessageContaining("argument"); + } +} diff --git a/dialect/db/derby/pom.xml b/dialect/db/derby/pom.xml new file mode 100644 index 0000000..9d89f5d --- /dev/null +++ b/dialect/db/derby/pom.xml @@ -0,0 +1,80 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.derby + Eclipse Daanse JDBC DB Dialect Derby + Apache Derby database dialect implementation providing + Derby-specific SQL generation, query optimization, and embedded database + feature handling. Supports Derby embedded and server modes with + comprehensive SQL support for analytical operations. + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + org.apache.derby + derby + 10.16.1.1 + test + + + org.apache.derby + derbytools + 10.16.1.1 + test + + + diff --git a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java new file mode 100644 index 0000000..e956045 --- /dev/null +++ b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialect.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.derby; + +import java.sql.Date; +import java.util.List; + +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * @author jhyde + * @since Nov 23, 2008 + */ +public class DerbyDialect extends AbstractJdbcDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "DERBY"; + + /** JDBC-free constructor for SQL generation. */ + public DerbyDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public DerbyDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + /** Derby has no {@code IF NOT EXISTS} on any DDL. */ + @Override + public boolean supportsCreateTableIfNotExists() { + return false; + } + + @Override + public boolean supportsCreateIndexIfNotExists() { + return false; + } + + @Override + public boolean supportsDropIndexIfExists() { + return false; + } + + @Override + public boolean supportsCreateOrReplaceView() { + return false; + } + + @Override + public boolean supportsDropViewIfExists() { + return false; + } + + @Override + public boolean supportsDropConstraintIfExists() { + return false; + } + + @Override + public boolean supportsDropSchemaIfExists() { + return false; + } + + /** Derby's {@code DROP TABLE} doesn't accept {@code CASCADE}. */ + @Override + public boolean supportsDropTableCascade() { + return false; + } + + @Override + public boolean supportsDropTableIfExists() { + return false; + } + + /** Derby has no {@code IF NOT EXISTS} on {@code CREATE SCHEMA} — strip it. */ + @Override + public String createSchema(String schemaName, boolean ifNotExists) { + return "CREATE SCHEMA " + quoteIdentifier(schemaName); + } + + /** + * Derby requires the SQL-92 {@code RESTRICT} keyword on {@code DROP SCHEMA}. + */ + @Override + public boolean requiresDropSchemaRestrict() { + return true; + } + + @Override + protected void quoteDateLiteral(StringBuilder buf, Date date) { + // Derby accepts DATE('2008-01-23') but not SQL:2003 format. + buf.append("DATE("); + DialectUtil.singleQuoteString(date.toString(), buf); + buf.append(")"); + } + + @Override + public boolean requiresAliasForFromQuery() { + return true; + } + + @Override + public boolean allowsMultipleCountDistinct() { + // Derby allows at most one distinct-count per query. + return false; + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineForAnsi("t", columnNames, columnTypes, valueList, true); + } + + @Override + public boolean supportsGroupByExpressions() { + return false; + } + + @Override + public boolean allowsFieldAlias() { + // Derby fully supports (quoted) field aliases — `select x as "Store Sqft"` executes + // fine (the false setting was inherited for DB2/AS400-style dialects and made the + // engine drop select-list aliases, so drill-through result labels degraded to the + // physical column names and per-dialect SQL asserts diverged). + return true; + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } +} diff --git a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectFactory.java b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectFactory.java new file mode 100644 index 0000000..42e4959 --- /dev/null +++ b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.derby; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@DialectName("DERBY") +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +public class DerbyDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return DerbyDialect::new; + } + +} diff --git a/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/package-info.java b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/package-info.java new file mode 100644 index 0000000..dc4a740 --- /dev/null +++ b/dialect/db/derby/src/main/java/org/eclipse/daanse/sql/dialect/db/derby/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.derby; diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectQuotingPolicyTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectQuotingPolicyTest.java new file mode 100644 index 0000000..7871aa4 --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * 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.derby; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class DerbyDialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private DerbyDialect dialectNever() { + DerbyDialect d = new DerbyDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("\"").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("\"").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("\"").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("\"") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java new file mode 100644 index 0000000..5b8bca5 --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/DerbyDialectTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.derby; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DerbyDialectTest { + + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private DerbyDialect dialect; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("Apache Derby"); + when(metaData.getDatabaseProductVersion()).thenReturn("10.15.2.0"); + dialect = new DerbyDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + } + + @Test + void testGetDialectName() { + assertEquals("derby", dialect.name()); + } + + @Test + void testRequiresAliasForFromQuery() { + assertTrue(dialect.requiresAliasForFromQuery()); + } + + @Test + void testAllowsMultipleCountDistinct() { + assertFalse(dialect.allowsMultipleCountDistinct()); + } + + @Test + void testSupportsGroupByExpressions() { + assertFalse(dialect.supportsGroupByExpressions()); + } + + @Test + void testAllowsFieldAs() { + // Derby supports (quoted) select-list aliases, including ones with spaces. + assertTrue(dialect.allowsFieldAlias()); + } + + @Test + void testQuoteDateLiteral() { + StringBuilder buf = new StringBuilder(); + dialect.quoteDateLiteral(buf, java.sql.Date.valueOf("2024-01-15")); + assertEquals("DATE('2024-01-15')", buf.toString()); + } + +} diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/integration/ServiceTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/integration/ServiceTest.java new file mode 100644 index 0000000..b140fac --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.derby.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.derby.DerbyDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(DerbyDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyDdlRoundTripTest.java b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyDdlRoundTripTest.java new file mode 100644 index 0000000..39386cb --- /dev/null +++ b/dialect/db/derby/src/test/java/org/eclipse/daanse/sql/dialect/db/derby/sqlgen/DerbyDdlRoundTripTest.java @@ -0,0 +1,239 @@ +/* + * 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.derby.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.derby.DerbyDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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; + +@TestInstance(Lifecycle.PER_CLASS) +class DerbyDdlRoundTripTest { + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference ORDERS = new TableReference(Optional.of(SCHEMA), "ORDERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", + TableReference.TYPE_VIEW); + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + Class.forName("org.apache.derby.iapi.jdbc.AutoloadedDriver"); + this.connection = DriverManager.getConnection("jdbc:derby:memory:rt_test;create=true"); + this.dialect = new DerbyDialect(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — Derby supports correlated scalar subqueries. + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + try (PreparedStatement ps = connection.prepareStatement("UPDATE " + qC + " SET " + + dialect.quoteIdentifier("NAME") + " = (" + " SELECT MAX(" + dialect.quoteIdentifier("NOTE") + ")" + + " FROM " + qO + " WHERE " + qO + "." + dialect.quoteIdentifier("CUSTOMER_ID") + " = " + qC + "." + + dialect.quoteIdentifier("ID") + ")")) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropSchema(SCHEMA.name(), true, true), info -> { + }); + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/duckdb/pom.xml b/dialect/db/duckdb/pom.xml new file mode 100644 index 0000000..b0a1f74 --- /dev/null +++ b/dialect/db/duckdb/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.duckdb + Eclipse Daanse JDBC DB Dialect DuckDB + DuckDB database dialect implementation. DuckDB is an embedded, + Postgres-flavored ANSI analytical database; this dialect mostly inherits + the ANSI defaults and only pins the LIMIT/OFFSET pagination form, native + NULLS FIRST/LAST ordering and GROUPING SETS support. + + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + diff --git a/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java new file mode 100644 index 0000000..b3b909a --- /dev/null +++ b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialect.java @@ -0,0 +1,206 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +*/ +package org.eclipse.daanse.sql.dialect.db.duckdb; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * Dialect for DuckDB (embedded, Postgres-flavored ANSI analytical database). + * + *

DuckDB tracks PostgreSQL syntax closely: identifiers are quoted with + * double quotes (the ANSI default reported by the driver), identifier matching + * is case-insensitive but case-preserving, and {@code LIMIT n OFFSET m}, + * {@code ORDER BY ... NULLS FIRST/LAST} and {@code GROUPING SETS} / + * {@code ROLLUP} / {@code CUBE} are all documented core features. Those are + * the only capabilities this dialect pins; everything else inherits the + * conservative ANSI defaults of {@link AbstractJdbcDialect}. + * + *

Regular expressions in {@code WHERE} are supported via the documented + * {@code regexp_matches(string, pattern[, options])} scalar function (RE2 + * engine). See {@link #generateRegularExpression(String, String)} for the + * Java-regex translation choices. + * + *

Capabilities deliberately left at their conservative inherited defaults + * (DuckDB may well support them, but they are not needed and not verified + * here): + *

    + *
  • {@code supportsMultiValueInExpr()} — row-value {@code IN} lists are not + * relied upon.
  • + *
  • {@code allowsCompoundCountDistinct()} — multi-column + * {@code COUNT(DISTINCT a, b)} stays disabled.
  • + *
  • {@code FETCH NEXT ... ROWS ONLY} pagination — DuckDB documents + * {@code LIMIT/OFFSET}; the ANSI fetch-first form is not assumed, hence the + * explicit {@link #paginationGenerator()} override.
  • + *
+ */ +public class DuckDbDialect extends AbstractJdbcDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "DUCKDB"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; + + /** JDBC-free constructor for SQL generation. */ + public DuckDbDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public DuckDbDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + /** + * DuckDB matches unquoted identifiers case-insensitively but preserves the + * case they were created with — no automatic folding. + */ + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.PRESERVE; + } + + /** + * DuckDB supports regular-expression predicates in {@code WHERE} through the + * documented {@code regexp_matches()} scalar function. + */ + @Override + public boolean allowsRegularExpressionInWhereClause() { + return true; + } + + /** + * duckdb_jdbc silently ignores {@link java.sql.Statement#setMaxRows(int)}, so the + * drill-through row limit must be rendered into the SQL as {@code LIMIT n} + * (same treatment as ClickHouse). + */ + @Override + public boolean requiresDrillthroughMaxRowsInLimit() { + return true; + } + + /** + * Translate a Java regex into a DuckDB {@code regexp_matches} predicate. + * + *

Semantics and translation choices (kept deliberately in line with the + * MySQL/PostgreSQL generators so native and non-native evaluation agree on + * the engine's {@code .*}-wrapped MATCHES patterns): + *

    + *
  • {@code regexp_matches(string, pattern[, options])} is DuckDB's + * documented partial-match predicate (true if the pattern matches anywhere + * in the string) — the same contains-semantics as MySQL {@code REGEXP} and + * PostgreSQL {@code ~} used by those dialects' generators.
  • + *
  • A leading Java {@code (?i)} embedded flag is extracted and passed as + * the documented {@code 'i'} (case-insensitive) options argument instead of + * being left inline.
  • + *
  • DuckDB's regex engine is RE2, which does not support Java's + * {@code \Q…\E} literal quoting — quoted sections are unescaped the same + * way the MySQL generator does.
  • + *
  • A {@code source IS NOT NULL AND} guard is prepended, matching the + * other generators, so NULL captions never match.
  • + *
  • If the input is not a valid Java regex the translation is refused + * (empty result), which makes the engine fall back to non-native + * evaluation.
  • + *
+ */ + @Override + public Optional generateRegularExpression(String source, String javaRegex) { + try { + Pattern.compile(javaRegex); + } catch (PatternSyntaxException e) { + // Not a valid Java regex. Too risky to continue. + return Optional.empty(); + } + javaRegex = DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex); + StringBuilder mappedFlags = new StringBuilder(); + String[][] mapping = new String[][] { { "i", "i" } }; + javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags); + // RE2 has no \Q...\E support: unescape quoted sections (same as MySqlDialect). + final Matcher escapeMatcher = DialectUtil.ESCAPE_PATTERN.matcher(javaRegex); + while (escapeMatcher.find()) { + javaRegex = javaRegex.replace(escapeMatcher.group(1), escapeMatcher.group(2)); + } + final StringBuilder sb = new StringBuilder(); + sb.append(source); + // regexp_matches needs VARCHAR; DuckDB won't coerce numeric columns (e.g. store_sqft), + // so cast like PostgreSqlDialect does. + sb.append(" IS NOT NULL AND regexp_matches(CAST("); + sb.append(source); + sb.append(" AS VARCHAR), "); + quoteStringLiteral(sb, javaRegex); + if (mappedFlags.length() > 0) { + sb.append(", "); + quoteStringLiteral(sb, mappedFlags.toString()); + } + sb.append(")"); + return Optional.of(sb.toString()); + } + + /** DuckDB supports {@code GROUPING SETS}, {@code ROLLUP} and {@code CUBE}. */ + @Override + public boolean supportsGroupingSets() { + return true; + } + + /** DuckDB supports {@code ORDER BY ... NULLS FIRST/LAST} natively. */ + @Override + protected boolean supportsNullsOrdering() { + return true; + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + return generateOrderByNullsAnsi(expr, ascending, collateNullsLast); + } + + /** + * DuckDB: {@code LIMIT n OFFSET m} (Postgres form) — both clauses optional. + * The inherited ANSI {@code OFFSET ... ROWS FETCH NEXT ... ROWS ONLY} form + * is not assumed to be supported. + */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + var local = cachedPaginationGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + StringBuilder sb = new StringBuilder(); + limit.ifPresent(l -> { + if (l < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" LIMIT ").append(l); + }); + offset.ifPresent(o -> { + if (o < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" OFFSET ").append(o); + }); + return sb.toString(); + } + }; + cachedPaginationGenerator = local; + return local; + } + +} diff --git a/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectFactory.java b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectFactory.java new file mode 100644 index 0000000..d463bd5 --- /dev/null +++ b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectFactory.java @@ -0,0 +1,37 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +*/ +package org.eclipse.daanse.sql.dialect.db.duckdb; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** + * Factory for {@link DuckDbDialect}. The JDBC driver reports the database + * product name as {@code "DuckDB"}. + */ +@DialectName("DUCKDB") +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +public class DuckDbDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return DuckDbDialect::new; + } + +} diff --git a/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/package-info.java b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/package-info.java new file mode 100644 index 0000000..b674e4b --- /dev/null +++ b/dialect/db/duckdb/src/main/java/org/eclipse/daanse/sql/dialect/db/duckdb/package-info.java @@ -0,0 +1,16 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.duckdb; diff --git a/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectCapabilitiesTest.java b/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectCapabilitiesTest.java new file mode 100644 index 0000000..2e7fb2b --- /dev/null +++ b/dialect/db/duckdb/src/test/java/org/eclipse/daanse/sql/dialect/db/duckdb/DuckDbDialectCapabilitiesTest.java @@ -0,0 +1,24 @@ +/* + * 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.duckdb; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class DuckDbDialectCapabilitiesTest { + + @Test + void drillthroughMaxRowsMustRenderAsLimit() { + // duckdb_jdbc silently ignores Statement.setMaxRows, so the drill-through + // row limit must be inlined as LIMIT n (same treatment as ClickHouse). + assertTrue(new DuckDbDialect().requiresDrillthroughMaxRowsInLimit()); + } +} diff --git a/dialect/db/h2/pom.xml b/dialect/db/h2/pom.xml new file mode 100644 index 0000000..4313025 --- /dev/null +++ b/dialect/db/h2/pom.xml @@ -0,0 +1,55 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.h2 + Eclipse Daanse JDBC DB Dialect H2 + H2 database dialect implementation providing H2-specific SQL + generation, query optimization, and database feature handling. Supports H2 + embedded and server modes with comprehensive SQL dialect support for + analytical and transactional operations. + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + com.h2database + h2 + 2.2.224 + test + + + diff --git a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java new file mode 100644 index 0000000..fbc7733 --- /dev/null +++ b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2Dialect.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.h2; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class H2Dialect extends AbstractJdbcDialect { + + private static final Logger LOGGER = LoggerFactory.getLogger(H2Dialect.class); + + private static final String SUPPORTED_PRODUCT_NAME = "H2"; + + /** JDBC-free constructor for SQL generation. */ + public H2Dialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + protected boolean supportsNullsOrdering() { + return true; // H2 supports NULLS FIRST/LAST + } + + // Unified BitOperation methods + + @Override + public java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + StringBuilder buf = new StringBuilder(64); + StringBuilder result = switch (operation) { + case AND -> buf.append("BIT_AND_AGG(").append(operand).append(")"); + case OR -> buf.append("BIT_OR_AGG(").append(operand).append(")"); + case XOR -> buf.append("BIT_XOR_AGG(").append(operand).append(")"); + case NAND -> buf.append("BIT_NAND_AGG(").append(operand).append(")"); + case NOR -> buf.append("BIT_NOR_AGG(").append(operand).append(")"); + case NXOR -> buf.append("BIT_XNOR_AGG(").append(operand).append(")"); + }; + return java.util.Optional.of(result.toString()); + } + + @Override + public boolean supportsBitAggregation(BitOperation operation) { + return true; // H2 supports all bit operations + } + + @Override + public java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_DISC", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_CONT", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + StringBuilder buf = new StringBuilder(64); + buf.append("LISTAGG"); + buf.append("( "); + if (distinct) { + buf.append("DISTINCT "); + } + if (coalesce != null) { + buf.append("COALESCE(").append(operand).append(", '").append(coalesce).append("')"); + } else { + buf.append(operand); + } + buf.append(", '"); + if (separator != null) { + buf.append(separator); + } else { + buf.append(", "); + } + buf.append("'"); + if (onOverflowTruncate != null) { + buf.append(" ON OVERFLOW TRUNCATE '").append(onOverflowTruncate).append("' WITHOUT COUNT)"); + } else { + buf.append(")"); + } + if (columns != null && !columns.isEmpty()) { + buf.append(" WITHIN GROUP (ORDER BY "); + buf.append(buildOrderedColumnsClause(columns)); + buf.append(")"); + } + return java.util.Optional.of((buf).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("NTH_VALUE", operand, ignoreNulls, n, columns, true)).toString()); + } + + @Override + public boolean supportsPercentileDisc() { + return true; + } + + @Override + public boolean supportsPercentileCont() { + return true; + } + + @Override + public boolean supportsNthValue() { + return true; + } + + @Override + public boolean supportsNthValueIgnoreNulls() { + return true; + } + + @Override + public boolean supportsListAgg() { + return true; + } +} diff --git a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectFactory.java b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectFactory.java new file mode 100644 index 0000000..b61b5b8 --- /dev/null +++ b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.h2; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("H2") +public class H2DialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return H2Dialect::new; + } + +} diff --git a/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/package-info.java b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/package-info.java new file mode 100644 index 0000000..001ede5 --- /dev/null +++ b/dialect/db/h2/src/main/java/org/eclipse/daanse/sql/dialect/db/h2/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.h2; diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectQuotingPolicyTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectQuotingPolicyTest.java new file mode 100644 index 0000000..318efea --- /dev/null +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class H2DialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private H2Dialect dialectNever() { + H2Dialect d = new H2Dialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("\"").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("\"").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("\"").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("\"") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java new file mode 100644 index 0000000..a83bea8 --- /dev/null +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/H2DialectTest.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.h2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.sql.NullsOrder; +import org.eclipse.daanse.sql.model.sql.SortDirection; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class H2DialectTest { + + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private H2Dialect dialect; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("H2"); + when(metaData.getDatabaseProductVersion()).thenReturn("2.2.224"); + dialect = new H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + } + + @Test + void testGetDialectName() { + assertEquals("h2", dialect.name()); + } + + @Nested + @DisplayName("Bit Aggregation Tests") + class BitAggregationTests { + + @ParameterizedTest + @EnumSource(BitOperation.class) + void testSupportsBitAggregation(BitOperation operation) { + assertTrue(dialect.supportsBitAggregation(operation)); + } + + @Test + void testGenerateBitAggregation_AND() { + String result = dialect.generateBitAggregation(BitOperation.AND, "column1").orElseThrow(); + assertEquals("BIT_AND_AGG(column1)", result); + } + + @Test + void testGenerateBitAggregation_OR() { + String result = dialect.generateBitAggregation(BitOperation.OR, "column1").orElseThrow(); + assertEquals("BIT_OR_AGG(column1)", result); + } + + @Test + void testGenerateBitAggregation_XOR() { + String result = dialect.generateBitAggregation(BitOperation.XOR, "column1").orElseThrow(); + assertEquals("BIT_XOR_AGG(column1)", result); + } + + @Test + void testGenerateBitAggregation_NAND() { + String result = dialect.generateBitAggregation(BitOperation.NAND, "column1").orElseThrow(); + assertEquals("BIT_NAND_AGG(column1)", result); + } + + @Test + void testGenerateBitAggregation_NOR() { + String result = dialect.generateBitAggregation(BitOperation.NOR, "column1").orElseThrow(); + assertEquals("BIT_NOR_AGG(column1)", result); + } + + @Test + void testGenerateBitAggregation_NXOR() { + String result = dialect.generateBitAggregation(BitOperation.NXOR, "column1").orElseThrow(); + assertEquals("BIT_XNOR_AGG(column1)", result); + } + } + + @Nested + @DisplayName("Window Function Support Tests") + class WindowFunctionSupportTests { + + @Test + void testSupportsPercentileDisc() { + assertTrue(dialect.supportsPercentileDisc()); + } + + @Test + void testSupportsPercentileCont() { + assertTrue(dialect.supportsPercentileCont()); + } + + @Test + void testSupportsNthValue() { + assertTrue(dialect.supportsNthValue()); + } + + @Test + void testSupportsNthValueIgnoreNulls() { + assertTrue(dialect.supportsNthValueIgnoreNulls()); + } + + @Test + void testSupportsListAgg() { + assertTrue(dialect.supportsListAgg()); + } + } + + @Nested + @DisplayName("ListAgg Generation Tests") + class ListAggGenerationTests { + + @Test + void testGenerateListAgg_Simple() { + String result = dialect.generateListAgg("column1", false, null, null, null, null).orElseThrow(); + assertEquals("LISTAGG( column1, ', ')", result); + } + + @Test + void testGenerateListAgg_WithDistinct() { + String result = dialect.generateListAgg("column1", true, null, null, null, null).orElseThrow(); + assertEquals("LISTAGG( DISTINCT column1, ', ')", result); + } + + @Test + void testGenerateListAgg_WithSeparator() { + String result = dialect.generateListAgg("column1", false, ";", null, null, null).orElseThrow(); + assertEquals("LISTAGG( column1, ';')", result); + } + + @Test + void testGenerateListAgg_WithCoalesce() { + String result = dialect.generateListAgg("column1", false, null, "N/A", null, null).orElseThrow(); + assertEquals("LISTAGG( COALESCE(column1, 'N/A'), ', ')", result); + } + + @Test + void testGenerateListAgg_WithOverflowTruncate() { + String result = dialect.generateListAgg("column1", false, null, null, "...", null).orElseThrow(); + assertEquals("LISTAGG( column1, ', ' ON OVERFLOW TRUNCATE '...' WITHOUT COUNT)", result); + } + + @Test + void testGenerateListAgg_WithOrderBy_DbDefault() { + List columns = List.of(new OrderedColumn("id", null)); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id)", result); + } + + @Test + void testGenerateListAgg_WithOrderByAsc() { + List columns = List.of(new OrderedColumn("id", null, SortDirection.ASC)); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id ASC)", result); + } + + @Test + void testGenerateListAgg_WithOrderByDesc() { + List columns = List.of(new OrderedColumn("id", null, SortDirection.DESC)); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id DESC)", result); + } + + @Test + void testGenerateListAgg_WithNullsFirst() { + List columns = List + .of(new OrderedColumn("id", null, Optional.empty(), Optional.of(NullsOrder.FIRST))); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id NULLS FIRST)", result); + } + + @Test + void testGenerateListAgg_WithNullsLast() { + List columns = List + .of(new OrderedColumn("id", null, Optional.empty(), Optional.of(NullsOrder.LAST))); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id NULLS LAST)", result); + } + + @Test + void testGenerateListAgg_WithDescNullsFirst() { + List columns = List + .of(new OrderedColumn("id", null, Optional.of(SortDirection.DESC), Optional.of(NullsOrder.FIRST))); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id DESC NULLS FIRST)", result); + } + + @Test + void testGenerateListAgg_WithAscNullsLast() { + List columns = List + .of(new OrderedColumn("id", null, Optional.of(SortDirection.ASC), Optional.of(NullsOrder.LAST))); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id ASC NULLS LAST)", result); + } + } + + @Nested + @DisplayName("OrderedColumn Factory Method Tests") + class OrderedColumnFactoryTests { + + @Test + void testOrderedColumn_FactoryAsc() { + List columns = List.of(OrderedColumn.asc("id")); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id ASC)", result); + } + + @Test + void testOrderedColumn_FactoryDesc() { + List columns = List.of(OrderedColumn.desc("id")); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id DESC)", result); + } + + @Test + void testOrderedColumn_FactoryOf() { + List columns = List.of(OrderedColumn.of("id", SortDirection.DESC, NullsOrder.FIRST)); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id DESC NULLS FIRST)", result); + } + + @Test + void testOrderedColumn_FactoryOfNullDirection() { + List columns = List.of(OrderedColumn.of("id", null, NullsOrder.LAST)); + String result = dialect.generateListAgg("column1", false, null, null, null, columns).orElseThrow(); + assertEquals("LISTAGG( column1, ', ') WITHIN GROUP (ORDER BY id NULLS LAST)", result); + } + } + + @Nested + @DisplayName("Percentile Generation Tests") + class PercentileGenerationTests { + + @Test + void testGeneratePercentileDisc() { + String result = dialect.generatePercentileDisc(0.5, false, "table1", "column1").orElseThrow(); + assertTrue(result.contains("PERCENTILE_DISC")); + assertTrue(result.contains("0.5")); + } + + @Test + void testGeneratePercentileCont() { + String result = dialect.generatePercentileCont(0.75, false, "table1", "column1").orElseThrow(); + assertTrue(result.contains("PERCENTILE_CONT")); + assertTrue(result.contains("0.75")); + } + } +} diff --git a/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/integration/ServiceTest.java b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/integration/ServiceTest.java new file mode 100644 index 0000000..ffdb9ad --- /dev/null +++ b/dialect/db/h2/src/test/java/org/eclipse/daanse/sql/dialect/db/h2/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.h2.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.h2.H2DialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(H2DialectFactory.class::isInstance); + } +} diff --git a/dialect/db/mariadb/pom.xml b/dialect/db/mariadb/pom.xml new file mode 100644 index 0000000..392003e --- /dev/null +++ b/dialect/db/mariadb/pom.xml @@ -0,0 +1,112 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.mariadb + Eclipse Daanse JDBC DB Dialect MariaDB + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.mysql + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + org.mariadb.jdbc + mariadb-java-client + 3.4.1 + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + org.testcontainers + mariadb + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + org.assertj + assertj-core + test + + + + + + maven-surefire-plugin + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java new file mode 100644 index 0000000..c8f406b --- /dev/null +++ b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialect.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ + +package org.eclipse.daanse.sql.dialect.db.mariadb; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; + +public class MariaDBDialect extends MySqlDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "MARIADB"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; + + /** JDBC-free constructor for SQL generation. */ + public MariaDBDialect() { + super(); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public MariaDBDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public boolean supportsSequences() { + return true; + } + + /** + * MariaDB accepts {@code IF NOT EXISTS} on {@code CREATE INDEX} (MySQL parent + * disables). + */ + @Override + public boolean supportsCreateIndexIfNotExists() { + return true; + } + + /** + * MariaDB accepts {@code IF EXISTS} on {@code DROP INDEX} (MySQL parent + * disables). + */ + @Override + public boolean supportsDropIndexIfExists() { + return true; + } + + @Override + public boolean supportsDropConstraintIfExists() { + return dialectVersion.isUnknownOrAtLeast(10, 5); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { + var local = cachedReturningGenerator; + if (local != null) + return local; + if (!dialectVersion.isUnknownOrAtLeast(10, 5)) { + local = super.returningGenerator(); + cachedReturningGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator() { + @Override + public boolean supportsReturning() { + return true; + } + + @Override + public java.util.Optional returning(java.util.List columns) { + if (columns == null || columns.isEmpty()) + return java.util.Optional.empty(); + if (columns.size() == 1 && "*".equals(columns.get(0))) { + return java.util.Optional.of(" RETURNING *"); + } + StringBuilder sb = new StringBuilder(" RETURNING "); + boolean first = true; + for (String c : columns) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)); + first = false; + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedReturningGenerator = local; + return local; + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.PRESERVE; + } +} diff --git a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialectFactory.java b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialectFactory.java new file mode 100644 index 0000000..d119659 --- /dev/null +++ b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mariadb; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("MARIADB") +public class MariaDBDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return MariaDBDialect::new; + } + +} diff --git a/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/package-info.java b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/package-info.java new file mode 100644 index 0000000..ecf2988 --- /dev/null +++ b/dialect/db/mariadb/src/main/java/org/eclipse/daanse/sql/dialect/db/mariadb/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.mariadb; diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java new file mode 100644 index 0000000..f8b1bba --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBGeneratorsRoundTripTest.java @@ -0,0 +1,98 @@ +/* + * 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.mariadb; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +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.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MariaDBGeneratorsRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MariaDBContainer CONTAINER = new MariaDBContainer<>("mariadb:11").withDatabaseName("rt") + .withUsername("rt").withPassword("rt"); + + private Connection conn; + private MariaDBDialect dialect; + private TableReference users; + + @BeforeAll + void setUp() throws Exception { + conn = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), CONTAINER.getPassword()); + dialect = new MariaDBDialect(DialectInitData.fromConnection(conn)); + users = new TableReference(Optional.of(new SchemaReference(Optional.empty(), "rt")), "users", + TableReference.TYPE_TABLE); + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE `rt`.`users` (id INT PRIMARY KEY, name VARCHAR(50))"); + s.execute("INSERT INTO `rt`.`users` VALUES (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')"); + } + } + + @AfterAll + void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_limit_offset_executes() throws SQLException { + // MariaDB inherits MySQL's `LIMIT off, lim` shape from MySqlDialect. + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertThat(tail).isEqualTo(" LIMIT 1, 2"); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + tail, 2, 2); + } + + @Test + void returning_clause_returns_inserted_id() throws SQLException { + // MariaDB 10.5+ supports RETURNING (unlike MySQL); mariadb:11 reports >= 10.5. + String returning = dialect.returningGenerator().returning(List.of("id")).orElseThrow(); + assertFirstIntEquals(conn, "INSERT INTO `rt`.`users` (id, name) VALUES (99, 'returned')" + returning, 99); + } + + @Test + void upsert_on_duplicate_key_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), + List.of("name")); + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql1, 1, "upsert: insert path"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + // MariaDB matches MySQL's "row count = 2 for update" semantics. + assertExecuteUpdateAffected(conn, sql2, 2, "upsert: update path"); + assertFirstStringEquals(conn, "SELECT name FROM `rt`.`users` WHERE id = 10", "second"); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBInitMetaInfoLatencyTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..46c3682 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBInitMetaInfoLatencyTest.java @@ -0,0 +1,183 @@ +/* + * 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.mariadb; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +/** + * {@code createMetaInfo} vs lightweight dialect-init read latency on MariaDB + * 10.11. + */ +class MariaDBInitMetaInfoLatencyTest { + + private static final String DATABASE = "bench"; + private static final String USER = "root"; + private static final String PASSWORD = "test"; + + @SuppressWarnings("resource") + private static final GenericContainer MARIADB = new GenericContainer<>("mariadb:10.11") + .withEnv("MARIADB_ROOT_PASSWORD", PASSWORD).withEnv("MARIADB_DATABASE", DATABASE).withExposedPorts(3306) + .waitingFor( + Wait.forLogMessage(".*ready for connections.*\\n", 2).withStartupTimeout(Duration.ofMinutes(2))); + + private static String jdbcUrl; + private final DatabaseService service = new DatabaseServiceImpl(); + + @BeforeAll + static void setUp() throws Exception { + MARIADB.start(); + Class.forName("org.mariadb.jdbc.Driver"); + jdbcUrl = "jdbc:mariadb://" + MARIADB.getHost() + ":" + MARIADB.getMappedPort(3306) + "/" + DATABASE; + } + + @Test + void measure() throws SQLException { + DataSource ds = ds(); + System.out.println(); + System.out.println("=== MariaDB 10.11: createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100, 500 }) { + resetSchema(); + populate(n); + for (int i = 0; i < 2; i++) { + service.createMetaInfo(ds); + lightweight(ds); + } + long meta = bestOf(3, () -> service.createMetaInfo(ds)); + long light = bestOf(3, () -> lightweight(ds)); + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", n + " tables", meta / 1_000_000.0, + light / 1_000_000.0, light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + private DataSource ds() { + return new javax.sql.DataSource() { + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl, USER, PASSWORD); + } + + @Override + public Connection getConnection(String u, String p) throws SQLException { + return DriverManager.getConnection(jdbcUrl, u, p); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter w) { + } + + @Override + public void setLoginTimeout(int s) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return null; + } + + @Override + public T unwrap(Class i) { + return null; + } + + @Override + public boolean isWrapperFor(Class i) { + return false; + } + }; + } + + private static void lightweight(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) + best = t; + } + return best; + } + + private static void resetSchema() throws SQLException { + try (Connection c = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); Statement s = c.createStatement()) { + s.execute("DROP DATABASE " + DATABASE); + s.execute("CREATE DATABASE " + DATABASE); + } + } + + private static void populate(int n) throws SQLException { + if (n == 0) + return; + try (Connection c = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); Statement s = c.createStatement()) { + for (int i = 0; i < n; i++) { + s.execute("CREATE TABLE T_" + i + " (ID INT PRIMARY KEY, NAME VARCHAR(50)," + + " VAL DECIMAL(12,3), BIRTHDAY DATE, CREATED DATETIME)"); + if (i > 0) { + s.execute("ALTER TABLE T_" + i + " ADD CONSTRAINT FK_" + i + " FOREIGN KEY (ID) REFERENCES T_" + + (i - 1) + "(ID)"); + } + s.execute("CREATE INDEX IDX_" + i + "_NAME ON T_" + i + "(NAME)"); + } + } + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBKnownFunctionTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBKnownFunctionTest.java new file mode 100644 index 0000000..11cb01e --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBKnownFunctionTest.java @@ -0,0 +1,33 @@ +/* + * 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.mariadb; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.junit.jupiter.api.Test; + +/** MariaDB extends MySqlDialect; it must inherit the MySQL KnownFunction spellings. */ +class MariaDBKnownFunctionTest { + + private final MariaDBDialect d = new MariaDBDialect(); + + @Test + void inherits_mysql_known_function_overrides() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.CONCAT, List.of("a", "b"))) + .hasToString("CONCAT(a, b)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h"))) + .hasToString("LOCATE(n, h)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.LENGTH, List.of("x"))) + .hasToString("CHAR_LENGTH(x)"); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBQuotingPolicyTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBQuotingPolicyTest.java new file mode 100644 index 0000000..e41e8c6 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBQuotingPolicyTest.java @@ -0,0 +1,59 @@ +/* + * 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.mariadb; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class MariaDBQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + + private MariaDBDialect dialectNever() { + MariaDBDialect d = new MariaDBDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("`").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void renameIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", EMP)).doesNotContain("`") + .contains("EMPLOYEES").contains("IDX_OLD").contains("IDX_NEW"); + } + + @Test + void createTriggerProcedure_unquoted() { + // MariaDB inherits MySQL's CREATE PROCEDURE wrapper. + assertThat( + dialectNever().ddlGenerator().createTriggerProcedure("AUDIT_PROC", "TEST", "BEGIN END").orElseThrow()) + .doesNotContain("`").contains("AUDIT_PROC").contains("TEST"); + } + + @Test + void dropProcedure_unquoted() { + assertThat(dialectNever().ddlGenerator().dropProcedure("AUDIT_PROC", "TEST", true).orElseThrow()) + .doesNotContain("`").contains("AUDIT_PROC").contains("TEST"); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBVersionCapabilitiesTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBVersionCapabilitiesTest.java new file mode 100644 index 0000000..6841ef6 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/MariaDBVersionCapabilitiesTest.java @@ -0,0 +1,55 @@ +/* + * 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.mariadb; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.junit.jupiter.api.Test; + +/** + * MariaDB reports two-digit major versions ("10.x"/"11.x"). The inherited MySQL + * capability checks must compare numerically — a lexicographic productVersion + * comparison classifies "11.4" < "5.7"/"8.0" and flips the capabilities. + */ +class MariaDBVersionCapabilitiesTest { + + private static MariaDBDialect dialect(int major, int minor) { + return new MariaDBDialect(DialectInitData.ansiDefaults() + .withQuoteIdentifierString("`").withVersion(major, minor)); + } + + @Test + void twoDigitMajorIsAtLeast57ForOrderByAlias() { + assertTrue(dialect(11, 4).requiresOrderByAlias()); + assertTrue(dialect(10, 6).requiresOrderByAlias()); + } + + @Test + void twoDigitMajorSupportsPercentiles() { + assertTrue(dialect(11, 4).supportsPercentileDisc()); + assertTrue(dialect(11, 4).supportsPercentileCont()); + assertTrue(dialect(10, 6).supportsPercentileDisc()); + } + + @Test + void twoDigitMajorAllowsFromQuery() { + assertTrue(dialect(11, 4).allowsFromQuery()); + } + + @Test + void oldMySqlStyleVersionsStillGateCorrectly() { + assertFalse(dialect(5, 6).requiresOrderByAlias()); + assertTrue(dialect(5, 7).requiresOrderByAlias()); + assertFalse(dialect(5, 7).supportsPercentileDisc()); + assertTrue(dialect(8, 0).supportsPercentileDisc()); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/integration/ServiceTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/integration/ServiceTest.java new file mode 100644 index 0000000..6d36e19 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/integration/ServiceTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.mariadb.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(MariaDBDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDBDdlRoundTripTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDBDdlRoundTripTest.java new file mode 100644 index 0000000..08124e5 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDBDdlRoundTripTest.java @@ -0,0 +1,262 @@ +/* + * 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.mariadb.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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.mariadb.jdbc.MariaDbDataSource; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MariaDBDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MariaDBContainer CONTAINER = new MariaDBContainer<>("mariadb:11.3") + // schema == database in MariaDB; provision under the test's name. + .withDatabaseName("RT_TEST").withUsername("rt").withPassword("rt"); + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference ORDERS = new TableReference(Optional.of(SCHEMA), "ORDERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", + TableReference.TYPE_VIEW); + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private DataSource dataSource; + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + MariaDbDataSource ds = new MariaDbDataSource(); + ds.setUrl(CONTAINER.getJdbcUrl()); + ds.setUser(CONTAINER.getUsername()); + ds.setPassword(CONTAINER.getPassword()); + this.dataSource = ds; + this.dialect = new MariaDBDialect(); + this.connection = dataSource.getConnection(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE + // CREATE SCHEMA IF NOT EXISTS — harmless because container provisions RT_TEST. + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), info -> { + /* schema may show up in catalogs() on MariaDB, not schemas() */ }); + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), info -> { + /* loader may not surface tables on first call — drop side-effect verifies */ }); + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — CUSTOMERS.NAME ← MAX(ORDERS.NOTE). + // MariaDB note: cannot reference target table in a subquery — use JOIN syntax + // instead. + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + try (PreparedStatement ps = connection.prepareStatement("UPDATE " + qC + " C JOIN (" + " SELECT " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + " MAX(" + dialect.quoteIdentifier("NOTE") + + ") AS NEW_NAME" + " FROM " + qO + " GROUP BY " + dialect.quoteIdentifier("CUSTOMER_ID") + + ") O ON O." + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID") + + " SET C." + dialect.quoteIdentifier("NAME") + " = O.NEW_NAME")) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), info -> { + }); + // Don't DROP SCHEMA — the container's user only owns RT_TEST and we'd + // need root privileges to recreate it; tearDown re-uses the schema anyway. + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java new file mode 100644 index 0000000..a9a81c8 --- /dev/null +++ b/dialect/db/mariadb/src/test/java/org/eclipse/daanse/sql/dialect/db/mariadb/sqlgen/MariaDbAlterRenameOfflineTest.java @@ -0,0 +1,57 @@ +/* + * 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.mariadb.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.mariadb.MariaDBDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +/** + * MariaDB extends MySqlDialect — confirms the MySQL overrides are inherited. + */ +class MariaDbAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "appdb"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + + private final MariaDBDialect dialect = new MariaDBDialect(); + + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size) { + return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), + ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void inherits_MODIFY_COLUMN_from_MySqlDialect() { + assertThat(dialect.ddlGenerator().alterColumnType(T, "SALARY", meta(JDBCType.DECIMAL, OptionalInt.of(12)))) + .contains("MODIFY COLUMN"); + } + + @Test + void inherits_table_scoped_renameIndex() { + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)).startsWith("ALTER TABLE ") + .contains("RENAME INDEX"); + } + + @Test + void inherits_renameConstraint_returning_null() { + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } +} diff --git a/dialect/db/mssqlserver/pom.xml b/dialect/db/mssqlserver/pom.xml new file mode 100644 index 0000000..d3d21e9 --- /dev/null +++ b/dialect/db/mssqlserver/pom.xml @@ -0,0 +1,111 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.mssqlserver + Eclipse Daanse JDBC DB Dialect SQL Server + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + biz.aQute.bnd + biz.aQute.bndlib + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + com.microsoft.sqlserver + mssql-jdbc + 12.6.1.jre11 + test + + + org.testcontainers + mssqlserver + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + + + + maven-surefire-plugin + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java new file mode 100644 index 0000000..de8cf7a --- /dev/null +++ b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialect.java @@ -0,0 +1,649 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mssqlserver; + +import java.sql.Date; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * @author jhyde + * @since Nov 23, 2008 + */ +public class MicrosoftSqlServerDialect extends AbstractJdbcDialect { + + private final DateFormat df = new SimpleDateFormat("yyyyMMdd"); + + private static final String SUPPORTED_PRODUCT_NAME = "MSSQL"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cachedCteGenerator; + + public MicrosoftSqlServerDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public MicrosoftSqlServerDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + return new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public java.util.Optional selectPrefix(java.util.OptionalLong limit, java.util.OptionalLong offset) { + // SQL Server expresses a pure row cap as a leading "TOP n". When an offset is + // present it uses the trailing OFFSET/FETCH form instead (see paginate). + if (limit.isPresent() && offset.isEmpty()) { + return java.util.Optional.of("TOP " + limit.getAsLong()); + } + return java.util.Optional.empty(); + } + + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + // No offset → the cap is emitted as a leading TOP (selectPrefix), so nothing + // trailing. With an offset, fall back to ANSI OFFSET/FETCH (SQL Server 2012+). + return offset.isEmpty() + ? "" + : org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator.super.paginate(limit, + offset); + } + }; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { + var local = cachedReturningGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator() { + @Override + public boolean supportsReturning() { + return true; + } + + @Override + public java.util.Optional returning(java.util.List columns) { + if (columns == null || columns.isEmpty()) + return java.util.Optional.empty(); + if (columns.size() == 1 && "*".equals(columns.get(0))) { + return java.util.Optional.of(" OUTPUT INSERTED.*"); + } + StringBuilder sb = new StringBuilder(" OUTPUT "); + boolean first = true; + for (String c : columns) { + if (!first) + sb.append(", "); + sb.append("INSERTED.").append(quoteIdentifier(c)); + first = false; + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedReturningGenerator = local; + return local; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator mergeGenerator() { + var local = cachedMergeGenerator; + if (local != null) + return local; + // SQL Server major version map: 2008=10, 2012=11, 2014=12, 2016=13, 2017=14, + // 2019=15, 2022=16. + if (!dialectVersion.isUnknownOrAtLeast(10, 0)) { + local = super.mergeGenerator(); + cachedMergeGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator() { + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public java.util.Optional upsert(UpsertSpec spec, java.util.List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException("values must match insertColumns in length"); + } + StringBuilder sb = new StringBuilder("MERGE INTO ").append(qualified(spec.target())) + .append(" AS T USING (VALUES ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(values.get(i)); + } + sb.append(")) AS S("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") ON "); + for (int i = 0; i < spec.keyColumns().size(); i++) { + if (i > 0) + sb.append(" AND "); + sb.append("T.").append(quoteIdentifier(spec.keyColumns().get(i))).append(" = S.") + .append(quoteIdentifier(spec.keyColumns().get(i))); + } + if (!spec.updateColumns().isEmpty()) { + sb.append(" WHEN MATCHED THEN UPDATE SET "); + boolean first = true; + for (String c : spec.updateColumns()) { + if (!first) + sb.append(", "); + sb.append("T.").append(quoteIdentifier(c)).append(" = S.").append(quoteIdentifier(c)); + first = false; + } + } + sb.append(" WHEN NOT MATCHED THEN INSERT ("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") VALUES ("); + for (int i = 0; i < spec.insertColumns().size(); i++) { + if (i > 0) + sb.append(", "); + sb.append("S.").append(quoteIdentifier(spec.insertColumns().get(i))); + } + sb.append(");"); + return java.util.Optional.of(sb.toString()); + } + }; + cachedMergeGenerator = local; + return local; + } + + /** + * SQL Server: CTE {@code WITH …} but no {@code RECURSIVE} keyword + * (auto-detected). + */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cteGenerator() { + var local = cachedCteGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.CteGenerator() { + @Override + public boolean emitsRecursiveKeyword() { + return false; + } + }; + cachedCteGenerator = local; + return local; + } + + /** SQL Server rejects {@code CASCADE} on {@code DROP TABLE}. */ + @Override + public boolean supportsDropTableCascade() { + return false; + } + + /** SQL Server rejects {@code IF NOT EXISTS} on {@code CREATE TABLE}. */ + @Override + public boolean supportsCreateTableIfNotExists() { + return false; + } + + /** SQL Server rejects {@code IF NOT EXISTS} on {@code CREATE INDEX}. */ + @Override + public boolean supportsCreateIndexIfNotExists() { + return false; + } + + /** SQL Server requires DROP+CREATE — no {@code OR REPLACE} on views. */ + @Override + public boolean supportsCreateOrReplaceView() { + return false; + } + + /** SQL Server: {@code DROP INDEX name ON table_name}. */ + @Override + public boolean dropIndexRequiresTable() { + return true; + } + + /** + * SQL Server rejects {@code IF EXISTS} on + * {@code ALTER TABLE … DROP CONSTRAINT}. + */ + @Override + public boolean supportsDropConstraintIfExists() { + return false; + } + + @Override + public String createSchema(String schemaName, boolean ifNotExists) { + if (!ifNotExists) { + return "CREATE SCHEMA " + quoteIdentifier(schemaName); + } + // Single-line guarded form; works in any batch with QUOTED_IDENTIFIER on. + return "IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = '" + schemaName.replace("'", "''") + + "') EXEC('CREATE SCHEMA " + quoteIdentifier(schemaName) + "')"; + } + + @Override + public String dropTable(String schemaName, String tableName, boolean ifExists) { + if (!ifExists) { + return "DROP TABLE " + quoteIdentifier(schemaName, tableName); + } + return "IF OBJECT_ID('" + (schemaName == null ? "" : schemaName + ".") + tableName + + "', 'U') IS NOT NULL DROP TABLE " + quoteIdentifier(schemaName, tableName); + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineGeneric(columnNames, columnTypes, valueList, null, false); + } + + @Override + public boolean requiresAliasForFromQuery() { + return true; + } + + @Override + public boolean requiresUnionOrderByOrdinal() { + return false; + } + + @Override + public void quoteBooleanLiteral(StringBuilder buf, String value) { + // avoid padding origin values with blanks to n for char(n), + // when ANSI_PADDING=ON + String boolLiteral = value.trim(); + if (!boolLiteral.equalsIgnoreCase("TRUE") && !(boolLiteral.equalsIgnoreCase("FALSE")) + && !(boolLiteral.equalsIgnoreCase("1")) && !(boolLiteral.equalsIgnoreCase("0"))) { + throw new NumberFormatException("Illegal BOOLEAN literal: " + value); + } + buf.append(DialectUtil.singleQuoteString(value)); + } + + @Override + protected void quoteDateLiteral(StringBuilder buf, Date date) { + buf.append("CONVERT(DATE, '"); + buf.append(df.format(date)); + // Format 112 is equivalent to "yyyyMMdd" in Java. + // See http://msdn.microsoft.com/en-us/library/ms187928.aspx + buf.append("', 112)"); + } + + @Override + protected void quoteTimestampLiteral(StringBuilder buf, String value, Timestamp timestamp) { + buf.append("CONVERT(datetime, '"); + buf.append(timestamp.toString()); + // Format 120 is equivalent to "yyyy-mm-dd hh:mm:ss" in Java. + // See http://msdn.microsoft.com/en-us/library/ms187928.aspx + buf.append("', 120)"); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.PRESERVE; + } + + /** + * T-SQL spells statement hints as a trailing query-option clause: + * {@code OPTION (RECOMPILE, MAXDOP 2)} (leading space) — per hint the name followed by + * its arguments separated by spaces ({@code name args...}), hints separated by commas. + */ + @Override + public StringBuilder statementOption(List hints) { + if (hints.isEmpty()) { + return new StringBuilder(); + } + StringBuilder sb = new StringBuilder(" OPTION ("); + boolean first = true; + for (StatementHint hint : hints) { + if (!first) { + sb.append(", "); + } + first = false; + sb.append(hint.name()); + for (String argument : hint.arguments()) { + sb.append(' ').append(argument); + } + } + return sb.append(")"); + } + + @Override + public StringBuilder generateKnownFunction(KnownFunction function, List arguments) { + return switch (function) { + case LENGTH -> { + if (arguments.size() != 1) { + throw new IllegalArgumentException("LENGTH expects 1 argument(s), got " + arguments.size()); + } + yield new StringBuilder("LEN(").append(arguments.get(0)).append(")"); + } + case CONCAT -> { + // T-SQL + concatenates but coerces/NULL-propagates awkwardly; CONCAT is the idiom. + if (arguments.size() < 2) { + throw new IllegalArgumentException("CONCAT expects 2 or more argument(s), got " + arguments.size()); + } + StringBuilder sb = new StringBuilder("CONCAT("); + for (int i = 0; i < arguments.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(arguments.get(i)); + } + yield sb.append(")"); + } + case INDEX_OF -> { + if (arguments.size() != 2) { + throw new IllegalArgumentException("INDEX_OF expects 2 argument(s), got " + arguments.size()); + } + yield new StringBuilder("CHARINDEX(").append(arguments.get(0)).append(", ").append(arguments.get(1)) + .append(")"); + } + case YEAR, MONTH, DAY, HOUR, MINUTE, SECOND -> { + if (arguments.size() != 1) { + throw new IllegalArgumentException( + function.name() + " expects 1 argument(s), got " + arguments.size()); + } + // T-SQL has no EXTRACT; DATEPART covers the whole family. + yield new StringBuilder("DATEPART(").append(function.name().toLowerCase(java.util.Locale.ROOT)) + .append(", ").append(arguments.get(0)).append(")"); + } + case NOW -> { + if (!arguments.isEmpty()) { + throw new IllegalArgumentException("NOW expects 0 argument(s), got " + arguments.size()); + } + // GETDATE() (datetime) chosen over SYSDATETIME() (datetime2): it is the + // ubiquitous T-SQL spelling and its precision suffices for NOW's intent. + yield new StringBuilder("GETDATE()"); + } + default -> super.generateKnownFunction(function, arguments); + }; + } + + @Override + public java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_DISC", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_CONT", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public boolean supportsPercentileDisc() { + return true; + } + + @Override + public boolean supportsPercentileCont() { + return true; + } + + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + StringBuilder buf = new StringBuilder(64); + buf.append("STRING_AGG"); + buf.append("( "); + if (distinct) { + buf.append("DISTINCT "); + } + buf.append(operand); + buf.append(", '"); + if (separator != null) { + buf.append(separator); + } else { + buf.append(", "); + } + buf.append("'"); + buf.append(")"); + if (columns != null && !columns.isEmpty()) { + buf.append(" WITHIN GROUP (ORDER BY "); + buf.append(buildOrderedColumnsClause(columns)); + buf.append(")"); + } + // STRING_AGG(CONVERT (NVARCHAR (MAX), EmailAddress), ';') WITHIN GROUP (ORDER + // BY EmailAddress ASC) + return java.util.Optional.of((buf).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("NTH_VALUE", operand, ignoreNulls, n, columns, true)).toString()); + } + + @Override + public boolean supportsNthValue() { + return true; + } + + @Override + public boolean supportsNthValueIgnoreNulls() { + return true; + } + + @Override + public boolean supportsListAgg() { + return true; + } + + @Override + public String createTrigger(String triggerName, org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming timing, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent event, + org.eclipse.daanse.sql.model.schema.TableReference table, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope scope, String whenCondition, String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for SQL Server CREATE TRIGGER"); + } + StringBuilder sb = new StringBuilder("CREATE TRIGGER "); + sb.append(quoteIdentifier(triggerName)); + sb.append(" ON ").append(qualified(table)); + // SQL Server: AFTER (or FOR), INSTEAD OF — no BEFORE. + if (timing == org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming.INSTEAD_OF) { + sb.append(" INSTEAD OF "); + } else { + sb.append(" AFTER "); + } + sb.append(event); + // Body should already include the {@code AS} keyword; if not, prepend. + String trimmed = body.stripLeading(); + if (!trimmed.regionMatches(true, 0, "AS", 0, 2) && !trimmed.regionMatches(true, 0, "WITH", 0, 4)) { + sb.append(" AS "); + } else { + sb.append(' '); + } + sb.append(body); + return sb.toString(); + } + + // -------------------- Trigger procedures -------------------- + + @Override + public Optional createTriggerProcedure(String procedureName, String schemaName, String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for SQL Server trigger procedure"); + } + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("CREATE OR ALTER PROCEDURE " + qualified + " AS " + body); + } + + @Override + public String createTriggerUsingProcedure(String triggerName, String schemaName, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming timing, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent event, + org.eclipse.daanse.sql.model.schema.TableReference table, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope scope, String whenCondition, + String procedureName) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return createTrigger(triggerName, timing, event, table, scope, whenCondition, "AS EXEC " + qualified); + } + + /** SQL Server: {@code DROP PROCEDURE [IF EXISTS] schema.procedureName}. */ + @Override + public Optional dropProcedure(String procedureName, String schemaName, boolean ifExists) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("DROP PROCEDURE " + (ifExists ? "IF EXISTS " : "") + qualified); + } + + // -------------------- DDL — ALTER / RENAME (SQL-Server-specific) + // -------------------- + + @Override + public String alterTableAddColumn(TableReference table, ColumnDefinition column) { + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD "); + sb.append(quoteIdentifier(column.column().name())); + sb.append(' ').append(nativeType(column.columnMetaData())); + if (column.columnMetaData().nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + column.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + return sb.toString(); + } + + /** + * SQL Server: {@code ALTER TABLE x ALTER COLUMN c } (no {@code TYPE} + * keyword). + */ + @Override + public String alterColumnType(TableReference table, String columnName, ColumnMetaData newMeta) { + if (newMeta == null) { + throw new IllegalArgumentException("newMeta must not be null for ALTER COLUMN"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(newMeta)).toString(); + } + + @Override + public String alterColumnSetNullability(TableReference table, String columnName, boolean nullable) { + throw new UnsupportedOperationException( + "SQL Server requires the column type when altering nullability — use the " + + "alterColumnSetNullability(table, column, nullable, currentMeta) overload."); + } + + @Override + public String alterColumnSetNullability(TableReference table, String columnName, boolean nullable, + ColumnMetaData currentMeta) { + if (currentMeta == null) { + throw new IllegalArgumentException("currentMeta must not be null for SQL Server ALTER COLUMN"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ALTER COLUMN ") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(currentMeta)) + .append(nullable ? " NULL" : " NOT NULL").toString(); + } + + @Override + public String alterColumnSetDefault(TableReference table, String columnName, String defaultExpression) { + if (defaultExpression == null || defaultExpression.isBlank()) { + throw new IllegalArgumentException("defaultExpression must not be blank for SET DEFAULT"); + } + String dfName = "DF_" + table.name() + "_" + columnName; + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" ADD CONSTRAINT ") + .append(quoteIdentifier(dfName)).append(" DEFAULT (").append(defaultExpression).append(") FOR ") + .append(quoteIdentifier(columnName)).toString(); + } + + /** + * Drops the assumed {@code DF_ + * + + * _} default-constraint produced by {@link #alterColumnSetDefault}. + */ + @Override + public String alterColumnDropDefault(TableReference table, String columnName) { + String dfName = "DF_" + table.name() + "_" + columnName; + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" DROP CONSTRAINT ") + .append(quoteIdentifier(dfName)).toString(); + } + + @Override + public String renameColumn(TableReference table, String oldName, String newName) { + return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "COLUMN"); + } + + /** SQL Server: {@code EXEC sp_rename 'schema.table', 'new'}. */ + @Override + public String renameTable(TableReference table, String newName) { + return spRename(qualified(table), newName, null); + } + + /** SQL Server: {@code EXEC sp_rename 'schema.table.idx', 'new', 'INDEX'}. */ + @Override + public String renameIndex(String oldName, String newName, TableReference table) { + if (table == null) { + throw new IllegalArgumentException("table must not be null for SQL Server sp_rename INDEX"); + } + return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "INDEX"); + } + + /** + * SQL Server: + * {@code EXEC sp_rename 'schema.table.constraint', 'new', 'OBJECT'}. + */ + @Override + public String renameConstraint(TableReference table, String oldName, String newName) { + return spRename(qualified(table) + "." + quoteIdentifier(oldName), newName, "OBJECT"); + } + + /** + * @param objectType the sp_rename type ({@code COLUMN}/{@code INDEX}/ + * {@code OBJECT}), or {@code null} for the table-rename + * overload (which omits the third argument entirely) + */ + private String spRename(String quotedSourcePath, String newName, String objectType) { + StringBuilder sb = new StringBuilder("EXEC sp_rename '"); + sb.append(quotedSourcePath); + sb.append("', '").append(newName).append("'"); + if (objectType != null) + sb.append(", '").append(objectType).append("'"); + return sb.toString(); + } +} diff --git a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectFactory.java b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectFactory.java new file mode 100644 index 0000000..bdb1c54 --- /dev/null +++ b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mssqlserver; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("MSSQL") +public class MicrosoftSqlServerDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return MicrosoftSqlServerDialect::new; + } + +} diff --git a/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/package-info.java b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/package-info.java new file mode 100644 index 0000000..7b63898 --- /dev/null +++ b/dialect/db/mssqlserver/src/main/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.mssqlserver; diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectTest.java new file mode 100644 index 0000000..3b564a1 --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerDialectTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mssqlserver; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MicrosoftSqlServerDialectTest { + + private static final String ILLEGAL_BOOLEAN_LITERAL = "illegal for this dialect boolean literal"; + private static final String ILLEGAL_BOOLEAN_LITERAL_MESSAGE = "Illegal BOOLEAN literal: "; + private static final String BOOLEAN_LITERAL_TRUE = "True"; + private static final String BOOLEAN_LITERAL_FALSE = "False"; + private static final String BOOLEAN_LITERAL_ONE = "1"; + private static final String BOOLEAN_LITERAL_ZERO = "0"; + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private MicrosoftSqlServerDialect dialect; + private StringBuilder buf; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("MSSQL"); + dialect = new MicrosoftSqlServerDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + buf = new StringBuilder(); + } + + @Test + void testQuoteBooleanLiteral_True() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_TRUE); + assertEquals(DialectUtil.singleQuoteString(BOOLEAN_LITERAL_TRUE), buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_False() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_FALSE); + assertEquals(DialectUtil.singleQuoteString(BOOLEAN_LITERAL_FALSE), buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_One() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ONE); + assertEquals(DialectUtil.singleQuoteString(BOOLEAN_LITERAL_ONE), buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_Zero() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ZERO); + assertEquals(DialectUtil.singleQuoteString(BOOLEAN_LITERAL_ZERO), buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_TrowsException() throws Exception { + assertEquals(0, buf.length()); + try { + dialect.quoteBooleanLiteral(buf, ILLEGAL_BOOLEAN_LITERAL); + fail("The illegal boolean literal exception should appear BUT it was not."); + } catch (NumberFormatException e) { + assertEquals(ILLEGAL_BOOLEAN_LITERAL_MESSAGE + ILLEGAL_BOOLEAN_LITERAL, e.getMessage()); + } + } + +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java new file mode 100644 index 0000000..4de8882 --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsRoundTripTest.java @@ -0,0 +1,99 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MicrosoftSqlServerGeneratorsRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private Connection conn; + private MicrosoftSqlServerDialect dialect; + private TableReference users; + + @BeforeAll + void setUp() throws Exception { + conn = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), CONTAINER.getPassword()); + dialect = new MicrosoftSqlServerDialect(DialectInitData.fromConnection(conn)); + users = new TableReference(Optional.of(new SchemaReference(Optional.empty(), "dbo")), "users", + TableReference.TYPE_TABLE); + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE [dbo].[users] (id INT PRIMARY KEY, name VARCHAR(50))"); + s.execute("INSERT INTO [dbo].[users] VALUES (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')"); + } + } + + @AfterAll + void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_offset_fetch_next_executes() throws SQLException { + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertThat(tail).contains("OFFSET 1 ROWS").contains("FETCH NEXT 2 ROWS ONLY"); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM [dbo].[users] ORDER BY id" + tail, 2, 2); + } + + @Test + void merge_upsert_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), + List.of("name")); + // T-SQL MERGE statements need a trailing semicolon. + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql1 + ";", 1, "merge: insert path"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql2 + ";", 1, "merge: update path"); + assertFirstStringEquals(conn, "SELECT name FROM [dbo].[users] WHERE id = 10", "second"); + } + + @Test + void output_clause_returns_inserted_id() throws SQLException { + // SQL Server's RETURNING-equivalent is OUTPUT INSERTED.col. + String returning = dialect.returningGenerator().returning(List.of("id")).orElseThrow(); + assertThat(returning).contains("OUTPUT INSERTED"); + // OUTPUT goes between the table and VALUES, not at the end. + assertFirstIntEquals(conn, "INSERT INTO [dbo].[users] (id, name)" + returning + " VALUES (99, 'returned')", 99); + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java new file mode 100644 index 0000000..74db5cd --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerGeneratorsTest.java @@ -0,0 +1,136 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.table; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpec; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.junit.jupiter.api.Test; + +/** Smoke tests for SQL Server's engine-specific overrides of new generators. */ +class MicrosoftSqlServerGeneratorsTest { + + private final MicrosoftSqlServerDialect d = new MicrosoftSqlServerDialect(); + + @Test + void output_clause() { + assertThat(d.returningGenerator().supportsReturning()).isTrue(); + assertThat(d.returningGenerator().returning(List.of("ID", "NAME")).orElseThrow()) + .isEqualTo(" OUTPUT INSERTED.\"ID\", INSERTED.\"NAME\""); + } + + @Test + void output_star() { + assertThat(d.returningGenerator().returning(List.of("*")).orElseThrow()).isEqualTo(" OUTPUT INSERTED.*"); + } + + @Test + void merge_into_using_values() { + MergeGenerator.UpsertSpec spec = upsertSpec(table("dbo", "USERS"), "ID", "NAME"); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql).contains("MERGE INTO \"dbo\".\"USERS\" AS T USING (VALUES (1, 'foo')) AS S") + .contains("WHEN MATCHED THEN UPDATE SET T.\"NAME\" = S.\"NAME\"") + .contains("WHEN NOT MATCHED THEN INSERT"); + } + + @Test + void cte_no_recursive_keyword() { + var ctes = List.of(new org.eclipse.daanse.sql.dialect.api.generator.CteGenerator.Cte("t", "SELECT 1")); + assertThat(d.cteGenerator().withClause(ctes, true)).doesNotContain("RECURSIVE").contains("WITH t AS"); + } + + @Test + void known_function_length_uses_len() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.LENGTH, List.of("x"))) + .hasToString("LEN(x)"); + } + + @Test + void known_function_index_of_uses_charindex() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h"))) + .hasToString("CHARINDEX(n, h)"); + } + + @Test + void known_function_concat_uses_concat_call() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.CONCAT, List.of("a", "b", "c"))) + .hasToString("CONCAT(a, b, c)"); + } + + @Test + void known_function_datetime_parts_use_datepart() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.YEAR, List.of("x"))) + .hasToString("DATEPART(year, x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.MONTH, List.of("x"))) + .hasToString("DATEPART(month, x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.DAY, List.of("x"))) + .hasToString("DATEPART(day, x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.HOUR, List.of("x"))) + .hasToString("DATEPART(hour, x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.MINUTE, List.of("x"))) + .hasToString("DATEPART(minute, x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.SECOND, List.of("x"))) + .hasToString("DATEPART(second, x)"); + } + + @Test + void known_function_now_uses_getdate() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.NOW, List.of())) + .hasToString("GETDATE()"); + } + + @Test + void known_function_ceiling_and_defaults_delegate() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.CEILING, List.of("x"))) + .hasToString("CEILING(x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.ABS, List.of("x"))) + .hasToString("ABS(x)"); + } + + @Test + void statement_option_trailing_clause() { + assertThat(d.hintGenerator().statementOption(List.of(new StatementHint("RECOMPILE", List.of()), + new StatementHint("MAXDOP", List.of("2"))))) + .hasToString(" OPTION (RECOMPILE, MAXDOP 2)"); + } + + @Test + void statement_option_multi_argument_hint() { + assertThat(d.hintGenerator().statementOption( + List.of(new StatementHint("USE HINT", List.of("'DISABLE_OPTIMIZER_ROWGOAL'", "'RECOMPILE'"))))) + .hasToString(" OPTION (USE HINT 'DISABLE_OPTIMIZER_ROWGOAL' 'RECOMPILE')"); + } + + @Test + void statement_option_empty_list_emits_nothing() { + assertThat(d.hintGenerator().statementOption(List.of())).isEmpty(); + } + + @Test + void select_hint_stays_default_empty() { + assertThat(d.hintGenerator().selectHint(List.of(new StatementHint("MAXDOP", List.of("2"))))).isEmpty(); + } + + @Test + void known_function_override_guards_arity() { + assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.LENGTH, List.of("x", "y"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("LENGTH"); + assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.NOW, List.of("x"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("NOW"); + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerInitMetaInfoLatencyTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..46c1128 --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerInitMetaInfoLatencyTest.java @@ -0,0 +1,193 @@ +/* + * 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; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * {@code createMetaInfo} vs lightweight dialect-init read latency on SQL Server + * 2022. + */ +@Testcontainers +class MicrosoftSqlServerInitMetaInfoLatencyTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private static String jdbcUrl; + private static String user; + private static String password; + private final DatabaseService service = new DatabaseServiceImpl(); + + @Test + void measure() throws SQLException { + jdbcUrl = CONTAINER.getJdbcUrl(); + user = CONTAINER.getUsername(); + password = CONTAINER.getPassword(); + DataSource ds = ds(); + System.out.println(); + System.out.println("=== SQL Server 2022: createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100, 500 }) { + resetSchema(); + populate(n); + for (int i = 0; i < 2; i++) { + service.createMetaInfo(ds); + lightweight(ds); + } + long meta = bestOf(3, () -> service.createMetaInfo(ds)); + long light = bestOf(3, () -> lightweight(ds)); + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", n + " tables", meta / 1_000_000.0, + light / 1_000_000.0, light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + private DataSource ds() { + return new javax.sql.DataSource() { + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl, user, password); + } + + @Override + public Connection getConnection(String u, String p) throws SQLException { + return DriverManager.getConnection(jdbcUrl, u, p); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter w) { + } + + @Override + public void setLoginTimeout(int s) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return null; + } + + @Override + public T unwrap(Class i) { + return null; + } + + @Override + public boolean isWrapperFor(Class i) { + return false; + } + }; + } + + private static void lightweight(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) + best = t; + } + return best; + } + + private static void resetSchema() throws SQLException { + try (Connection c = DriverManager.getConnection(jdbcUrl, user, password); Statement s = c.createStatement()) { + // Drop all FKs first, then iterate and drop tables explicitly + java.util.List fks = new java.util.ArrayList<>(); + try (java.sql.ResultSet rs = c.createStatement() + .executeQuery("SELECT 'ALTER TABLE [' + OBJECT_SCHEMA_NAME(parent_object_id) + '].[' " + + " + OBJECT_NAME(parent_object_id) + '] DROP CONSTRAINT [' + name + ']'" + + " FROM sys.foreign_keys")) { + while (rs.next()) + fks.add(rs.getString(1)); + } + for (String sql : fks) + s.execute(sql); + + java.util.List tables = new java.util.ArrayList<>(); + try (java.sql.ResultSet rs = c.createStatement().executeQuery("SELECT name FROM sys.tables")) { + while (rs.next()) + tables.add(rs.getString(1)); + } + for (String t : tables) + s.execute("DROP TABLE [" + t + "]"); + } + } + + private static void populate(int n) throws SQLException { + if (n == 0) + return; + try (Connection c = DriverManager.getConnection(jdbcUrl, user, password); Statement s = c.createStatement()) { + for (int i = 0; i < n; i++) { + s.execute("CREATE TABLE T_" + i + " (ID INT PRIMARY KEY, NAME NVARCHAR(50)," + + " VAL DECIMAL(12,3), BIRTHDAY DATE, CREATED DATETIME2)"); + if (i > 0) { + s.execute("ALTER TABLE T_" + i + " ADD CONSTRAINT FK_" + i + " FOREIGN KEY (ID) REFERENCES T_" + + (i - 1) + "(ID)"); + } + s.execute("CREATE INDEX IDX_" + i + "_NAME ON T_" + i + "(NAME)"); + } + } + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java new file mode 100644 index 0000000..85a58aa --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/MicrosoftSqlServerQuotingPolicyTest.java @@ -0,0 +1,83 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class MicrosoftSqlServerQuotingPolicyTest { + + private static final SchemaReference DBO = new SchemaReference(Optional.empty(), "dbo"); + private static final TableReference EMP = new TableReference(Optional.of(DBO), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(DBO), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private MicrosoftSqlServerDialect dialectNever() { + MicrosoftSqlServerDialect d = new MicrosoftSqlServerDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void renameTable_via_spRename_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContainPattern("\\[[^]]*\\]") // no + // [bracket]-quoted + // identifiers + .contains("EMPLOYEES").contains("PERSON"); + } + + @Test + void renameIndex_via_spRename_unquoted() { + assertThat(dialectNever().ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", EMP)) + .doesNotContainPattern("\\[[^]]*\\]").contains("EMPLOYEES").contains("IDX_OLD").contains("IDX_NEW"); + } + + @Test + void renameConstraint_via_spRename_unquoted() { + assertThat(dialectNever().ddlGenerator().renameConstraint(EMP, "PK_OLD", "PK_NEW")) + .doesNotContainPattern("\\[[^]]*\\]").contains("EMPLOYEES").contains("PK_OLD").contains("PK_NEW"); + } + + @Test + void createTrigger_unquoted() { + assertThat(dialectNever().ddlGenerator().createTrigger("TRG_EMP", + org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming.AFTER, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent.INSERT, EMP, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope.ROW, null, "INSERT INTO LOG VALUES (1)")) + .doesNotContainPattern("\\[[^]]*\\]").contains("TRG_EMP").contains("EMPLOYEES"); + } + + @Test + void dropTable_ifExists_unquoted() { + assertThat(dialectNever().ddlGenerator().dropTable(EMP, true)).doesNotContainPattern("\\[[^]]*\\]") + .contains("EMPLOYEES"); + } + + @Test + void primaryKey_constraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContainPattern("\\[[^]]*\\]").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void foreignKey_constraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContainPattern("\\[[^]]*\\]").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/integration/ServiceTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/integration/ServiceTest.java new file mode 100644 index 0000000..a266b1c --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.mssqlserver.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(MicrosoftSqlServerDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MsSqlServerDdlRoundTripTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MsSqlServerDdlRoundTripTest.java new file mode 100644 index 0000000..2672a86 --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MsSqlServerDdlRoundTripTest.java @@ -0,0 +1,260 @@ +/* + * 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.math.BigDecimal; +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MsSqlServerDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference ORDERS = new TableReference(Optional.of(SCHEMA), "ORDERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", + TableReference.TYPE_VIEW); + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private DataSource dataSource; + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(CONTAINER.getJdbcUrl()); + ds.setUser(CONTAINER.getUsername()); + ds.setPassword(CONTAINER.getPassword()); + this.dataSource = ds; + this.dialect = new MicrosoftSqlServerDialect(); + this.connection = dataSource.getConnection(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), info -> { + /* SQL Server: schema is a namespace inside the master DB */ }); + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), info -> { + /* loader may not surface tables on first call — drop side-effect verifies */ }); + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — CUSTOMERS.NAME ← MAX(ORDERS.NOTE) per customer. + // SQL Server: UPDATE … SET … FROM … JOIN — T-SQL flavour. + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + try (PreparedStatement ps = connection.prepareStatement("UPDATE C SET " + dialect.quoteIdentifier("NAME") + + " = O.NEW_NAME" + " FROM " + qC + " AS C" + " JOIN (" + " SELECT " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + " MAX(" + dialect.quoteIdentifier("NOTE") + + ") AS NEW_NAME" + " FROM " + qO + " GROUP BY " + dialect.quoteIdentifier("CUSTOMER_ID") + ") AS O" + + " ON O." + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"))) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropSchema(SCHEMA.name(), true, true), info -> { + }); + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlAlterRenameOfflineTest.java b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlAlterRenameOfflineTest.java new file mode 100644 index 0000000..04e9bdd --- /dev/null +++ b/dialect/db/mssqlserver/src/test/java/org/eclipse/daanse/sql/dialect/db/mssqlserver/sqlgen/MssqlAlterRenameOfflineTest.java @@ -0,0 +1,120 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +class MssqlAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "dbo"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + + private final MicrosoftSqlServerDialect dialect = new MicrosoftSqlServerDialect(); + + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size, ColumnMetaData.Nullability n) { + return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), n, + OptionalInt.empty(), Optional.empty(), Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterColumnType_omits_TYPE_keyword() { + String sql = dialect.ddlGenerator().alterColumnType(T, "SALARY", + meta(JDBCType.DECIMAL, OptionalInt.of(12), ColumnMetaData.Nullability.NULLABLE)); + assertThat(sql).startsWith("ALTER TABLE "); + assertThat(sql).contains("EMPLOYEES"); + assertThat(sql).contains("ALTER COLUMN"); + assertThat(sql).contains("SALARY"); + assertThat(sql).contains("DECIMAL(12)"); + assertThat(sql).doesNotContain(" TYPE "); + } + + @Test + void alterColumnSetNullability_typeFree_throws() { + assertThatThrownBy(() -> dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false)) + .isInstanceOf(UnsupportedOperationException.class).hasMessageContaining("requires the column type"); + } + + @Test + void alterColumnSetNullability_typeAware_restates_type() { + ColumnMetaData m = meta(JDBCType.VARCHAR, OptionalInt.of(100), ColumnMetaData.Nullability.NULLABLE); + String setNotNull = dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false, m); + assertThat(setNotNull).contains("ALTER COLUMN"); + assertThat(setNotNull).contains("EMAIL"); + assertThat(setNotNull).contains("VARCHAR(100)"); + assertThat(setNotNull).endsWith(" NOT NULL"); + + String dropNotNull = dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", true, m); + assertThat(dropNotNull).endsWith(" NULL"); + } + + @Test + void alterColumnSetDefault_creates_named_constraint() { + String sql = dialect.ddlGenerator().alterColumnSetDefault(T, "REGION", "'EU'"); + assertThat(sql).contains("ADD CONSTRAINT"); + assertThat(sql).contains("DF_EMPLOYEES_REGION"); + assertThat(sql).contains("DEFAULT ('EU')"); + assertThat(sql).contains("FOR "); + assertThat(sql).contains("REGION"); + } + + @Test + void alterColumnDropDefault_drops_assumed_constraint() { + String sql = dialect.ddlGenerator().alterColumnDropDefault(T, "REGION"); + assertThat(sql).contains("DROP CONSTRAINT"); + assertThat(sql).contains("DF_EMPLOYEES_REGION"); + } + + @Test + void renameColumn_uses_sp_rename_with_COLUMN() { + String sql = dialect.ddlGenerator().renameColumn(T, "OLD", "NEW"); + assertThat(sql).startsWith("EXEC sp_rename '"); + assertThat(sql).contains("EMPLOYEES"); + assertThat(sql).contains("OLD"); + assertThat(sql).endsWith(", 'NEW', 'COLUMN'"); + } + + @Test + void renameTable_uses_sp_rename() { + String sql = dialect.ddlGenerator().renameTable(T, "STAFF"); + assertThat(sql).startsWith("EXEC sp_rename '"); + assertThat(sql).contains("EMPLOYEES"); + assertThat(sql).endsWith(", 'STAFF'"); + } + + @Test + void renameIndex_uses_sp_rename_with_INDEX() { + String sql = dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T); + assertThat(sql).startsWith("EXEC sp_rename '"); + assertThat(sql).contains("EMPLOYEES"); + assertThat(sql).contains("IDX_OLD"); + assertThat(sql).endsWith(", 'IDX_NEW', 'INDEX'"); + } + + @Test + void renameConstraint_uses_sp_rename_with_OBJECT() { + String sql = dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK"); + assertThat(sql).startsWith("EXEC sp_rename '"); + assertThat(sql).contains("OLD_FK"); + assertThat(sql).endsWith(", 'NEW_FK', 'OBJECT'"); + } +} diff --git a/dialect/db/mysql/pom.xml b/dialect/db/mysql/pom.xml new file mode 100644 index 0000000..609d52e --- /dev/null +++ b/dialect/db/mysql/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.mysql + Eclipse Daanse JDBC DB Dialect MySQL + MySQL database dialect implementation for Daanse JDBC. Provides + MySQL-specific SQL generation, query optimization, and database feature + handling including MySQL data types, functions, and syntax variations. + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + com.mysql + mysql-connector-j + 8.4.0 + test + + + org.testcontainers + mysql + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + + + + maven-surefire-plugin + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java new file mode 100644 index 0000000..c058b8d --- /dev/null +++ b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect.java @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mysql; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * @author jhyde + * @since Nov 23, 2008 + */ +public class MySqlDialect extends AbstractJdbcDialect { + private static final String SUPPORTED_PRODUCT_NAME = "MYSQL"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cachedCteGenerator; + + /** JDBC-free constructor for SQL generation. Uses MySQL backtick quoting. */ + public MySqlDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults().withQuoteIdentifierString("`")); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public MySqlDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + public static boolean looksLikeInfobright(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + String version = init.productVersion(); + return version != null && version.toLowerCase(java.util.Locale.ROOT).contains("infobright"); + } + + /** MySQL/MariaDB: {@code LIMIT [offset,] count} — both forms accepted. */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + var local = cachedPaginationGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + StringBuilder sb = new StringBuilder(); + if (limit.isPresent()) { + if (limit.getAsLong() < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" LIMIT "); + if (offset.isPresent()) { + if (offset.getAsLong() < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(offset.getAsLong()).append(", "); + } + sb.append(limit.getAsLong()); + } else if (offset.isPresent()) { + // MySQL has no syntax for OFFSET without LIMIT — use the documented huge-limit + // trick. + if (offset.getAsLong() < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" LIMIT ").append(offset.getAsLong()).append(", 18446744073709551615"); + } + return sb.toString(); + } + }; + cachedPaginationGenerator = local; + return local; + } + + /** + * MySQL/MariaDB: + * {@code INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col), ...}. + */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator mergeGenerator() { + var local = cachedMergeGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator() { + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public java.util.Optional upsert(UpsertSpec spec, java.util.List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException("values must match insertColumns in length"); + } + StringBuilder sb = new StringBuilder("INSERT INTO ").append(qualified(spec.target())).append(" ("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") VALUES ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(values.get(i)); + } + sb.append(")"); + if (!spec.updateColumns().isEmpty()) { + sb.append(" ON DUPLICATE KEY UPDATE "); + boolean first = true; + for (String c : spec.updateColumns()) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)).append(" = VALUES(").append(quoteIdentifier(c)).append(")"); + first = false; + } + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedMergeGenerator = local; + return local; + } + + /** MySQL silently ignores {@code CASCADE} on {@code DROP TABLE}. */ + @Override + public boolean supportsDropTableCascade() { + return false; + } + + /** MySQL has no SQL sequences — use {@code AUTO_INCREMENT} columns. */ + @Override + public boolean supportsSequences() { + return false; + } + + /** + * MySQL/MariaDB: index names are table-scoped — + * {@code DROP INDEX name ON table}. + */ + @Override + public boolean dropIndexRequiresTable() { + return true; + } + + /** + * MySQL rejects {@code IF NOT EXISTS} on {@code CREATE INDEX} (MariaDB accepts + * it). + */ + @Override + public boolean supportsCreateIndexIfNotExists() { + return false; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.CteGenerator cteGenerator() { + var local = cachedCteGenerator; + if (local != null) + return local; + boolean supports = org.eclipse.daanse.sql.dialect.api.DialectVersion.UNKNOWN.equals(dialectVersion) + || dialectVersion.atLeast(8, 0); + local = new org.eclipse.daanse.sql.dialect.api.generator.CteGenerator() { + @Override + public boolean supportsRecursiveCte() { + return supports; + } + }; + cachedCteGenerator = local; + return local; + } + + /** + * MySQL rejects {@code IF EXISTS} on {@code DROP INDEX} (MariaDB accepts it). + */ + @Override + public boolean supportsDropIndexIfExists() { + return false; + } + + /** MySQL rejects {@code IF EXISTS} on {@code ALTER TABLE … DROP CONSTRAINT}. */ + @Override + public boolean supportsDropConstraintIfExists() { + return false; + } + + /** + * @param metaData DatabaseMetaData + * @return Whether this is Infobright + */ + public static boolean isInfobright(DatabaseMetaData metaData) { + // Infobright detection is currently disabled. A separate Infobright dialect + // or a configurable flag could be added if Infobright support is needed. + // Detection would require querying for the BRIGHTHOUSE engine presence. + return false; + } + + @Override + public void appendHintsAfterFromClause(StringBuilder buf, Map hints) { + if (hints != null) { + String forcedIndex = hints.get("force_index"); + if (forcedIndex != null) { + buf.append(" FORCE INDEX ("); + buf.append(forcedIndex); + buf.append(")"); + } + } + } + + /** + * MySQL spells statement hints as an optimizer block directly after the {@code SELECT} + * keyword: {@code /*+ name(arg1, arg2) name2 *}{@code /} (trailing space). Hints are + * joined by a space; a hint without arguments is the bare name. Any {@code *}{@code /} + * inside a name or argument is stripped so the block cannot terminate early. + */ + @Override + public StringBuilder selectHint(List hints) { + if (hints.isEmpty()) { + return new StringBuilder(); + } + StringBuilder sb = new StringBuilder("/*+ "); + boolean first = true; + for (StatementHint hint : hints) { + if (!first) { + sb.append(' '); + } + first = false; + sb.append(stripCommentEnd(hint.name())); + if (!hint.arguments().isEmpty()) { + sb.append('('); + for (int i = 0; i < hint.arguments().size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(stripCommentEnd(hint.arguments().get(i))); + } + sb.append(')'); + } + } + return sb.append(" */ "); + } + + /** Neutralizes a premature comment terminator inside a hint name/argument. */ + private static String stripCommentEnd(String s) { + return s.replace("*/", ""); + } + + @Override + public boolean requiresAliasForFromQuery() { + return true; + } + + @Override + public boolean allowsFromQuery() { + // MySQL before 4.0 does not allow FROM subqueries in the FROM clause. + // Compare numerically: the former lexicographic productVersion + // comparison against "4." misclassified two-digit majors (MariaDB + // "10.x"/"11.x" < "4." as strings) as pre-4.0. + return dialectVersion.isUnknownOrAtLeast(4, 0); + } + + @Override + public boolean allowsCompoundCountDistinct() { + return true; + } + + @Override + public void quoteStringLiteral(StringBuilder buf, String s) { + // Go beyond standard singleQuoteString; also quote backslash. + buf.append('\''); + String s0 = s.replace("'", "''"); + String s1 = s0.replace("\\", "\\\\"); + buf.append(s1); + buf.append('\''); + } + + @Override + public void quoteBooleanLiteral(StringBuilder buf, String value) { + if (!value.equalsIgnoreCase("1") && !(value.equalsIgnoreCase("0"))) { + super.quoteBooleanLiteral(buf, value); + } else { + buf.append(value); + } + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineGeneric(columnNames, columnTypes, valueList, null, false); + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + // In MYSQL, Null values are worth negative infinity. + return DialectUtil.generateOrderByNullsWithIsnull(expr, ascending, collateNullsLast); + } + + @Override + public boolean requiresHavingAlias() { + return true; + } + + @Override + public boolean supportsMultiValueInExpr() { + return true; + } + + private enum Scope { + SESSION, GLOBAL + } + + @Override + public boolean allowsRegularExpressionInWhereClause() { + return true; + } + + @Override + public Optional generateRegularExpression(String source, String javaRegex) { + try { + Pattern.compile(javaRegex); + } catch (PatternSyntaxException e) { + // Not a valid Java regex. Too risky to continue. + return Optional.empty(); + } + + // We might have to use case-insensitive matching + javaRegex = DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex); + StringBuilder mappedFlags = new StringBuilder(); + String[][] mapping = new String[][] { { "i", "i" } }; + javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags); + boolean caseSensitive = true; + if (mappedFlags.toString().contains("i")) { + caseSensitive = false; + } + final Matcher escapeMatcher = DialectUtil.ESCAPE_PATTERN.matcher(javaRegex); + while (escapeMatcher.find()) { + javaRegex = javaRegex.replace(escapeMatcher.group(1), escapeMatcher.group(2)); + } + final StringBuilder sb = new StringBuilder(); + + // Now build the string. + sb.append(source); + sb.append(" IS NOT NULL AND "); + if (caseSensitive) { + sb.append(source); + } else { + sb.append("UPPER("); + sb.append(source); + sb.append(")"); + } + sb.append(" REGEXP "); + if (caseSensitive) { + quoteStringLiteral(sb, javaRegex); + } else { + quoteStringLiteral(sb, javaRegex.toUpperCase()); + } + return Optional.of(sb.toString()); + } + + /** + * @return true when MySQL version is 5.7 or larger + */ + @Override + public boolean requiresOrderByAlias() { + // Compare numerically: the former lexicographic productVersion comparison + // against "5.7" misclassified two-digit majors (MariaDB "10.x"/"11.x" < "5.7" + // as strings) as pre-5.7. MySQL byte-neutral: majors 4-9 agree in both forms. + return dialectVersion.isUnknownOrAtLeast(5, 7); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.PRESERVE; + } + + @Override + public StringBuilder generateKnownFunction(KnownFunction function, List arguments) { + return switch (function) { + case CONCAT -> { + // MySQL treats || as logical OR (unless PIPES_AS_CONCAT); use CONCAT(...). + if (arguments.size() < 2) { + throw new IllegalArgumentException("CONCAT expects 2 or more argument(s), got " + arguments.size()); + } + StringBuilder sb = new StringBuilder("CONCAT("); + for (int i = 0; i < arguments.size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(arguments.get(i)); + } + yield sb.append(")"); + } + case INDEX_OF -> { + if (arguments.size() != 2) { + throw new IllegalArgumentException("INDEX_OF expects 2 argument(s), got " + arguments.size()); + } + yield new StringBuilder("LOCATE(").append(arguments.get(0)).append(", ").append(arguments.get(1)) + .append(")"); + } + // LENGTH stays CHAR_LENGTH: MySQL's LENGTH() counts bytes, CHAR_LENGTH() characters. + default -> super.generateKnownFunction(function, arguments); + }; + } + + // Unified BitOperation methods + + @Override + public java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + StringBuilder buf = new StringBuilder(64); + StringBuilder result = switch (operation) { + case AND -> buf.append("BIT_AND(").append(operand).append(")"); + case OR -> buf.append("BIT_OR(").append(operand).append(")"); + case XOR -> buf.append("BIT_XOR(").append(operand).append(")"); + case NAND -> buf.append("NOT(BIT_AND(").append(operand).append("))"); + case NOR -> buf.append("NOT(BIT_OR(").append(operand).append("))"); + case NXOR -> buf.append("NOT(BIT_XOR(").append(operand).append("))"); + }; + return java.util.Optional.of(result.toString()); + } + + @Override + public boolean supportsBitAggregation(BitOperation operation) { + return true; // MySQL supports all bit operations + } + + @Override + public java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_DISC", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_CONT", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public boolean supportsPercentileDisc() { + // Numeric compare — see requiresOrderByAlias (MariaDB "10.x"/"11.x" vs "8.0"). + return dialectVersion.isUnknownOrAtLeast(8, 0); + } + + @Override + public boolean supportsPercentileCont() { + // Numeric compare — see requiresOrderByAlias (MariaDB "10.x"/"11.x" vs "8.0"). + return dialectVersion.isUnknownOrAtLeast(8, 0); + } + + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + StringBuilder buf = new StringBuilder(64); + buf.append("GROUP_CONCAT"); + buf.append("( "); + if (distinct) { + buf.append("DISTINCT "); + } + buf.append(operand); + if (columns != null && !columns.isEmpty()) { + buf.append(" ORDER BY "); + buf.append(buildOrderedColumnsClause(columns)); + } + if (separator != null) { + buf.append(" SEPARATOR '").append(separator).append("'"); + } + buf.append(")"); + // GROUP_CONCAT(DISTINCT cate_id ORDER BY cate_id ASC SEPARATOR ' ') + return java.util.Optional.of((buf).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("NTH_VALUE", operand, ignoreNulls, n, columns, false)).toString()); + } + + @Override + public boolean supportsNthValue() { + return true; + } + + @Override + public boolean supportsListAgg() { + return true; + } + + // -------------------- Trigger procedures -------------------- + + @Override + public Optional createTriggerProcedure(String procedureName, String schemaName, String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for MySQL trigger procedure"); + } + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("CREATE PROCEDURE " + qualified + "() " + body); + } + + @Override + public String createTriggerUsingProcedure(String triggerName, String schemaName, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming timing, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent event, + org.eclipse.daanse.sql.model.schema.TableReference table, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope scope, String whenCondition, + String procedureName) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return createTrigger(triggerName, timing, event, table, scope, whenCondition, "CALL " + qualified + "()"); + } + + /** MySQL/MariaDB: {@code DROP PROCEDURE [IF EXISTS] schema.procedureName}. */ + @Override + public Optional dropProcedure(String procedureName, String schemaName, boolean ifExists) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("DROP PROCEDURE " + (ifExists ? "IF EXISTS " : "") + qualified); + } + + // -------------------- DDL — ALTER (MySQL: MODIFY syntax) -------------------- + + /** MySQL: {@code ALTER TABLE x MODIFY COLUMN c }. */ + @Override + public String alterColumnType(TableReference table, String columnName, ColumnMetaData newMeta) { + if (newMeta == null) { + throw new IllegalArgumentException("newMeta must not be null for ALTER COLUMN"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY COLUMN ") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(newMeta)).toString(); + } + + @Override + public String alterColumnSetNullability(TableReference table, String columnName, boolean nullable) { + throw new UnsupportedOperationException("MySQL requires the column type when altering nullability — use the " + + "alterColumnSetNullability(table, column, nullable, currentMeta) overload."); + } + + @Override + public String alterColumnSetNullability(TableReference table, String columnName, boolean nullable, + ColumnMetaData currentMeta) { + if (currentMeta == null) { + throw new IllegalArgumentException("currentMeta must not be null for MySQL ALTER COLUMN"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY COLUMN ") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(currentMeta)) + .append(nullable ? " NULL" : " NOT NULL").toString(); + } + + // SET DEFAULT / DROP DEFAULT inherit the SQL-99 default — MySQL 8.0+ accepts + // it. + + /** + * MySQL: {@code ALTER TABLE x RENAME INDEX old TO new} — index names are + * table-scoped. + */ + @Override + public String renameIndex(String oldName, String newName, TableReference table) { + if (table == null) { + throw new IllegalArgumentException("table must not be null for MySQL RENAME INDEX"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" RENAME INDEX ") + .append(quoteIdentifier(oldName)).append(" TO ").append(quoteIdentifier(newName)).toString(); + } + + /** MySQL has no constraint rename. */ + @Override + public String renameConstraint(TableReference table, String oldName, String newName) { + return null; + } + + private static org.eclipse.daanse.sql.dialect.api.DialectInitData initDataFor(java.sql.Connection c) { + try { + return org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(c); + } catch (java.sql.SQLException e) { + return org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults(); + } + } +} diff --git a/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialectFactory.java b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialectFactory.java new file mode 100644 index 0000000..7e4bf80 --- /dev/null +++ b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialectFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mysql; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("MYSQL") +public class MySqlDialectFactory extends AbstractDialectFactory { + + @Override + public Dialect createDialect(DialectInitData init) { + if (MySqlDialect.looksLikeInfobright(init)) { + throw new IllegalStateException("Snapshot looks like Infobright (productVersion=" + init.productVersion() + + "); use InfobrightDialectFactory"); + } + return super.createDialect(init); + } + + @Override + public Function getConstructorFunction() { + return MySqlDialect::new; + } + +} diff --git a/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/package-info.java b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/package-info.java new file mode 100644 index 0000000..c9fd4c4 --- /dev/null +++ b/dialect/db/mysql/src/main/java/org/eclipse/daanse/sql/dialect/db/mysql/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.mysql; diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java new file mode 100644 index 0000000..91ca61a --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlDialect3Test.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.mysql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class MySqlDialect3Test { + private static final String ILLEGAL_BOOLEAN_LITERAL = "illegal for this dialect boolean literal"; + private static final String ILLEGAL_BOOLEAN_LITERAL_MESSAGE = "Illegal BOOLEAN literal: "; + private static final String BOOLEAN_LITERAL_TRUE = "True"; + private static final String BOOLEAN_LITERAL_FALSE = "False"; + private static final String BOOLEAN_LITERAL_ONE = "1"; + private static final String BOOLEAN_LITERAL_ZERO = "0"; + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private Dialect dialect; + private StringBuilder buf; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("MYSQL"); + when(metaData.getDatabaseProductVersion()).thenReturn("5.0"); + dialect = new MySqlDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + buf = new StringBuilder(); + } + + @Test + void testAllowsRegularExpressionInWhereClause() { + assertTrue(dialect.allowsRegularExpressionInWhereClause()); + } + + @Test + void testGenerateRegularExpression_InvalidRegex() throws Exception { + assertTrue(dialect.regexGenerator().generateRegularExpression("table.column", "(a").isEmpty(), + "Invalid regex should be ignored"); + } + + @Test + void testGenerateRegularExpression_CaseInsensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get() + .toString(); + assertEquals("table.column IS NOT NULL AND UPPER(table.column) REGEXP '.*A.*'", sql); + } + + @Test + void testGenerateRegularExpression_CaseSensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString(); + assertEquals("table.column IS NOT NULL AND table.column REGEXP '.*a.*'", sql); + } + + @Test + void testQuoteBooleanLiteral_True() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_TRUE); + assertEquals(BOOLEAN_LITERAL_TRUE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_False() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_FALSE); + assertEquals(BOOLEAN_LITERAL_FALSE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_One() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ONE); + assertEquals(BOOLEAN_LITERAL_ONE, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_Zero() throws Exception { + assertEquals(0, buf.length()); + dialect.quoteBooleanLiteral(buf, BOOLEAN_LITERAL_ZERO); + assertEquals(BOOLEAN_LITERAL_ZERO, buf.toString()); + } + + @Test + void testQuoteBooleanLiteral_TrowsException() throws Exception { + assertEquals(0, buf.length()); + try { + dialect.quoteBooleanLiteral(buf, ILLEGAL_BOOLEAN_LITERAL); + fail("The illegal boolean literal exception should appear BUT it was not."); + } catch (NumberFormatException e) { + assertEquals(ILLEGAL_BOOLEAN_LITERAL_MESSAGE + ILLEGAL_BOOLEAN_LITERAL, e.getMessage()); + } + } + +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java new file mode 100644 index 0000000..fb0073b --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsRoundTripTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.mysql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +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.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MySqlGeneratorsRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:8.4").withDatabaseName("rt") + .withUsername("rt").withPassword("rt"); + + private Connection conn; + private MySqlDialect dialect; + private TableReference users; + + @BeforeAll + void setUp() throws Exception { + conn = java.sql.DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + dialect = new MySqlDialect(DialectInitData.fromConnection(conn)); + users = new TableReference(Optional.of(new SchemaReference(Optional.empty(), "rt")), "users", + TableReference.TYPE_TABLE); + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE `rt`.`users` (id INT PRIMARY KEY, name VARCHAR(50))"); + s.execute("INSERT INTO `rt`.`users` VALUES (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')"); + } + } + + @AfterAll + void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_limit_offset_executes() throws SQLException { + // MySQL paginate emits ` LIMIT off, lim` — different shape from PG. + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertThat(tail).isEqualTo(" LIMIT 1, 2"); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM `rt`.`users` ORDER BY id" + tail, 2, 2); + } + + @Test + void pagination_offset_only_uses_huge_limit_trick() throws SQLException { + // MySQL has no offset-only form, so the generator emits LIMIT off, MAX_UINT64. + // WHERE id <= 5 isolates this test from rows inserted by the upsert test. + String tail = dialect.paginationGenerator().paginate(OptionalLong.empty(), OptionalLong.of(2)); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM `rt`.`users` WHERE id <= 5 ORDER BY id" + tail, 3, 3); + } + + @Test + void upsert_on_duplicate_key_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), + List.of("name")); + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + // ON DUPLICATE KEY UPDATE returns 1 for insert, 2 for update. + assertExecuteUpdateAffected(conn, sql1, 1, "first upsert inserts a new row"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql2, 2, "second upsert updates the existing row"); + assertFirstStringEquals(conn, "SELECT name FROM `rt`.`users` WHERE id = 10", "second"); + } + + @Test + void returning_unsupported_returns_empty() { + // MySQL has no RETURNING — callers fall back to LAST_INSERT_ID() or SELECT. + assertThat(dialect.returningGenerator().returning(List.of("id"))).isEmpty(); + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java new file mode 100644 index 0000000..26654c1 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlGeneratorsTest.java @@ -0,0 +1,120 @@ +/* + * 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.mysql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.table; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpec; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.junit.jupiter.api.Test; + +/** Smoke tests for MySQL's engine-specific overrides of new generators. */ +class MySqlGeneratorsTest { + + private final MySqlDialect d = new MySqlDialect(); + + @Test + void pagination_offset_limit() { + assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.of(40))) + .isEqualTo(" LIMIT 40, 20"); + } + + @Test + void pagination_limit_only() { + assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.empty())).isEqualTo(" LIMIT 20"); + } + + @Test + void pagination_offset_only_uses_huge_limit_trick() { + assertThat(d.paginationGenerator().paginate(OptionalLong.empty(), OptionalLong.of(40))) + .isEqualTo(" LIMIT 40, 18446744073709551615"); + } + + @Test + void upsert_on_duplicate_key_update() { + MergeGenerator.UpsertSpec spec = upsertSpec(table("test", "USERS"), "ID", "NAME"); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql).contains("INSERT INTO `test`.`USERS`") + .contains("ON DUPLICATE KEY UPDATE `NAME` = VALUES(`NAME`)"); + } + + @Test + void known_function_concat_uses_concat_call() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.CONCAT, List.of("a", "b", "c"))) + .hasToString("CONCAT(a, b, c)"); + } + + @Test + void known_function_index_of_uses_locate() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h"))) + .hasToString("LOCATE(n, h)"); + } + + @Test + void known_function_length_stays_char_length() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.LENGTH, List.of("x"))) + .hasToString("CHAR_LENGTH(x)"); + } + + @Test + void known_function_delegates_default_spellings() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.YEAR, List.of("x"))) + .hasToString("EXTRACT(YEAR FROM x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.NOW, List.of())) + .hasToString("CURRENT_TIMESTAMP"); + } + + @Test + void select_hint_block_after_select_keyword() { + assertThat(d.hintGenerator().selectHint(List.of(new StatementHint("MAX_EXECUTION_TIME", List.of("1000")), + new StatementHint("NO_INDEX_MERGE", List.of())))) + .hasToString("/*+ MAX_EXECUTION_TIME(1000) NO_INDEX_MERGE */ "); + } + + @Test + void select_hint_multi_argument_hint() { + assertThat(d.hintGenerator().selectHint( + List.of(new StatementHint("BKA", List.of("t1", "t2"))))) + .hasToString("/*+ BKA(t1, t2) */ "); + } + + @Test + void select_hint_strips_comment_terminator() { + assertThat(d.hintGenerator().selectHint( + List.of(new StatementHint("EVIL*/", List.of("a*/rg"))))) + .hasToString("/*+ EVIL(arg) */ "); + } + + @Test + void select_hint_empty_list_emits_nothing() { + assertThat(d.hintGenerator().selectHint(List.of())).isEmpty(); + } + + @Test + void statement_option_stays_default_empty() { + assertThat(d.hintGenerator().statementOption(List.of(new StatementHint("RECOMPILE", List.of())))).isEmpty(); + } + + @Test + void known_function_override_guards_arity() { + assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.CONCAT, List.of("a"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("CONCAT"); + assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("INDEX_OF"); + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlInitMetaInfoLatencyTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..992bb24 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlInitMetaInfoLatencyTest.java @@ -0,0 +1,184 @@ +/* + * 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.mysql; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +/** + * {@code createMetaInfo} vs lightweight dialect-init read latency on MySQL 8. + */ +@Testcontainers +class MySqlInitMetaInfoLatencyTest { + + @Container + @SuppressWarnings("resource") + static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:8.4").withDatabaseName("BENCH") + .withUsername("rt").withPassword("rt"); + + private static String jdbcUrl; + private final DatabaseService service = new DatabaseServiceImpl(); + + @BeforeAll + static void setUp() throws Exception { + Class.forName("com.mysql.cj.jdbc.Driver"); + jdbcUrl = CONTAINER.getJdbcUrl(); + } + + @AfterAll + static void tearDown() { + // container auto-stops + } + + @Test + void measure() throws SQLException { + DataSource ds = ds(); + System.out.println(); + System.out.println("=== MySQL 8: createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100, 500 }) { + resetSchema(); + populate(n); + + for (int i = 0; i < 2; i++) { + service.createMetaInfo(ds); + lightweight(ds); + } + long meta = bestOf(3, () -> service.createMetaInfo(ds)); + long light = bestOf(3, () -> lightweight(ds)); + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", n + " tables", meta / 1_000_000.0, + light / 1_000_000.0, light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + private DataSource ds() { + return new javax.sql.DataSource() { + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl, "rt", "rt"); + } + + @Override + public Connection getConnection(String u, String p) throws SQLException { + return DriverManager.getConnection(jdbcUrl, u, p); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter w) { + } + + @Override + public void setLoginTimeout(int s) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return null; + } + + @Override + public T unwrap(Class i) { + return null; + } + + @Override + public boolean isWrapperFor(Class i) { + return false; + } + }; + } + + private static void lightweight(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) + best = t; + } + return best; + } + + private static void resetSchema() throws SQLException { + try (Connection c = DriverManager.getConnection(jdbcUrl, "rt", "rt"); Statement s = c.createStatement()) { + s.execute("DROP DATABASE BENCH"); + s.execute("CREATE DATABASE BENCH"); + } + } + + private static void populate(int n) throws SQLException { + if (n == 0) + return; + try (Connection c = DriverManager.getConnection(jdbcUrl, "rt", "rt"); Statement s = c.createStatement()) { + for (int i = 0; i < n; i++) { + s.execute("CREATE TABLE T_" + i + " (ID INT PRIMARY KEY, NAME VARCHAR(50)," + + " VAL DECIMAL(12,3), BIRTHDAY DATE, CREATED DATETIME)"); + if (i > 0) { + s.execute("ALTER TABLE T_" + i + " ADD CONSTRAINT FK_" + i + " FOREIGN KEY (ID) REFERENCES T_" + + (i - 1) + "(ID)"); + } + s.execute("CREATE INDEX IDX_" + i + "_NAME ON T_" + i + "(NAME)"); + } + } + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlQuotingPolicyTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlQuotingPolicyTest.java new file mode 100644 index 0000000..ebed613 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/MySqlQuotingPolicyTest.java @@ -0,0 +1,81 @@ +/* + * 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.mysql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +class MySqlQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + + private MySqlDialect dialectNever() { + MySqlDialect d = new MySqlDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + private static ColumnMetaData intColumn() { + return new ColumnMetaDataRecord(JDBCType.INTEGER, "INT", OptionalInt.of(10), OptionalInt.empty(), + OptionalInt.empty(), ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), Optional.empty(), + Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterColumnType_unquoted() { + assertThat(dialectNever().ddlGenerator().alterColumnType(EMP, "EMAIL", intColumn())).doesNotContain("`") + .contains("EMPLOYEES").contains("EMAIL"); + } + + @Test + void alterColumnSetNullability_with_meta_unquoted() { + // MySQL requires the 4-arg overload (needs current type for MODIFY). + assertThat(dialectNever().ddlGenerator().alterColumnSetNullability(EMP, "EMAIL", false, intColumn())) + .doesNotContain("`").contains("EMPLOYEES").contains("EMAIL"); + } + + @Test + void renameIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", EMP)).doesNotContain("`") + .contains("EMPLOYEES").contains("IDX_OLD").contains("IDX_NEW"); + } + + @Test + void createTriggerProcedure_unquoted() { + assertThat( + dialectNever().ddlGenerator().createTriggerProcedure("AUDIT_PROC", "TEST", "BEGIN END").orElseThrow()) + .doesNotContain("`").contains("AUDIT_PROC").contains("TEST"); + } + + @Test + void dropProcedure_unquoted() { + assertThat(dialectNever().ddlGenerator().dropProcedure("AUDIT_PROC", "TEST", true).orElseThrow()) + .doesNotContain("`").contains("AUDIT_PROC").contains("TEST"); + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("`").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/integration/OSGiServiceTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/integration/OSGiServiceTest.java new file mode 100644 index 0000000..bb26c79 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/integration/OSGiServiceTest.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.dialect.db.mysql.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class OSGiServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(MySqlDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java new file mode 100644 index 0000000..e3953a4 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlAlterRenameOfflineTest.java @@ -0,0 +1,82 @@ +/* + * 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.mysql.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +/** + * MySQL uses {@code MODIFY COLUMN} and table-scoped {@code RENAME INDEX}; no + * constraint rename. + */ +class MySqlAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "appdb"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + + private final MySqlDialect dialect = new MySqlDialect(); + + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size, ColumnMetaData.Nullability n) { + return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), n, + OptionalInt.empty(), Optional.empty(), Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterColumnType_uses_MODIFY_COLUMN() { + assertThat(dialect.ddlGenerator().alterColumnType(T, "SALARY", + meta(JDBCType.DECIMAL, OptionalInt.of(12), ColumnMetaData.Nullability.NULLABLE))) + .startsWith("ALTER TABLE `appdb`.`EMPLOYEES` MODIFY COLUMN `SALARY` ").doesNotContain(" TYPE "); + } + + @Test + void alterColumnSetNullability_typeFree_throws() { + assertThatThrownBy(() -> dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false)) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void alterColumnSetNullability_typeAware_restates_type() { + ColumnMetaData m = meta(JDBCType.VARCHAR, OptionalInt.of(100), ColumnMetaData.Nullability.NULLABLE); + assertThat(dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false, m)) + .startsWith("ALTER TABLE `appdb`.`EMPLOYEES` MODIFY COLUMN `EMAIL` ").endsWith(" NOT NULL") + .contains("VARCHAR(100)"); + } + + @Test + void renameIndex_uses_ALTER_TABLE_RENAME_INDEX() { + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME INDEX `IDX_OLD` TO `IDX_NEW`"); + } + + @Test + void renameConstraint_returns_null() { + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")).isNull(); + } + + @Test + void renameColumn_and_renameTable_inherit_ANSI_form() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME COLUMN `OLD` TO `NEW`"); + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE `appdb`.`EMPLOYEES` RENAME TO `STAFF`"); + } +} diff --git a/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlDdlRoundTripTest.java b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlDdlRoundTripTest.java new file mode 100644 index 0000000..525bcd9 --- /dev/null +++ b/dialect/db/mysql/src/test/java/org/eclipse/daanse/sql/dialect/db/mysql/sqlgen/MySqlDdlRoundTripTest.java @@ -0,0 +1,259 @@ +/* + * 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.mysql.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.mysql.cj.jdbc.MysqlDataSource; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MySqlDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final MySQLContainer CONTAINER = new MySQLContainer<>("mysql:8.4") + // schema == database in MySQL; provision under the test's name. + .withDatabaseName("RT_TEST").withUsername("rt").withPassword("rt"); + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference ORDERS = new TableReference(Optional.of(SCHEMA), "ORDERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", + TableReference.TYPE_VIEW); + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private DataSource dataSource; + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + MysqlDataSource ds = new MysqlDataSource(); + ds.setUrl(CONTAINER.getJdbcUrl()); + ds.setUser(CONTAINER.getUsername()); + ds.setPassword(CONTAINER.getPassword()); + this.dataSource = ds; + this.dialect = new MySqlDialect(); + this.connection = dataSource.getConnection(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), info -> { + /* schema == database in MySQL */ }); + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — MySQL UPDATE … JOIN syntax (same as MariaDB). + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + try (PreparedStatement ps = connection.prepareStatement("UPDATE " + qC + " C JOIN (" + " SELECT " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + " MAX(" + dialect.quoteIdentifier("NOTE") + + ") AS NEW_NAME" + " FROM " + qO + " GROUP BY " + dialect.quoteIdentifier("CUSTOMER_ID") + + ") O ON O." + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID") + + " SET C." + dialect.quoteIdentifier("NAME") + " = O.NEW_NAME")) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), info -> { + }); + // Don't DROP SCHEMA — the container's user only owns RT_TEST. + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/oracle/pom.xml b/dialect/db/oracle/pom.xml new file mode 100644 index 0000000..3c2e3e2 --- /dev/null +++ b/dialect/db/oracle/pom.xml @@ -0,0 +1,121 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.oracle + Eclipse Daanse JDBC DB Dialect Oracle + Oracle database dialect implementation providing Oracle-specific + SQL generation, query optimization, and advanced database feature handling. + Supports Oracle-specific data types, functions, PL/SQL constructs, and + enterprise-grade analytical capabilities for OLAP operations. + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + biz.aQute.bnd + biz.aQute.bndlib + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.metadata + ${project.version} + test + + + com.oracle.database.jdbc + ojdbc11 + 23.4.0.24.05 + test + + + org.testcontainers + oracle-xe + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + + + + maven-surefire-plugin + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java new file mode 100644 index 0000000..0978fa3 --- /dev/null +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialect.java @@ -0,0 +1,622 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * @author jhyde + * @since Nov 23, 2008 + */ +public class OracleDialect extends AbstractJdbcDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "ORACLE"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; + + /** JDBC-free constructor for SQL generation. */ + public OracleDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public OracleDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + /** + * Oracle 9i+: SQL-2003 + * {@code MERGE INTO target USING ... ON ... WHEN MATCHED ... WHEN NOT MATCHED ...}. + */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator mergeGenerator() { + var local = cachedMergeGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator() { + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public java.util.Optional upsert(UpsertSpec spec, java.util.List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException("values must match insertColumns in length"); + } + StringBuilder sb = new StringBuilder("MERGE INTO ").append(qualified(spec.target())) + .append(" T USING (SELECT "); + for (int i = 0; i < values.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(values.get(i)).append(" AS ").append(quoteIdentifier(spec.insertColumns().get(i))); + } + sb.append(" FROM dual) S ON ("); + for (int i = 0; i < spec.keyColumns().size(); i++) { + if (i > 0) + sb.append(" AND "); + sb.append("T.").append(quoteIdentifier(spec.keyColumns().get(i))).append(" = S.") + .append(quoteIdentifier(spec.keyColumns().get(i))); + } + sb.append(")"); + if (!spec.updateColumns().isEmpty()) { + sb.append(" WHEN MATCHED THEN UPDATE SET "); + boolean first = true; + for (String c : spec.updateColumns()) { + if (!first) + sb.append(", "); + sb.append("T.").append(quoteIdentifier(c)).append(" = S.").append(quoteIdentifier(c)); + first = false; + } + } + sb.append(" WHEN NOT MATCHED THEN INSERT ("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") VALUES ("); + for (int i = 0; i < spec.insertColumns().size(); i++) { + if (i > 0) + sb.append(", "); + sb.append("S.").append(quoteIdentifier(spec.insertColumns().get(i))); + } + sb.append(")"); + return java.util.Optional.of(sb.toString()); + } + }; + cachedMergeGenerator = local; + return local; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { + var local = cachedReturningGenerator; + if (local != null) + return local; + if (!dialectVersion.isUnknownOrAtLeast(23, 0)) { + local = super.returningGenerator(); + cachedReturningGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator() { + @Override + public boolean supportsReturning() { + return true; + } + + @Override + public java.util.Optional returning(java.util.List columns) { + if (columns == null || columns.isEmpty()) + return java.util.Optional.empty(); + if (columns.size() == 1 && "*".equals(columns.get(0))) { + // Oracle 23c does NOT support RETURNING * — only explicit columns. + return java.util.Optional.empty(); + } + StringBuilder sb = new StringBuilder(" RETURNING "); + boolean first = true; + for (String c : columns) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)); + first = false; + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedReturningGenerator = local; + return local; + } + + @Override + public boolean supportsDropTableCascade() { + return false; + } + + /** Oracle: no {@code IF NOT EXISTS} on any DDL. */ + @Override + public boolean supportsCreateTableIfNotExists() { + return false; + } + + @Override + public boolean supportsCreateIndexIfNotExists() { + return false; + } + + @Override + public boolean supportsDropIndexIfExists() { + return false; + } + + @Override + public boolean supportsDropViewIfExists() { + return false; + } + + @Override + public boolean supportsDropConstraintIfExists() { + return false; + } + + /** Oracle 11g+ supports {@code CREATE OR REPLACE TRIGGER}. */ + @Override + public boolean supportsCreateOrReplaceTrigger() { + return true; + } + + @Override + public boolean supportsDropTableIfExists() { + return false; + } + + @Override + public boolean allowsFromAlias() { + return false; + } + + @Override + public StringBuilder generateInline(List columnNames, List columnTypes, List valueList) { + return generateInlineGeneric(columnNames, columnTypes, valueList, " from dual", false); + } + + @Override + public boolean supportsGroupingSets() { + return true; + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + return generateOrderByNullsAnsi(expr, ascending, collateNullsLast); + } + + @Override + public boolean allowsJoinOn() { + return false; + } + + @Override + public boolean allowsRegularExpressionInWhereClause() { + return true; + } + + @Override + public Optional generateRegularExpression(String source, String javaRegex) { + try { + Pattern.compile(javaRegex); + } catch (PatternSyntaxException e) { + // Not a valid Java regex. Too risky to continue. + return Optional.empty(); + } + javaRegex = DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex); + StringBuilder mappedFlags = new StringBuilder(); + String[][] mapping = new String[][] { { "c", "c" }, { "i", "i" }, { "m", "m" } }; + javaRegex = extractEmbeddedFlags(javaRegex, mapping, mappedFlags); + + final Matcher escapeMatcher = DialectUtil.ESCAPE_PATTERN.matcher(javaRegex); + while (escapeMatcher.find()) { + javaRegex = javaRegex.replace(escapeMatcher.group(1), escapeMatcher.group(2)); + } + final StringBuilder sb = new StringBuilder(); + sb.append(source); + sb.append(" IS NOT NULL AND "); + sb.append("REGEXP_LIKE("); + sb.append(source); + sb.append(", "); + quoteStringLiteral(sb, javaRegex); + sb.append(", "); + quoteStringLiteral(sb, mappedFlags.toString()); + sb.append(")"); + return Optional.of(sb.toString()); + } + + /** + * @param metaData Resultset metadata + * @param columnIndex index of the column in the result set + * @return For Types.NUMERIC and Types.DECIMAL, getType() will return a + * Type.INT, Type.DOUBLE, or Type.OBJECT based on scale, precision, and + * column name. + * @throws SQLException + */ + @Override + public BestFitColumnType getType(ResultSetMetaData metaData, int columnIndex) throws SQLException { + final int columnType = metaData.getColumnType(columnIndex + 1); + final int precision = metaData.getPrecision(columnIndex + 1); + final int scale = metaData.getScale(columnIndex + 1); + final String columnName = metaData.getColumnName(columnIndex + 1); + BestFitColumnType type; + + if (columnType == Types.NUMERIC || columnType == Types.DECIMAL) { + type = getNumericDecimalType(columnType, precision, scale, columnName); + } else { + type = super.getType(metaData, columnIndex); + } + logTypeInfo(metaData, columnIndex, type); + return type; + } + + private BestFitColumnType getNumericDecimalType(final int columnType, final int precision, final int scale, + final String columnName) { + if (scale == -127 && precision != 0) { + // non zero precision w/ -127 scale means float in Oracle. + return BestFitColumnType.DOUBLE; + } else if (columnType == Types.NUMERIC && (scale == 0 || scale == -127) && precision == 0 + && columnName.startsWith("m")) { + // In GROUPING SETS queries, Oracle + // loosens the type of columns compared to mere GROUP BY + // queries. We need integer GROUP BY columns to remain integers, + // otherwise the segments won't be found; but if we convert + // measure (whose column names are like "m0", "m1") to integers, + // data loss will occur. DOUBLE (not OBJECT) avoids the integer + // truncation while keeping measure values on the same double + // path every other dialect uses (OBJECT let Oracle BigDecimals + // through to the cell layer, where exact half-way decimal sums + // format differently than double-summed values). + return BestFitColumnType.DOUBLE; + } else if (scale == -127 && precision == 0) { + return BestFitColumnType.INT; + } else if (scale == 0 && (precision == 38 || precision == 0)) { + // NUMBER(38, 0) is conventionally used in + // Oracle for integers of unspecified precision, so let's be + // bold and assume that they can fit into an int. + return BestFitColumnType.INT; + } else if (scale == 0 && precision <= 9) { + // An int (up to 2^31 = 2.1B) can hold any NUMBER(10, 0) value + // (up to 10^9 = 1B). + return BestFitColumnType.INT; + } else { + return BestFitColumnType.DOUBLE; + } + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + protected boolean supportsNullsOrdering() { + return true; // Oracle supports NULLS FIRST/LAST + } + + /** + * Oracle spells statement hints as an optimizer block directly after the {@code SELECT} + * keyword: {@code /*+ name(arg1, arg2) name2 *}{@code /} (trailing space). Hints are + * joined by a space; a hint without arguments is the bare name. Any {@code *}{@code /} + * inside a name or argument is stripped so the block cannot terminate early. + */ + @Override + public StringBuilder selectHint(List hints) { + if (hints.isEmpty()) { + return new StringBuilder(); + } + StringBuilder sb = new StringBuilder("/*+ "); + boolean first = true; + for (StatementHint hint : hints) { + if (!first) { + sb.append(' '); + } + first = false; + sb.append(stripCommentEnd(hint.name())); + if (!hint.arguments().isEmpty()) { + sb.append('('); + for (int i = 0; i < hint.arguments().size(); i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(stripCommentEnd(hint.arguments().get(i))); + } + sb.append(')'); + } + } + return sb.append(" */ "); + } + + /** Neutralizes a premature comment terminator inside a hint name/argument. */ + private static String stripCommentEnd(String s) { + return s.replace("*/", ""); + } + + // Unified BitOperation methods + + @Override + public StringBuilder generateKnownFunction(KnownFunction function, List arguments) { + return switch (function) { + case SUBSTRING -> { + if (arguments.size() < 2 || arguments.size() > 3) { + throw new IllegalArgumentException("SUBSTRING expects 2..3 argument(s), got " + arguments.size()); + } + StringBuilder sb = new StringBuilder("SUBSTR(").append(arguments.get(0)).append(", ") + .append(arguments.get(1)); + if (arguments.size() == 3) { + sb.append(", ").append(arguments.get(2)); + } + yield sb.append(")"); + } + case INDEX_OF -> { + if (arguments.size() != 2) { + throw new IllegalArgumentException("INDEX_OF expects 2 argument(s), got " + arguments.size()); + } + // Oracle INSTR takes (haystack, needle) - swapped vs INDEX_OF(needle, haystack). + yield new StringBuilder("INSTR(").append(arguments.get(1)).append(", ").append(arguments.get(0)) + .append(")"); + } + default -> super.generateKnownFunction(function, arguments); + }; + } + + @Override + public java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + StringBuilder buf = new StringBuilder(64); + StringBuilder result = switch (operation) { + case AND -> buf.append("BIT_AND_AGG(").append(operand).append(")"); + case OR -> buf.append("BIT_OR_AGG(").append(operand).append(")"); + case XOR -> buf.append("BIT_XOR(").append(operand).append(")"); + case NAND, NOR, NXOR -> + throw new UnsupportedOperationException("Oracle does not support " + operation + " bit aggregation"); + }; + return java.util.Optional.of(result.toString()); + } + + @Override + public boolean supportsBitAggregation(BitOperation operation) { + return switch (operation) { + case AND, OR, XOR -> true; + case NAND, NOR, NXOR -> false; + }; + } + + @Override + public java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_DISC", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_CONT", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public boolean supportsPercentileDisc() { + return true; + } + + @Override + public boolean supportsPercentileCont() { + return true; + } + + @Override + public java.util.Optional generateListAgg(CharSequence operand, boolean distinct, String separator, + String coalesce, String onOverflowTruncate, List columns) { + StringBuilder buf = new StringBuilder(64); + buf.append("LISTAGG"); + buf.append("( "); + if (distinct) { + buf.append("DISTINCT "); + } + if (coalesce != null) { + buf.append("COALESCE(").append(operand).append(", '").append(coalesce).append("')"); + } else { + buf.append(operand); + } + buf.append(", '"); + if (separator != null) { + buf.append(separator); + } else { + buf.append(", "); + } + buf.append("'"); + if (onOverflowTruncate != null) { + buf.append(" ON OVERFLOW TRUNCATE '").append(onOverflowTruncate).append("' WITHOUT COUNT)"); + } else { + buf.append(")"); + } + if (columns != null && !columns.isEmpty()) { + buf.append(" WITHIN GROUP (ORDER BY "); + buf.append(buildOrderedColumnsClause(columns)); + buf.append(")"); + } + // LISTAGG(NAME, ', ') WITHIN GROUP (ORDER BY ID) + // LISTAGG(COALESCE(NAME, 'null'), ', ') WITHIN GROUP (ORDER BY ID) + // LISTAGG(ID, ', ') WITHIN GROUP (ORDER BY ID) OVER (ORDER BY ID) + // LISTAGG(ID, ';' ON OVERFLOW TRUNCATE 'etc' WITHOUT COUNT) WITHIN GROUP (ORDER + // BY ID) + return java.util.Optional.of((buf).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("NTH_VALUE", operand, ignoreNulls, n, columns, true)).toString()); + } + + @Override + public boolean supportsNthValue() { + return true; + } + + @Override + public boolean supportsNthValueIgnoreNulls() { + return true; + } + + @Override + public boolean supportsListAgg() { + return true; + } + + // -------------------- Trigger procedures -------------------- + + @Override + public Optional createTriggerProcedure(String procedureName, String schemaName, String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for Oracle trigger procedure"); + } + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("CREATE OR REPLACE PROCEDURE " + qualified + " AS " + body); + } + + @Override + public String createTriggerUsingProcedure(String triggerName, String schemaName, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming timing, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent event, + org.eclipse.daanse.sql.model.schema.TableReference table, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope scope, String whenCondition, + String procedureName) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return createTrigger(triggerName, timing, event, table, scope, whenCondition, "BEGIN " + qualified + "; END;"); + } + + /** Oracle: {@code DROP PROCEDURE schema.procedureName}. */ + @Override + public Optional dropProcedure(String procedureName, String schemaName, boolean ifExists) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + // Oracle has no IF EXISTS on DROP PROCEDURE. Callers are expected to + // handle "doesn't exist" themselves; keep ifExists for parity. + return Optional.of("DROP PROCEDURE " + qualified); + } + + @Override + public String addForeignKeyConstraint(TableReference table, String constraintName, java.util.List fkColumns, + TableReference referencedTable, java.util.List referencedColumns, String onDelete, + String onUpdate) { + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD CONSTRAINT ").append(quoteIdentifier(constraintName)); + sb.append(" FOREIGN KEY ("); + appendQuotedCsv(sb, fkColumns); + sb.append(") REFERENCES ").append(qualified(referencedTable)).append(" ("); + appendQuotedCsv(sb, referencedColumns); + sb.append(")"); + if (onDelete != null && !onDelete.isBlank()) + sb.append(" ON DELETE ").append(onDelete); + // Oracle: no ON UPDATE clause — silently drop {@code onUpdate}. + return sb.toString(); + } + + // -------------------- DDL — ALTER (Oracle: MODIFY syntax) -------------------- + + /** + * Oracle: {@code ALTER TABLE x ADD c [NOT NULL] [DEFAULT …]} — no + * {@code COLUMN} keyword. + */ + @Override + public String alterTableAddColumn(TableReference table, + org.eclipse.daanse.sql.model.schema.ColumnDefinition column) { + StringBuilder sb = new StringBuilder("ALTER TABLE "); + sb.append(qualified(table)); + sb.append(" ADD "); + sb.append(quoteIdentifier(column.column().name())); + sb.append(' ').append(nativeType(column.columnMetaData())); + column.columnMetaData().columnDefault().ifPresent(d -> sb.append(" DEFAULT ").append(d)); + if (column.columnMetaData().nullability() == ColumnMetaData.Nullability.NO_NULLS) { + sb.append(" NOT NULL"); + } + return sb.toString(); + } + + /** Oracle: {@code ALTER TABLE x MODIFY (c )}. */ + @Override + public String alterColumnType(TableReference table, String columnName, ColumnMetaData newMeta) { + if (newMeta == null) { + throw new IllegalArgumentException("newMeta must not be null for ALTER COLUMN"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY (") + .append(quoteIdentifier(columnName)).append(' ').append(nativeType(newMeta)).append(")").toString(); + } + + /** Oracle: {@code ALTER TABLE x MODIFY (c [NULL | NOT NULL])}. */ + @Override + public String alterColumnSetNullability(TableReference table, String columnName, boolean nullable) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY (") + .append(quoteIdentifier(columnName)).append(nullable ? " NULL" : " NOT NULL").append(")").toString(); + } + + /** Oracle: {@code ALTER TABLE x MODIFY (c DEFAULT )}. */ + @Override + public String alterColumnSetDefault(TableReference table, String columnName, String defaultExpression) { + if (defaultExpression == null || defaultExpression.isBlank()) { + throw new IllegalArgumentException("defaultExpression must not be blank for SET DEFAULT"); + } + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY (") + .append(quoteIdentifier(columnName)).append(" DEFAULT ").append(defaultExpression).append(")") + .toString(); + } + + /** Oracle: {@code MODIFY (c DEFAULT NULL)} clears the default. */ + @Override + public String alterColumnDropDefault(TableReference table, String columnName) { + return new StringBuilder("ALTER TABLE ").append(qualified(table)).append(" MODIFY (") + .append(quoteIdentifier(columnName)).append(" DEFAULT NULL)").toString(); + } + + // RENAME COLUMN/TABLE/INDEX/CONSTRAINT inherit the SQL-99 default. +} diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectFactory.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectFactory.java new file mode 100644 index 0000000..2a73d99 --- /dev/null +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("ORACLE") +public class OracleDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return OracleDialect::new; + } + +} diff --git a/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/package-info.java b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/package-info.java new file mode 100644 index 0000000..50579f2 --- /dev/null +++ b/dialect/db/oracle/src/main/java/org/eclipse/daanse/sql/dialect/db/oracle/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.oracle; diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/AdditionalTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/AdditionalTest.java new file mode 100644 index 0000000..d62e40a --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/AdditionalTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.junit.jupiter.api.Test; + +class AdditionalTest { + + @Test + void testOracleTypeMapQuirks() throws SQLException { + + ResultSetMetaData resultSet = mock(ResultSetMetaData.class); + DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); + Connection connection = mock(Connection.class); + when(connection.getMetaData()).thenReturn(databaseMetaData); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(0); + when(resultSet.getScale(1)).thenReturn(0); + + Dialect oracleDialect = new OracleDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + + assertSame(BestFitColumnType.INT, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMERIC type with 0 precision, 0 scale should map " + + "to INT, unless column starts with 'm'"); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(5); + when(resultSet.getScale(1)).thenReturn(-127); + + assertSame(BestFitColumnType.DOUBLE, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMERIC type with non-zero precision, -127 scale " + + " should map to DOUBLE. MONDRIAN-1044"); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(9); + when(resultSet.getScale(1)).thenReturn(0); + assertSame(BestFitColumnType.INT, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMERIC type with precision less than 10, 0 scale " + " should map to INT. "); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(38); + when(resultSet.getScale(1)).thenReturn(0); + assertSame(BestFitColumnType.INT, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMERIC type with precision = 38, scale = 0" + + " should map to INT. 38 is a magic number in Oracle " + + " for integers of unspecified precision."); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(20); + when(resultSet.getScale(1)).thenReturn(0); + assertSame(BestFitColumnType.DOUBLE, oracleDialect.getType(resultSet, 0), + "Oracle dialect DECIMAL type with precision > 9, scale = 0" + + " should map to DOUBLE (unless magic #38)"); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("c0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(0); + when(resultSet.getScale(1)).thenReturn(-127); + assertSame(BestFitColumnType.INT, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMBER type with precision =0 , scale = -127" + + " should map to INT. GROUPING SETS queries can shift" + + " scale for columns to -127, whether INT or other NUMERIC." + + " Assume INT unless the column name indicates it is a measure."); + + resultSet = mock(ResultSetMetaData.class); + when(resultSet.getColumnName(1)).thenReturn("m0"); + when(resultSet.getColumnType(1)).thenReturn(Types.NUMERIC); + when(resultSet.getPrecision(1)).thenReturn(0); + when(resultSet.getScale(1)).thenReturn(-127); + assertSame(BestFitColumnType.DOUBLE, oracleDialect.getType(resultSet, 0), + "Oracle dialect NUMBER type with precision =0 , scale = -127" + + " should map to DOUBLE if measure name starts with 'm'" + + " (not INT — data loss; not OBJECT — BigDecimal leaks to the cell layer)"); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java new file mode 100644 index 0000000..38dae0e --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleDialectTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OracleDialectTest { + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private OracleDialect dialect; + + @BeforeEach + public void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("ORACLE"); + dialect = new OracleDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + } + + @Test + void testAllowsRegularExpressionInWhereClause() { + assertTrue(dialect.allowsRegularExpressionInWhereClause()); + } + + @Test + void testGenerateRegularExpression_InvalidRegex() throws Exception { + assertTrue(dialect.regexGenerator().generateRegularExpression("table.column", "(a").isEmpty(), + "Invalid regex should be ignored"); + } + + @Test + void testGenerateRegularExpression_CaseInsensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get() + .toString(); + assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '.*a.*', 'i')", sql); + } + + @Test + void testGenerateRegularExpression_CaseSensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString(); + assertEquals("table.column IS NOT NULL AND REGEXP_LIKE(table.column, '.*a.*', '')", sql); + } +} +//End OracleDialectTest.java diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java new file mode 100644 index 0000000..8c793c3 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsRoundTripTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +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.OracleContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class OracleGeneratorsRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final OracleContainer CONTAINER = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") + .withUsername("rt").withPassword("rt"); + + private Connection conn; + private OracleDialect dialect; + private TableReference users; + + @BeforeAll + void setUp() throws Exception { + Class.forName("oracle.jdbc.OracleDriver"); + conn = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), CONTAINER.getPassword()); + dialect = new OracleDialect(DialectInitData.fromConnection(conn)); + // Oracle: schema == user (uppercased). + String schema = CONTAINER.getUsername().toUpperCase(); + users = new TableReference(Optional.of(new SchemaReference(Optional.empty(), schema)), "USERS", + TableReference.TYPE_TABLE); + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE \"" + schema + "\".\"USERS\" (id NUMBER PRIMARY KEY, name VARCHAR2(50))"); + // Oracle pre-23c can't do multi-row VALUES — INSERT one at a time. + for (int i = 1; i <= 5; i++) { + s.execute( + "INSERT INTO \"" + schema + "\".\"USERS\" VALUES (" + i + ", '" + (char) ('a' + i - 1) + "')"); + } + } + } + + @AfterAll + void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_offset_fetch_next_executes() throws SQLException { + // Default generator emits SQL-2008 OFFSET/FETCH which Oracle 12c+ accepts. + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertThat(tail).contains("OFFSET 1 ROWS").contains("FETCH NEXT 2 ROWS ONLY"); + String schema = CONTAINER.getUsername().toUpperCase(); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"" + schema + "\".\"USERS\" ORDER BY id" + tail, 2, 2); + } + + @Test + void merge_into_using_dual_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("ID"), List.of("ID", "NAME"), + List.of("NAME")); + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql1, 1, "merge: insert path"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql2, 1, "merge: update path"); + String schema = CONTAINER.getUsername().toUpperCase(); + assertFirstStringEquals(conn, "SELECT name FROM \"" + schema + "\".\"USERS\" WHERE id = 10", "second"); + } + + @Test + void returning_unsupported_on_pre_23c_oracle() { + // Oracle XE 21 reports major < 23 → generator falls through to empty default. + // (PG-style RETURNING was introduced in 23c, Aug 2023.) + assertThat(dialect.returningGenerator().returning(List.of("id"))).isEmpty(); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java new file mode 100644 index 0000000..ee0b7eb --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleGeneratorsTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.table; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpec; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.junit.jupiter.api.Test; + +/** Smoke tests for Oracle's engine-specific overrides of new generators. */ +class OracleGeneratorsTest { + + private final OracleDialect d = new OracleDialect(); + + @Test + void merge_into_using_dual() { + MergeGenerator.UpsertSpec spec = upsertSpec(table("RT", "USERS"), "ID", "NAME"); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql) + .contains("MERGE INTO \"RT\".\"USERS\" T USING (SELECT 1 AS \"ID\", 'foo' AS \"NAME\" FROM dual) S") + .contains("ON (T.\"ID\" = S.\"ID\")").contains("WHEN MATCHED THEN UPDATE SET T.\"NAME\" = S.\"NAME\"") + .contains("WHEN NOT MATCHED THEN INSERT"); + } + + @Test + void known_function_substring_uses_substr() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.SUBSTRING, List.of("x", "2"))) + .hasToString("SUBSTR(x, 2)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.SUBSTRING, List.of("x", "2", "3"))) + .hasToString("SUBSTR(x, 2, 3)"); + } + + @Test + void known_function_index_of_uses_instr_with_swapped_arguments() { + // INDEX_OF(needle, haystack) but Oracle INSTR(haystack, needle). + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h"))) + .hasToString("INSTR(h, n)"); + } + + @Test + void known_function_delegates_default_spellings() { + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.TRIM, List.of("x"))) + .hasToString("TRIM(x)"); + assertThat(d.functionGenerator().generateKnownFunction(KnownFunction.MOD, List.of("a", "b"))) + .hasToString("MOD(a, b)"); + } + + @Test + void select_hint_block_after_select_keyword() { + assertThat(d.hintGenerator().selectHint(List.of(new StatementHint("FIRST_ROWS", List.of("10")), + new StatementHint("PARALLEL", List.of("t", "4"))))) + .hasToString("/*+ FIRST_ROWS(10) PARALLEL(t, 4) */ "); + } + + @Test + void select_hint_bare_name_and_sanitizing() { + assertThat(d.hintGenerator().selectHint(List.of(new StatementHint("ALL_ROWS", List.of())))) + .hasToString("/*+ ALL_ROWS */ "); + assertThat(d.hintGenerator().selectHint(List.of(new StatementHint("EVIL*/", List.of("a*/rg"))))) + .hasToString("/*+ EVIL(arg) */ "); + } + + @Test + void select_hint_empty_list_emits_nothing() { + assertThat(d.hintGenerator().selectHint(List.of())).isEmpty(); + } + + @Test + void statement_option_stays_default_empty() { + assertThat(d.hintGenerator().statementOption(List.of(new StatementHint("RECOMPILE", List.of())))).isEmpty(); + } + + @Test + void known_function_override_guards_arity() { + assertThatThrownBy(() -> d.functionGenerator().generateKnownFunction(KnownFunction.SUBSTRING, List.of("x"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("SUBSTRING"); + assertThatThrownBy( + () -> d.functionGenerator().generateKnownFunction(KnownFunction.INDEX_OF, List.of("n", "h", "z"))) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("INDEX_OF"); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleInitMetaInfoLatencyTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..63a5800 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleInitMetaInfoLatencyTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +class OracleInitMetaInfoLatencyTest { + + @Container + @SuppressWarnings("resource") + static final OracleContainer CONTAINER = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") + .withUsername("rt").withPassword("rt"); + + private static String jdbcUrl; + private final DatabaseService service = new DatabaseServiceImpl(); + + @Test + void measure() throws SQLException { + jdbcUrl = CONTAINER.getJdbcUrl(); + DataSource ds = ds(); + System.out.println(); + System.out.println("=== Oracle XE 21c: createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100 }) { + resetSchema(); + populate(n); + for (int i = 0; i < 2; i++) { + runMetaInfo(ds); + lightweight(ds); + } + long meta = bestOf(3, () -> runMetaInfo(ds)); + long light = bestOf(3, () -> lightweight(ds)); + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", n + " tables", meta / 1_000_000.0, + light / 1_000_000.0, light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + private void runMetaInfo(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + service.createMetaInfo(c, new org.eclipse.daanse.sql.jdbc.metadata.OracleMetadataProvider()); + } + } + + private DataSource ds() { + return new javax.sql.DataSource() { + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl, "rt", "rt"); + } + + @Override + public Connection getConnection(String u, String p) throws SQLException { + return DriverManager.getConnection(jdbcUrl, u, p); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter w) { + } + + @Override + public void setLoginTimeout(int s) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return null; + } + + @Override + public T unwrap(Class i) { + return null; + } + + @Override + public boolean isWrapperFor(Class i) { + return false; + } + }; + } + + private static void lightweight(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) + best = t; + } + return best; + } + + private static void resetSchema() throws SQLException { + try (Connection c = DriverManager.getConnection(jdbcUrl, "rt", "rt")) { + // Collect first, then drop — Oracle stream-closes quickly when DDL runs on + // the same connection mid-iteration. + java.util.List tables = new java.util.ArrayList<>(); + try (Statement s2 = c.createStatement(); + java.sql.ResultSet rs = s2.executeQuery("SELECT table_name FROM user_tables")) { + while (rs.next()) + tables.add(rs.getString(1)); + } + try (Statement s = c.createStatement()) { + for (String t : tables) { + try { + s.execute("DROP TABLE \"" + t + "\" CASCADE CONSTRAINTS"); + } catch (SQLException ignore) { + /* best-effort */ } + } + } + } + } + + private static void populate(int n) throws SQLException { + if (n == 0) + return; + try (Connection c = DriverManager.getConnection(jdbcUrl, "rt", "rt"); Statement s = c.createStatement()) { + for (int i = 0; i < n; i++) { + s.execute("CREATE TABLE T_" + i + " (ID NUMBER(10) PRIMARY KEY, NAME VARCHAR2(50)," + + " VAL NUMBER(12,3), BIRTHDAY DATE, CREATED TIMESTAMP)"); + if (i > 0) { + s.execute("ALTER TABLE T_" + i + " ADD CONSTRAINT FK_" + i + " FOREIGN KEY (ID) REFERENCES T_" + + (i - 1) + "(ID)"); + } + s.execute("CREATE INDEX IDX_" + i + "_NAME ON T_" + i + "(NAME)"); + } + } + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleQuotingPolicyTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleQuotingPolicyTest.java new file mode 100644 index 0000000..e1fea74 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/OracleQuotingPolicyTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +class OracleQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "RT"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private OracleDialect dialectNever() { + OracleDialect d = new OracleDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + private static ColumnMetaData numColumn() { + return new ColumnMetaDataRecord(JDBCType.NUMERIC, "NUMBER", OptionalInt.of(10), OptionalInt.empty(), + OptionalInt.empty(), ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), Optional.empty(), + Optional.empty(), ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterTableAddColumn_unquoted() { + assertThat(dialectNever().ddlGenerator().alterTableAddColumn(EMP, + new org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord( + new org.eclipse.daanse.sql.model.schema.ColumnReference(Optional.of(EMP), "NEW_COL"), + numColumn()))) + .doesNotContain("\"").contains("EMPLOYEES").contains("NEW_COL"); + } + + @Test + void alterColumnType_unquoted() { + assertThat(dialectNever().ddlGenerator().alterColumnType(EMP, "EMAIL", numColumn())).doesNotContain("\"") + .contains("EMPLOYEES").contains("EMAIL"); + } + + @Test + void alterColumnSetNullability_unquoted() { + assertThat(dialectNever().ddlGenerator().alterColumnSetNullability(EMP, "EMAIL", false)).doesNotContain("\"") + .contains("EMPLOYEES").contains("EMAIL"); + } + + @Test + void alterColumnSetDefault_unquoted() { + assertThat(dialectNever().ddlGenerator().alterColumnSetDefault(EMP, "STATUS", "'A'")).doesNotContain("\"") + .contains("EMPLOYEES").contains("STATUS"); + } + + @Test + void addForeignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("\"").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void createTriggerProcedure_unquoted() { + assertThat(dialectNever().ddlGenerator().createTriggerProcedure("AUDIT_PROC", "RT", "BEGIN NULL; END;") + .orElseThrow()).doesNotContain("\"").contains("AUDIT_PROC").contains("RT"); + } + + @Test + void dropProcedure_unquoted() { + assertThat(dialectNever().ddlGenerator().dropProcedure("AUDIT_PROC", "RT", true).orElseThrow()) + .doesNotContain("\"").contains("AUDIT_PROC").contains("RT"); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/integration/ServiceTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/integration/ServiceTest.java new file mode 100644 index 0000000..9a0d584 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.oracle.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(OracleDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java new file mode 100644 index 0000000..be92674 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleAlterRenameOfflineTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +/** + * Oracle uses {@code MODIFY (...)} for column changes; renames are SQL-99 + * (default). + */ +class OracleAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "HR"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + + private final OracleDialect dialect = new OracleDialect(); + + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size) { + return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), + ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterColumnType_uses_MODIFY() { + assertThat(dialect.ddlGenerator().alterColumnType(T, "SALARY", meta(JDBCType.DECIMAL, OptionalInt.of(12)))) + .startsWith("ALTER TABLE \"HR\".\"EMPLOYEES\" MODIFY (\"SALARY\" ").endsWith(")"); + } + + @Test + void alterColumnSetNullability_uses_MODIFY() { + assertThat(dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false)) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" MODIFY (\"EMAIL\" NOT NULL)"); + assertThat(dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", true)) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" MODIFY (\"EMAIL\" NULL)"); + } + + @Test + void alterColumnSetDefault_uses_MODIFY_DEFAULT() { + assertThat(dialect.ddlGenerator().alterColumnSetDefault(T, "REGION", "'EU'")) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" MODIFY (\"REGION\" DEFAULT 'EU')"); + } + + @Test + void alterColumnDropDefault_emits_MODIFY_DEFAULT_NULL() { + assertThat(dialect.ddlGenerator().alterColumnDropDefault(T, "REGION")) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" MODIFY (\"REGION\" DEFAULT NULL)"); + } + + @Test + void renames_inherit_ANSI_default() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME TO \"STAFF\""); + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) + .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) + .isEqualTo("ALTER TABLE \"HR\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); + } +} diff --git a/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleDdlRoundTripTest.java b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleDdlRoundTripTest.java new file mode 100644 index 0000000..01a7d01 --- /dev/null +++ b/dialect/db/oracle/src/test/java/org/eclipse/daanse/sql/dialect/db/oracle/sqlgen/OracleDdlRoundTripTest.java @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.oracle.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.oracle.OracleDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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.OracleContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class OracleDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final OracleContainer CONTAINER = new OracleContainer("gvenzl/oracle-xe:21-slim-faststart") + .withUsername("rt").withPassword("rt"); + + private SchemaReference schema; + private TableReference customers; + private TableReference orders; + private TableReference view; + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + Class.forName("oracle.jdbc.OracleDriver"); + this.connection = DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + this.dialect = new OracleDialect(); + // Oracle: schema == user (uppercased). Use the connected user as schema. + String schemaName = CONTAINER.getUsername().toUpperCase(); + this.schema = new SchemaReference(Optional.empty(), schemaName); + this.customers = new TableReference(Optional.of(schema), "CUSTOMERS", TableReference.TYPE_TABLE); + this.orders = new TableReference(Optional.of(schema), "ORDERS", TableReference.TYPE_TABLE); + this.view = new TableReference(Optional.of(schema), "CUSTOMER_ORDERS", TableReference.TYPE_VIEW); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(customers, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(customers, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(customers, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(customers, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(orders, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(orders, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(orders, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(orders, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(orders, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE — skip CREATE SCHEMA (Oracle: schema == user, container provisions + // it). + executeAndVerify(dialect.ddlGenerator().createTable(customers, custCols, custPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createTable(orders, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(customers, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(customers, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", customers, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(orders, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), customers, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(view, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(schema.name(), customers.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(schema.name(), orders.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(schema.name(), customers.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(schema.name(), orders.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — Oracle correlated scalar subquery. + String qC = dialect.quoteIdentifier(schema.name(), customers.name()); + String qO = dialect.quoteIdentifier(schema.name(), orders.name()); + try (PreparedStatement ps = connection.prepareStatement("UPDATE " + qC + " SET " + + dialect.quoteIdentifier("NAME") + " = (" + " SELECT MAX(" + dialect.quoteIdentifier("NOTE") + ")" + + " FROM " + qO + " WHERE " + qO + "." + dialect.quoteIdentifier("CUSTOMER_ID") + " = " + qC + "." + + dialect.quoteIdentifier("ID") + ")")) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(schema.name(), view.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. Skip DROP SCHEMA (Oracle's container user is + // permanent). + executeAndVerify(dialect.ddlGenerator().dropView(view, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(orders, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", customers, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(customers, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(customers, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(orders, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(customers, true, true), info -> { + }); + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/pom.xml b/dialect/db/pom.xml new file mode 100644 index 0000000..35a7ea1 --- /dev/null +++ b/dialect/db/pom.xml @@ -0,0 +1,41 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db + pom + Eclipse Daanse JDBC DB Dialect Database Implementations + + + common + test-support + clickhouse + derby + duckdb + h2 + mariadb + mssqlserver + mysql + oracle + postgresql + sqlite + + diff --git a/dialect/db/postgresql/pom.xml b/dialect/db/postgresql/pom.xml new file mode 100644 index 0000000..6dd76f4 --- /dev/null +++ b/dialect/db/postgresql/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.postgresql + Eclipse Daanse JDBC DB Dialect PostgreSQL + PostgreSQL database dialect implementation providing + PostgreSQL-specific SQL generation, query optimization, and advanced + database feature handling. Supports PostgreSQL-specific data types, + functions, and analytical capabilities for enterprise OLAP operations. + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + test + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + test + + + biz.aQute.bnd + biz.aQute.bndlib + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + test + + + org.postgresql + postgresql + 42.7.3 + test + + + org.testcontainers + postgresql + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + + + + maven-surefire-plugin + + + ${env.DOCKER_HOST} + + + + + + diff --git a/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java new file mode 100644 index 0000000..04e6007 --- /dev/null +++ b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialect.java @@ -0,0 +1,381 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ + +package org.eclipse.daanse.sql.dialect.db.postgresql; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; +import org.eclipse.daanse.sql.dialect.db.common.DialectUtil; + +/** + * @author jhyde + * @since Nov 23, 2008 + */ +public class PostgreSqlDialect extends AbstractJdbcDialect { + + // Lazy-initialized generator caches — built on first call. Stateless + // implementations that capture the dialect's quoter / version, so safe + // to memoize for the dialect's lifetime. + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; + + /** JDBC-free constructor for SQL generation. */ + public PostgreSqlDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — preferred path. */ + public PostgreSqlDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public boolean requiresAliasForFromQuery() { + return true; + } + + @Override + public boolean supportsCreateOrReplaceTrigger() { + if (org.eclipse.daanse.sql.dialect.api.DialectVersion.UNKNOWN.equals(dialectVersion)) { + return true; + } + return dialectVersion.atLeast(14, 0); + } + + /** PostgreSQL: {@code LIMIT n OFFSET m} — both clauses optional. */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + var local = cachedPaginationGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + StringBuilder sb = new StringBuilder(); + limit.ifPresent(l -> { + if (l < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" LIMIT ").append(l); + }); + offset.ifPresent(o -> { + if (o < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" OFFSET ").append(o); + }); + return sb.toString(); + } + }; + cachedPaginationGenerator = local; + return local; + } + + /** PostgreSQL: {@code RETURNING col, col, ...} — supported since 8.2. */ + @Override + public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { + var local = cachedReturningGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator() { + @Override + public boolean supportsReturning() { + return true; + } + + @Override + public java.util.Optional returning(java.util.List columns) { + if (columns == null || columns.isEmpty()) + return java.util.Optional.empty(); + if (columns.size() == 1 && "*".equals(columns.get(0))) { + return java.util.Optional.of(" RETURNING *"); + } + StringBuilder sb = new StringBuilder(" RETURNING "); + boolean first = true; + for (String c : columns) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)); + first = false; + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedReturningGenerator = local; + return local; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator mergeGenerator() { + var local = cachedMergeGenerator; + if (local != null) + return local; + if (!dialectVersion.isUnknownOrAtLeast(9, 5)) { + local = super.mergeGenerator(); + cachedMergeGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator() { + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public java.util.Optional upsert(UpsertSpec spec, java.util.List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException("values must match insertColumns in length"); + } + StringBuilder sb = new StringBuilder("INSERT INTO ").append(qualified(spec.target())).append(" ("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") VALUES ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(values.get(i)); + } + sb.append(") ON CONFLICT ("); + appendQuotedCsv(sb, spec.keyColumns()); + sb.append(") "); + if (spec.updateColumns().isEmpty()) { + sb.append("DO NOTHING"); + } else { + sb.append("DO UPDATE SET "); + boolean first = true; + for (String c : spec.updateColumns()) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)).append(" = EXCLUDED.").append(quoteIdentifier(c)); + first = false; + } + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedMergeGenerator = local; + return local; + } + + /** + * PostgreSQL: + * {@code CREATE OR REPLACE FUNCTION schema.name() RETURNS trigger LANGUAGE plpgsql AS $$ body $$}. + */ + @Override + public Optional createTriggerProcedure(String procedureName, String schemaName, String body) { + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("body must not be blank for PG trigger procedure"); + } + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("CREATE OR REPLACE FUNCTION " + qualified + "() RETURNS trigger LANGUAGE plpgsql AS $$ " + + body + " $$"); + } + + /** + * PostgreSQL: {@code CREATE TRIGGER … EXECUTE FUNCTION schema.procedureName()}. + */ + @Override + public String createTriggerUsingProcedure(String triggerName, String schemaName, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming timing, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent event, + org.eclipse.daanse.sql.model.schema.TableReference table, + org.eclipse.daanse.sql.model.schema.Trigger.TriggerScope scope, String whenCondition, + String procedureName) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return createTrigger(triggerName, timing, event, table, scope, whenCondition, + "EXECUTE FUNCTION " + qualified + "()"); + } + + /** PostgreSQL: {@code DROP FUNCTION [IF EXISTS] schema.procedureName()}. */ + @Override + public Optional dropProcedure(String procedureName, String schemaName, boolean ifExists) { + String qualified = schemaName != null && !schemaName.isBlank() + ? quoteIdentifier(schemaName, procedureName).toString() + : quoteIdentifier(procedureName).toString(); + return Optional.of("DROP FUNCTION " + (ifExists ? "IF EXISTS " : "") + qualified + "()"); + } + + @Override + public java.util.List dropTriggerOnTable(String triggerName, + org.eclipse.daanse.sql.model.schema.TableReference table, boolean ifExists) { + StringBuilder sb = new StringBuilder("DROP TRIGGER "); + if (ifExists) + sb.append("IF EXISTS "); + sb.append(quoteIdentifier(triggerName)); + if (table != null) + sb.append(" ON ").append(qualified(table)); + return java.util.List.of(sb.toString()); + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + // Support for "ORDER BY ... NULLS LAST" was introduced in Postgres 8.3. + if (productVersion.compareTo("8.3") >= 0) { + return generateOrderByNullsAnsi(expr, ascending, collateNullsLast); + } else { + return super.generateOrderByNulls(expr, ascending, collateNullsLast); + } + } + + @Override + public boolean allowsRegularExpressionInWhereClause() { + return true; + } + + @Override + public Optional generateRegularExpression(String source, String javaRegex) { + try { + Pattern.compile(javaRegex); + } catch (PatternSyntaxException e) { + // Not a valid Java regex. Too risky to continue. + return Optional.empty(); + } + javaRegex = DialectUtil.cleanUnicodeAwareCaseFlag(javaRegex); + javaRegex = javaRegex.replace("\\Q", ""); + javaRegex = javaRegex.replace("\\E", ""); + final StringBuilder sb = new StringBuilder(); + sb.append("cast("); + sb.append(source); + sb.append(" as text) is not null and "); + sb.append("cast("); + sb.append(source); + sb.append(" as text) ~ "); + quoteStringLiteral(sb, javaRegex); + return Optional.of(sb.toString()); + } + + @Override + public BestFitColumnType getType(ResultSetMetaData metaData, int columnIndex) throws SQLException { + final int precision = metaData.getPrecision(columnIndex + 1); + final int scale = metaData.getScale(columnIndex + 1); + final int columnType = metaData.getColumnType(columnIndex + 1); + final String columnName = metaData.getColumnName(columnIndex + 1); + + // Check for column names starting with "m" (measure columns like "m0", "m1"): + // In GROUPING SETS queries, Greenplum/PostgreSQL may loosen type metadata for + // measure + // columns. Using OBJECT avoids data loss when converting measure values. This + // pattern + // matches the handling in OracleDialect.getNumericDecimalType() for similar + // cases. + // The "m" prefix check identifies measure columns in aggregation queries. + if (columnType == Types.NUMERIC && scale == 0 && precision == 0 && columnName.startsWith("m")) { + // In Greenplum/PostgreSQL, NUMERIC with no precision or scale means floating point. + // DOUBLE (not OBJECT): the historical data-loss concern was INT truncation of measure + // values; OBJECT let PostgreSQL BigDecimals through to the cell layer, where exact + // half-way decimal sums format differently than the double-summed values every other + // dialect produces (mondrian's GreenplumDialect used DOUBLE here as well). + logTypeInfo(metaData, columnIndex, BestFitColumnType.DOUBLE); + return BestFitColumnType.DOUBLE; + } + return super.getType(metaData, columnIndex); + } + + @Override + public String name() { + return "postgres"; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.LOWER; + } + + @Override + protected boolean supportsNullsOrdering() { + // Support for "ORDER BY ... NULLS LAST" was introduced in Postgres 8.3. + return productVersion != null && productVersion.compareTo("8.3") >= 0; + } + + // Unified BitOperation methods + + @Override + public java.util.Optional generateBitAggregation(BitOperation operation, CharSequence operand) { + StringBuilder buf = new StringBuilder(64); + StringBuilder result = switch (operation) { + case AND -> buf.append("bit_and(").append(operand).append(")"); + case OR -> buf.append("bit_or(").append(operand).append(")"); + case XOR -> buf.append("bit_xor(").append(operand).append(")"); + case NAND -> buf.append("NOT(bit_and(").append(operand).append("))"); + case NOR -> buf.append("NOT(bit_or(").append(operand).append("))"); + case NXOR -> buf.append("NOT(bit_xor(").append(operand).append("))"); + }; + return java.util.Optional.of(result.toString()); + } + + @Override + public boolean supportsBitAggregation(BitOperation operation) { + return true; // PostgreSQL supports all bit operations + } + + @Override + public java.util.Optional generatePercentileDisc(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_DISC", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generatePercentileCont(double percentile, boolean desc, String tableName, + String columnName) { + return java.util.Optional + .of((buildPercentileFunction("PERCENTILE_CONT", percentile, desc, tableName, columnName)).toString()); + } + + @Override + public java.util.Optional generateNthValueAgg(CharSequence operand, boolean ignoreNulls, Integer n, + List columns) { + return java.util.Optional + .of((buildNthValueFunction("NTH_VALUE", operand, ignoreNulls, n, columns, false)).toString()); + } + + @Override + public boolean supportsPercentileDisc() { + return true; + } + + @Override + public boolean supportsPercentileCont() { + return true; + } + + @Override + public boolean supportsNthValue() { + return true; + } +} diff --git a/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectFactory.java b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectFactory.java new file mode 100644 index 0000000..397ca61 --- /dev/null +++ b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectFactory.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.postgresql; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("POSTGRESQL") +public class PostgreSqlDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return PostgreSqlDialect::new; + } + +} diff --git a/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/package-info.java b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/package-info.java new file mode 100644 index 0000000..a975c7f --- /dev/null +++ b/dialect/db/postgresql/src/main/java/org/eclipse/daanse/sql/dialect/db/postgresql/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.postgresql; diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectTest.java new file mode 100644 index 0000000..f5e00e6 --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlDialectTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.postgresql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import org.eclipse.daanse.sql.jdbc.api.meta.IdentifierInfo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PostgreSqlDialectTest { + private Connection connection = mock(Connection.class); + private DatabaseMetaData metaData = mock(DatabaseMetaData.class); + private IdentifierInfo identifierInfo = mock(IdentifierInfo.class); + private PostgreSqlDialect dialect; + + @BeforeEach + protected void setUp() throws Exception { + when(connection.getMetaData()).thenReturn(metaData); + when(metaData.getDatabaseProductName()).thenReturn("POSTGRESQL"); + dialect = new PostgreSqlDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + } + + @Test + void testAllowsRegularExpressionInWhereClause() { + assertTrue(dialect.allowsRegularExpressionInWhereClause()); + } + + @Test + void testGenerateRegularExpression_InvalidRegex() throws Exception { + assertTrue(dialect.regexGenerator().generateRegularExpression("table.column", "(a").isEmpty(), + "Invalid regex should be ignored"); + } + + @Test + void testGenerateRegularExpression_CaseInsensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", "(?i)|(?u).*a.*").get() + .toString(); + assertEquals("cast(table.column as text) is not null and cast(table.column as text) ~ '(?i).*a.*'", sql); + } + + @Test + void testGenerateRegularExpression_CaseSensitive() throws Exception { + String sql = dialect.regexGenerator().generateRegularExpression("table.column", ".*a.*").get().toString(); + assertEquals("cast(table.column as text) is not null and cast(table.column as text) ~ '.*a.*'", sql); + } + +} +//End PostgreSqlDialectTest.java diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java new file mode 100644 index 0000000..c829a48 --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsRoundTripTest.java @@ -0,0 +1,99 @@ +/* + * 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.postgresql; + +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +class PostgreSqlGeneratorsRoundTripTest { + + private static final String DB = "rt"; + private static final String USER = "postgres"; + private static final String PWD = "secret"; + + @SuppressWarnings("resource") + private static final GenericContainer PG = new GenericContainer<>("postgres:15") + .withEnv("POSTGRES_PASSWORD", PWD).withEnv("POSTGRES_DB", DB).withExposedPorts(5432) + .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\\n", 2) + .withStartupTimeout(Duration.ofMinutes(2))); + + private static String jdbcUrl; + private static Connection conn; + private static PostgreSqlDialect dialect; + private static final TableReference USERS = new TableReference( + Optional.of(new SchemaReference(Optional.empty(), "public")), "users", TableReference.TYPE_TABLE); + + @BeforeAll + static void setUp() throws Exception { + PG.start(); + Class.forName("org.postgresql.Driver"); + jdbcUrl = "jdbc:postgresql://" + PG.getHost() + ":" + PG.getMappedPort(5432) + "/" + DB; + conn = DriverManager.getConnection(jdbcUrl, USER, PWD); + dialect = new PostgreSqlDialect(DialectInitData.fromConnection(conn)); + + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE \"public\".\"users\" (id INT PRIMARY KEY, name VARCHAR(50))"); + s.execute("INSERT INTO \"public\".\"users\" VALUES (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')"); + } + } + + @AfterAll + static void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_limit_offset_executes() throws SQLException { + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"public\".\"users\" ORDER BY id" + tail, 2, 2); + } + + @Test + void upsert_on_conflict_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(USERS, List.of("id"), List.of("id", "name"), + List.of("name")); + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql1, 1, "first upsert should insert one row"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql2, 1, "second upsert should update one row"); + assertFirstStringEquals(conn, "SELECT name FROM \"public\".\"users\" WHERE id = 10", "second"); + } + + @Test + void returning_clause_returns_inserted_id() throws SQLException { + String returning = dialect.returningGenerator().returning(List.of("id")).orElseThrow(); + assertFirstIntEquals(conn, "INSERT INTO \"public\".\"users\" (id, name) VALUES (99, 'returned')" + returning, + 99); + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java new file mode 100644 index 0000000..b0d387a --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlGeneratorsTest.java @@ -0,0 +1,65 @@ +/* + * 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.postgresql; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.table; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpec; +import static org.eclipse.daanse.sql.dialect.db.testsupport.GeneratorTestSupport.upsertSpecDoNothing; + +import java.util.List; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.junit.jupiter.api.Test; + +/** Smoke tests for PostgreSQL's engine-specific overrides of new generators. */ +class PostgreSqlGeneratorsTest { + + private final PostgreSqlDialect d = new PostgreSqlDialect(); + + @Test + void pagination_limit_offset() { + assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.of(40))) + .isEqualTo(" LIMIT 20 OFFSET 40"); + } + + @Test + void pagination_limit_only() { + assertThat(d.paginationGenerator().paginate(OptionalLong.of(20), OptionalLong.empty())).isEqualTo(" LIMIT 20"); + } + + @Test + void returning_columns() { + assertThat(d.returningGenerator().supportsReturning()).isTrue(); + assertThat(d.returningGenerator().returning(List.of("id", "name")).orElseThrow()) + .isEqualTo(" RETURNING \"id\", \"name\""); + } + + @Test + void returning_star() { + assertThat(d.returningGenerator().returning(List.of("*")).orElseThrow()).isEqualTo(" RETURNING *"); + } + + @Test + void upsert_on_conflict_do_update() { + MergeGenerator.UpsertSpec spec = upsertSpec(table("public", "USERS"), "ID", "NAME"); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql).contains("INSERT INTO \"public\".\"USERS\"").contains("ON CONFLICT (\"ID\")") + .contains("DO UPDATE SET \"NAME\" = EXCLUDED.\"NAME\""); + } + + @Test + void upsert_on_conflict_do_nothing() { + MergeGenerator.UpsertSpec spec = upsertSpecDoNothing(table("public", "USERS"), "ID", "NAME"); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql).contains("ON CONFLICT (\"ID\") DO NOTHING"); + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlInitMetaInfoLatencyTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..c0bb5f9 --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlInitMetaInfoLatencyTest.java @@ -0,0 +1,185 @@ +/* + * 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.postgresql; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +/** + * {@code createMetaInfo} vs lightweight dialect-init read latency on PostgreSQL + * 15. + */ +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +class PostgreSqlInitMetaInfoLatencyTest { + + private static final String DATABASE = "bench"; + private static final String USER = "postgres"; + private static final String PASSWORD = "secret"; + + @SuppressWarnings("resource") + private static final GenericContainer POSTGRES = new GenericContainer<>("postgres:15") + .withEnv("POSTGRES_PASSWORD", PASSWORD).withEnv("POSTGRES_DB", DATABASE).withExposedPorts(5432) + .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\\n", 2) + .withStartupTimeout(Duration.ofMinutes(2))); + + private static String jdbcUrl; + private final DatabaseService service = new DatabaseServiceImpl(); + + @BeforeAll + static void setUp() throws Exception { + POSTGRES.start(); + Class.forName("org.postgresql.Driver"); + jdbcUrl = "jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getMappedPort(5432) + "/" + DATABASE; + } + + @Test + void measure() throws SQLException { + DataSource ds = ds(); + System.out.println(); + System.out.println("=== PostgreSQL 15: createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100, 500 }) { + resetSchema(); + populate(n); + for (int i = 0; i < 2; i++) { + service.createMetaInfo(ds); + lightweight(ds); + } + long meta = bestOf(3, () -> service.createMetaInfo(ds)); + long light = bestOf(3, () -> lightweight(ds)); + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", n + " tables", meta / 1_000_000.0, + light / 1_000_000.0, light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + private DataSource ds() { + return new javax.sql.DataSource() { + @Override + public Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl, USER, PASSWORD); + } + + @Override + public Connection getConnection(String u, String p) throws SQLException { + return DriverManager.getConnection(jdbcUrl, u, p); + } + + @Override + public java.io.PrintWriter getLogWriter() { + return null; + } + + @Override + public void setLogWriter(java.io.PrintWriter w) { + } + + @Override + public void setLoginTimeout(int s) { + } + + @Override + public int getLoginTimeout() { + return 0; + } + + @Override + public java.util.logging.Logger getParentLogger() { + return null; + } + + @Override + public T unwrap(Class i) { + return null; + } + + @Override + public boolean isWrapperFor(Class i) { + return false; + } + }; + } + + private static void lightweight(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) + best = t; + } + return best; + } + + private static void resetSchema() throws SQLException { + try (Connection c = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); Statement s = c.createStatement()) { + s.execute("DROP SCHEMA public CASCADE"); + s.execute("CREATE SCHEMA public"); + } + } + + private static void populate(int n) throws SQLException { + if (n == 0) + return; + try (Connection c = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); Statement s = c.createStatement()) { + for (int i = 0; i < n; i++) { + s.execute("CREATE TABLE t_" + i + " (id INT PRIMARY KEY, name VARCHAR(50)," + + " val NUMERIC(12,3), birthday DATE, created TIMESTAMP)"); + if (i > 0) { + s.execute("ALTER TABLE t_" + i + " ADD CONSTRAINT fk_" + i + " FOREIGN KEY (id) REFERENCES t_" + + (i - 1) + "(id)"); + } + s.execute("CREATE INDEX idx_" + i + "_name ON t_" + i + "(name)"); + } + } + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlQuotingPolicyTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlQuotingPolicyTest.java new file mode 100644 index 0000000..12fbbb9 --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/PostgreSqlQuotingPolicyTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.dialect.db.postgresql; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class PostgreSqlQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private PostgreSqlDialect dialectNever() { + PostgreSqlDialect d = new PostgreSqlDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP_NAME", EMP, List.of("NAME"), false, false)) + .doesNotContain("\"").contains("EMPLOYEES").contains("IDX_EMP_NAME"); + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("\"").contains("PK_EMP").contains("ID"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("\"").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("\"").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createTriggerProcedure_unquoted() { + // PG-specific override — confirms the override routes through quoteIdentifier. + assertThat( + dialectNever().ddlGenerator().createTriggerProcedure("AUDIT_FN", "PUBLIC", "BEGIN END").orElseThrow()) + .doesNotContain("\"").contains("AUDIT_FN").contains("PUBLIC"); + } + + @Test + void dropProcedure_unquoted() { + assertThat(dialectNever().ddlGenerator().dropProcedure("AUDIT_FN", "PUBLIC", true).orElseThrow()) + .doesNotContain("\"").contains("AUDIT_FN").contains("PUBLIC"); + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/integration/ServiceTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/integration/ServiceTest.java new file mode 100644 index 0000000..2bcc69f --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.postgresql.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.postgresql.PostgreSqlDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(PostgreSqlDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java new file mode 100644 index 0000000..44c7813 --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlAlterRenameOfflineTest.java @@ -0,0 +1,87 @@ +/* + * 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.postgresql.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.db.postgresql.PostgreSqlDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +class PostgreSqlAlterRenameOfflineTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference T = new TableReference(Optional.of(S), "EMPLOYEES", TableReference.TYPE_TABLE); + + private final PostgreSqlDialect dialect = new PostgreSqlDialect(); + + private static ColumnMetaData meta(JDBCType jdbc, OptionalInt size) { + return new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, OptionalInt.empty(), OptionalInt.empty(), + ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + } + + @Test + void alterColumnType_emits_ansi_TYPE_form() { + assertThat(dialect.ddlGenerator().alterColumnType(T, "SALARY", meta(JDBCType.DECIMAL, OptionalInt.of(12)))) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ALTER COLUMN \"SALARY\" TYPE DECIMAL(12)"); + } + + @Test + void alterColumnSetNullability_uses_SET_NOT_NULL() { + assertThat(dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", false)) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ALTER COLUMN \"EMAIL\" SET NOT NULL"); + assertThat(dialect.ddlGenerator().alterColumnSetNullability(T, "EMAIL", true)) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ALTER COLUMN \"EMAIL\" DROP NOT NULL"); + } + + @Test + void alterColumnSetDefault_emits_SET_DEFAULT() { + assertThat(dialect.ddlGenerator().alterColumnSetDefault(T, "REGION", "'EU'")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ALTER COLUMN \"REGION\" SET DEFAULT 'EU'"); + } + + @Test + void alterColumnDropDefault_emits_DROP_DEFAULT() { + assertThat(dialect.ddlGenerator().alterColumnDropDefault(T, "REGION")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ALTER COLUMN \"REGION\" DROP DEFAULT"); + } + + @Test + void renameColumn_emits_ANSI_form() { + assertThat(dialect.ddlGenerator().renameColumn(T, "OLD", "NEW")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME COLUMN \"OLD\" TO \"NEW\""); + } + + @Test + void renameTable_emits_RENAME_TO() { + assertThat(dialect.ddlGenerator().renameTable(T, "STAFF")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME TO \"STAFF\""); + } + + @Test + void renameIndex_emits_ALTER_INDEX() { + assertThat(dialect.ddlGenerator().renameIndex("IDX_OLD", "IDX_NEW", T)) + .isEqualTo("ALTER INDEX \"IDX_OLD\" RENAME TO \"IDX_NEW\""); + } + + @Test + void renameConstraint_emits_RENAME_CONSTRAINT() { + assertThat(dialect.ddlGenerator().renameConstraint(T, "OLD_FK", "NEW_FK")) + .isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" RENAME CONSTRAINT \"OLD_FK\" TO \"NEW_FK\""); + } +} diff --git a/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlDdlRoundTripTest.java b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlDdlRoundTripTest.java new file mode 100644 index 0000000..e8b54ea --- /dev/null +++ b/dialect/db/postgresql/src/test/java/org/eclipse/daanse/sql/dialect/db/postgresql/sqlgen/PostgreSqlDdlRoundTripTest.java @@ -0,0 +1,263 @@ +/* + * 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.postgresql.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.postgresql.PostgreSqlDialect; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +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.postgresql.ds.PGSimpleDataSource; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class PostgreSqlDdlRoundTripTest { + + @Container + @SuppressWarnings("resource") + static final PostgreSQLContainer CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") + .withDatabaseName("rt").withUsername("rt").withPassword("rt"); + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = new TableReference(Optional.of(SCHEMA), "CUSTOMERS", + TableReference.TYPE_TABLE); + private static final TableReference ORDERS = new TableReference(Optional.of(SCHEMA), "ORDERS", + TableReference.TYPE_TABLE); + private static final TableReference VIEW = new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", + TableReference.TYPE_VIEW); + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private DataSource dataSource; + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + PGSimpleDataSource ds = new PGSimpleDataSource(); + ds.setURL(CONTAINER.getJdbcUrl()); + ds.setUser(CONTAINER.getUsername()); + ds.setPassword(CONTAINER.getPassword()); + this.dataSource = ds; + this.dialect = new PostgreSqlDialect(); + this.connection = dataSource.getConnection(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) + connection.close(); + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord(jdbc, jdbc.getName(), size, scale, OptionalInt.empty(), + nullability, OptionalInt.empty(), Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(100), + OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(50), + OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), + Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty(), + OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10), + OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(200), + OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // CREATE + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), + info -> assertThat(info.structureInfo().schemas().stream().map(SchemaReference::name).toList()) + .contains(SCHEMA.name())); + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), info -> { + /* tables visible via JDBC TABLES; loader may need time */ }); + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addCheckConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), info -> { + }); + executeAndVerify( + dialect.ddlGenerator().createIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { + }); + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), "CASCADE", null), info -> { + }); + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + "JOIN " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + "ON O." + + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"), + false), info -> { + }); + + // INSERT — both tables. + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement("INSERT INTO " + + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // Cross-table UPDATE — CUSTOMERS.NAME ← MAX(ORDERS.NOTE). + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + // PostgreSQL: UPDATE … FROM (joined update). Different syntax from MariaDB's + // UPDATE … JOIN. + try (PreparedStatement ps = connection.prepareStatement("UPDATE " + qC + " AS C SET " + + dialect.quoteIdentifier("NAME") + " = O.NEW_NAME" + " FROM (" + " SELECT " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + " MAX(" + dialect.quoteIdentifier("NOTE") + + ") AS NEW_NAME" + " FROM " + qO + " GROUP BY " + dialect.quoteIdentifier("CUSTOMER_ID") + ") AS O" + + " WHERE O." + dialect.quoteIdentifier("CUSTOMER_ID") + " = C." + dialect.quoteIdentifier("ID"))) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s + .executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + dialect.quoteIdentifier("NAME") + + " FROM " + qC + " ORDER BY " + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // SELECT through view. + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + dialect.quoteIdentifier("TOTAL") + " FROM " + + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + " ORDER BY " + + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // DELETE — FK CASCADE removes child orders. + try (PreparedStatement ps = connection + .prepareStatement("DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // DROP — reverse order. + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), info -> { + }); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), info -> { + }); + // PostgreSQL: schema is independent of database — drop it cleanly. + executeAndVerify(dialect.ddlGenerator().dropSchema(SCHEMA.name(), true, true), + info -> assertThat(info.structureInfo().schemas().stream().map(SchemaReference::name).toList()) + .doesNotContain(SCHEMA.name())); + } + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + check.verify(DB_SERVICE.createMetaInfo(connection)); + } catch (Exception e) { + System.out.println("[round-trip] verification skipped for: " + sql.substring(0, Math.min(80, sql.length())) + + " — " + e.getMessage()); + } + } +} diff --git a/dialect/db/sqlite/pom.xml b/dialect/db/sqlite/pom.xml new file mode 100644 index 0000000..ed83151 --- /dev/null +++ b/dialect/db/sqlite/pom.xml @@ -0,0 +1,64 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.sqlite + Eclipse Daanse JDBC DB Dialect SQLite + + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${revision} + + + biz.aQute.bnd + biz.aQute.bndlib + + + + org.xerial + sqlite-jdbc + 3.45.3.0 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.test-support + ${revision} + test + + + org.assertj + assertj-core + test + + + diff --git a/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java new file mode 100644 index 0000000..10f2926 --- /dev/null +++ b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialect.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.dialect.db.sqlite; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; + +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AbstractJdbcDialect; + +public class SqliteDialect extends AbstractJdbcDialect { + + private static final String SUPPORTED_PRODUCT_NAME = "SQLITE"; + + private volatile org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator cachedPaginationGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator cachedReturningGenerator; + private volatile org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator cachedMergeGenerator; + + /** JDBC-free constructor for SQL generation. */ + public SqliteDialect() { + super(org.eclipse.daanse.sql.dialect.api.DialectInitData.ansiDefaults()); + } + + /** Construct from a captured snapshot — the canonical entry point. */ + public SqliteDialect(org.eclipse.daanse.sql.dialect.api.DialectInitData init) { + super(init); + } + + @Override + public boolean supportsSequences() { + return false; + } + + @Override + public boolean supportsDropConstraintIfExists() { + // SQLite can't drop constraints at all — fall back to drop+recreate. + return false; + } + + @Override + public boolean supportsCreateOrReplaceView() { + return false; + } + + @Override + public boolean supportsDropTableCascade() { + return false; + } + + /** + * SQLite does not support the ANSI derived-column-list aliasing + * {@code (VALUES ...) AS t (c1, c2)} ("near \"(\": syntax error"); inline + * datasets render as the generic {@code select ... union all select ...} + * form instead (SQLite allows SELECT without FROM). + */ + @Override + public StringBuilder generateInline(java.util.List columnNames, java.util.List columnTypes, + java.util.List valueList) { + return generateInlineGeneric(columnNames, columnTypes, valueList, null, false); + } + + @Override + public StringBuilder generateOrderByNulls(CharSequence expr, boolean ascending, boolean collateNullsLast) { + return generateOrderByNullsAnsi(expr, ascending, collateNullsLast); + } + + /** + * SQLite has no static column types: for expression columns (no source table, + * e.g. {@code sum(x)}) the xerial driver derives the reported JDBC type from + * the CURRENT ROW's storage class. A {@code sum()} over a NUMERIC-affinity + * column therefore reports {@code INTEGER} whenever the first row's total + * happens to be integral, an INT/LONG accessor is chosen once for the whole + * result, and every later REAL row is silently truncated by {@code getInt()} + * (e.g. a closure-table salary rollup losing exactly the sum of all + * fractional parts). For numeric expression columns use the OBJECT accessor, + * which reads each row with its own storage class. Plain table columns keep + * the default mapping — their metadata comes from the declared type and is + * row-independent. + */ + @Override + public BestFitColumnType getType(ResultSetMetaData metaData, int columnIndex) throws SQLException { + final int columnType = metaData.getColumnType(columnIndex + 1); + switch (columnType) { + case Types.TINYINT, Types.SMALLINT, Types.INTEGER, Types.BIGINT, Types.NUMERIC, Types.DECIMAL, Types.REAL, + Types.FLOAT, Types.DOUBLE: { + final String table = metaData.getTableName(columnIndex + 1); + if (table == null || table.isEmpty()) { + logTypeInfo(metaData, columnIndex, BestFitColumnType.OBJECT); + return BestFitColumnType.OBJECT; + } + break; + } + default: + break; + } + return super.getType(metaData, columnIndex); + } + + @Override + public String name() { + return SUPPORTED_PRODUCT_NAME.toLowerCase(); + } + + @Override + public org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding caseFolding() { + return org.eclipse.daanse.sql.dialect.api.IdentifierCaseFolding.PRESERVE; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator paginationGenerator() { + var local = cachedPaginationGenerator; + if (local != null) + return local; + local = new org.eclipse.daanse.sql.dialect.api.generator.PaginationGenerator() { + @Override + public String paginate(java.util.OptionalLong limit, java.util.OptionalLong offset) { + if (limit.isEmpty() && offset.isEmpty()) + return ""; + StringBuilder sb = new StringBuilder(); + if (limit.isPresent()) { + long l = limit.getAsLong(); + if (l < 0) + throw new IllegalArgumentException("limit must be >= 0"); + sb.append(" LIMIT ").append(l); + } else { + sb.append(" LIMIT -1"); + } + if (offset.isPresent()) { + long o = offset.getAsLong(); + if (o < 0) + throw new IllegalArgumentException("offset must be >= 0"); + sb.append(" OFFSET ").append(o); + } + return sb.toString(); + } + }; + cachedPaginationGenerator = local; + return local; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator returningGenerator() { + var local = cachedReturningGenerator; + if (local != null) + return local; + if (!dialectVersion.isUnknownOrAtLeast(3, 35)) { + local = super.returningGenerator(); + cachedReturningGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.ReturningGenerator() { + @Override + public boolean supportsReturning() { + return true; + } + + @Override + public java.util.Optional returning(java.util.List columns) { + if (columns == null || columns.isEmpty()) + return java.util.Optional.empty(); + if (columns.size() == 1 && "*".equals(columns.get(0))) { + return java.util.Optional.of(" RETURNING *"); + } + StringBuilder sb = new StringBuilder(" RETURNING "); + boolean first = true; + for (String c : columns) { + if (!first) + sb.append(", "); + sb.append(quoteIdentifier(c)); + first = false; + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedReturningGenerator = local; + return local; + } + + @Override + public org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator mergeGenerator() { + var local = cachedMergeGenerator; + if (local != null) + return local; + if (!dialectVersion.isUnknownOrAtLeast(3, 24)) { + local = super.mergeGenerator(); + cachedMergeGenerator = local; + return local; + } + local = new org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator() { + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public java.util.Optional upsert(UpsertSpec spec, java.util.List values) { + if (values == null || values.size() != spec.insertColumns().size()) { + throw new IllegalArgumentException("values must match insertColumns in length"); + } + StringBuilder sb = new StringBuilder("INSERT INTO ").append(qualified(spec.target())).append(" ("); + appendQuotedCsv(sb, spec.insertColumns()); + sb.append(") VALUES ("); + for (int i = 0; i < values.size(); i++) { + if (i > 0) + sb.append(", "); + sb.append(values.get(i)); + } + sb.append(") ON CONFLICT ("); + appendQuotedCsv(sb, spec.keyColumns()); + sb.append(") DO UPDATE SET "); + for (int i = 0; i < spec.updateColumns().size(); i++) { + if (i > 0) + sb.append(", "); + String col = quoteIdentifier(spec.updateColumns().get(i)).toString(); + sb.append(col).append(" = excluded.").append(col); + } + return java.util.Optional.of(sb.toString()); + } + }; + cachedMergeGenerator = local; + return local; + } + +} diff --git a/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectFactory.java b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectFactory.java new file mode 100644 index 0000000..dea20c2 --- /dev/null +++ b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectFactory.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2023 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +package org.eclipse.daanse.sql.dialect.db.sqlite; + +import java.util.function.Function; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.DialectName; +import org.eclipse.daanse.sql.dialect.db.common.AbstractDialectFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +@Component(service = DialectFactory.class, scope = ServiceScope.SINGLETON) +@DialectName("SQLITE") +public class SqliteDialectFactory extends AbstractDialectFactory { + + @Override + public Function getConstructorFunction() { + return SqliteDialect::new; + } + +} diff --git a/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/package-info.java b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/package-info.java new file mode 100644 index 0000000..8462306 --- /dev/null +++ b/dialect/db/sqlite/src/main/java/org/eclipse/daanse/sql/dialect/db/sqlite/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.sqlite; diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectQuotingPolicyTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectQuotingPolicyTest.java new file mode 100644 index 0000000..b70f8c4 --- /dev/null +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectQuotingPolicyTest.java @@ -0,0 +1,72 @@ +/* + * 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.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.junit.jupiter.api.Test; + +class SqliteDialectQuotingPolicyTest { + + private static final SchemaReference S = new SchemaReference(Optional.empty(), "TEST"); + private static final TableReference EMP = new TableReference(Optional.of(S), "EMPLOYEES", + TableReference.TYPE_TABLE); + private static final TableReference DEPT = new TableReference(Optional.of(S), "DEPARTMENTS", + TableReference.TYPE_TABLE); + + private SqliteDialect dialectNever() { + SqliteDialect d = new SqliteDialect(); + d.setQuotingPolicy(IdentifierQuotingPolicy.NEVER); + return d; + } + + @Test + void primaryKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP", List.of("EMAIL"))) + .doesNotContain("\"").contains("EMPLOYEES").contains("UQ_EMP").contains("EMAIL"); + } + + @Test + void foreignKey_unquoted() { + assertThat(dialectNever().ddlGenerator().addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), DEPT, + List.of("ID"), "NO ACTION", "NO ACTION")).doesNotContain("\"").contains("EMPLOYEES") + .contains("FK_EMP_DEPT").contains("DEPARTMENTS"); + } + + @Test + void renameTable_unquoted() { + assertThat(dialectNever().ddlGenerator().renameTable(EMP, "PERSON")).doesNotContain("\"").contains("EMPLOYEES") + .contains("PERSON"); + } + + @Test + void createIndex_unquoted() { + assertThat(dialectNever().ddlGenerator().createIndex("IDX_EMP", EMP, List.of("NAME"), false, false)) + .doesNotContain("\"").contains("EMPLOYEES").contains("IDX_EMP").contains("NAME"); + } + + @Test + void dropConstraint_unquoted() { + assertThat(dialectNever().ddlGenerator().dropConstraint(EMP, "PK_EMP", true)).doesNotContain("\"") + .contains("EMPLOYEES").contains("PK_EMP"); + } +} diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java new file mode 100644 index 0000000..34e50f1 --- /dev/null +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteDialectTest.java @@ -0,0 +1,92 @@ +/* + * 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.sqlite; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +import org.junit.jupiter.api.Test; + +class SqliteDialectTest { + + private static final TableReference USERS = new TableReference( + Optional.of(new SchemaReference(Optional.empty(), "main")), "USERS", TableReference.TYPE_TABLE); + + @Test + void capability_flags_reflect_sqlite_constraints() { + SqliteDialect dialect = new SqliteDialect(); + // SQLite has no SQL sequences (uses ROWID/AUTOINCREMENT). + assertFalse(dialect.supportsSequences()); + // Constraints can't be dropped on existing tables — must rebuild. + assertFalse(dialect.supportsDropConstraintIfExists()); + // No OR REPLACE on CREATE VIEW. + assertFalse(dialect.supportsCreateOrReplaceView()); + // DROP TABLE doesn't accept CASCADE. + assertFalse(dialect.supportsDropTableCascade()); + // IF EXISTS forms ARE supported on most DDL — defaults remain true. + assertTrue(dialect.supportsDropTableIfExists()); + assertTrue(dialect.supportsCreateTableIfNotExists()); + } + + /** No-arg ctor → version=UNKNOWN → modern-engine assumption: emit RETURNING. */ + @Test + void returning_emitted_when_version_unknown() { + SqliteDialect d = new SqliteDialect(); + assertThat(d.returningGenerator().returning(List.of("id"))).contains(" RETURNING \"id\""); + } + + /** SQLite ≥3.35: emit RETURNING. */ + @Test + void returning_emitted_when_version_at_least_3_35() { + SqliteDialect d = withVersion(3, 35); + assertThat(d.returningGenerator().supportsReturning()).isTrue(); + assertThat(d.returningGenerator().returning(List.of("id", "name")).orElseThrow()) + .isEqualTo(" RETURNING \"id\", \"name\""); + } + + /** SQLite < 3.35 (e.g., 3.34): falls through to empty default. */ + @Test + void returning_empty_when_version_below_3_35() { + SqliteDialect d = withVersion(3, 34); + assertThat(d.returningGenerator().returning(List.of("id"))).isEmpty(); + } + + /** SQLite ≥3.24: ON CONFLICT … DO UPDATE. */ + @Test + void merge_emits_on_conflict_when_version_at_least_3_24() { + SqliteDialect d = withVersion(3, 24); + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(USERS, List.of("id"), List.of("id", "name"), + List.of("name")); + String sql = d.mergeGenerator().upsert(spec, List.of("1", "'foo'")).orElseThrow(); + assertThat(sql).contains("INSERT INTO \"main\".\"USERS\"") + .contains("ON CONFLICT (\"id\") DO UPDATE SET \"name\" = excluded.\"name\""); + } + + /** SQLite < 3.24 (e.g., 3.23): falls through to empty default. */ + @Test + void merge_unsupported_when_version_below_3_24() { + SqliteDialect d = withVersion(3, 23); + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(USERS, List.of("id"), List.of("id", "name"), + List.of("name")); + assertThat(d.mergeGenerator().upsert(spec, List.of("1", "'foo'"))).isEmpty(); + } + + private static SqliteDialect withVersion(int major, int minor) { + return new SqliteDialect(DialectInitData.ansiDefaults().withVersion(major, minor)); + } +} diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java new file mode 100644 index 0000000..4044361 --- /dev/null +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/SqliteGeneratorsRoundTripTest.java @@ -0,0 +1,87 @@ +/* + * 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.sqlite; + +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertExecuteUpdateAffected; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstIntEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertFirstStringEquals; +import static org.eclipse.daanse.sql.dialect.db.testsupport.RoundTripAssertions.assertSelectIdRowCountAndFirst; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; +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; + +@TestInstance(Lifecycle.PER_CLASS) +class SqliteGeneratorsRoundTripTest { + + private Connection conn; + private SqliteDialect dialect; + private TableReference users; + + @BeforeAll + void setUp() throws Exception { + Class.forName("org.sqlite.JDBC"); + // SQLite has no real schemas; "main" is the implicit default. + conn = DriverManager.getConnection("jdbc:sqlite::memory:"); + dialect = new SqliteDialect(DialectInitData.fromConnection(conn)); + users = new TableReference(Optional.of(new SchemaReference(Optional.empty(), "main")), "users", + TableReference.TYPE_TABLE); + try (Statement s = conn.createStatement()) { + s.execute("CREATE TABLE \"users\" (id INTEGER PRIMARY KEY, name TEXT)"); + s.execute("INSERT INTO \"users\" VALUES (1,'a'),(2,'b'),(3,'c'),(4,'d'),(5,'e')"); + } + } + + @AfterAll + void tearDown() throws SQLException { + if (conn != null && !conn.isClosed()) + conn.close(); + } + + @Test + void pagination_limit_offset_executes() throws SQLException { + // SQLite parses both ANSI " OFFSET m FETCH NEXT n ROWS ONLY" and "LIMIT n + // OFFSET m"; + // default generator emits the ANSI form. + String tail = dialect.paginationGenerator().paginate(OptionalLong.of(2), OptionalLong.of(1)); + assertSelectIdRowCountAndFirst(conn, "SELECT id FROM \"users\" ORDER BY id" + tail, 2, 2); + } + + @Test + void upsert_on_conflict_inserts_then_updates() throws SQLException { + MergeGenerator.UpsertSpec spec = new MergeGenerator.UpsertSpec(users, List.of("id"), List.of("id", "name"), + List.of("name")); + String sql1 = dialect.mergeGenerator().upsert(spec, List.of("10", "'first'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql1, 1, "upsert: insert path"); + String sql2 = dialect.mergeGenerator().upsert(spec, List.of("10", "'second'")).orElseThrow(); + assertExecuteUpdateAffected(conn, sql2, 1, "upsert: update path"); + assertFirstStringEquals(conn, "SELECT name FROM \"users\" WHERE id = 10", "second"); + } + + @Test + void returning_clause_returns_inserted_id() throws SQLException { + String returning = dialect.returningGenerator().returning(List.of("id")).orElseThrow(); + assertFirstIntEquals(conn, "INSERT INTO \"users\" (id, name) VALUES (99, 'returned')" + returning, 99); + } +} diff --git a/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/integration/ServiceTest.java b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/integration/ServiceTest.java new file mode 100644 index 0000000..a3da714 --- /dev/null +++ b/dialect/db/sqlite/src/test/java/org/eclipse/daanse/sql/dialect/db/sqlite/integration/ServiceTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 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 + * + * History: + * This files came from the mondrian project. Some of the Flies + * (mostly the Tests) did not have License Header. + * But the Project is EPL Header. 2002-2022 Hitachi Vantara. + * + * Contributors: + * Hitachi Vantara. + * SmartCity Jena - initial Java 8, Junit5 + */ +package org.eclipse.daanse.sql.dialect.db.sqlite.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.dialect.db.sqlite.SqliteDialectFactory; +import org.junit.jupiter.api.Test; +import org.osgi.test.common.annotation.InjectService; + +class ServiceTest { + @Test + void serviceExists(@InjectService List dialects) throws Exception { + + assertThat(dialects).isNotNull().isNotEmpty().anyMatch(SqliteDialectFactory.class::isInstance); + } +} diff --git a/dialect/db/test-support/pom.xml b/dialect/db/test-support/pom.xml new file mode 100644 index 0000000..717ef23 --- /dev/null +++ b/dialect/db/test-support/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db + ${revision} + ../pom.xml + + org.eclipse.daanse.sql.dialect.db.test-support + Daanse JDBC DB Dialect Test Support + Test scaffolding shared across per-engine dialect tests + (table/schema/UpsertSpec factories). Consumers add this jar at + test scope. Pure helpers, no production code paths exercise it. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + 0.0.1-SNAPSHOT + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${revision} + + + org.assertj + assertj-core + compile + + + diff --git a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java new file mode 100644 index 0000000..3f44f27 --- /dev/null +++ b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/GeneratorTestSupport.java @@ -0,0 +1,62 @@ +/* + * 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.testsupport; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.generator.MergeGenerator; + +/** + * Static factories for SchemaReference / TableReference / UpsertSpec used by + * per-engine generator tests. + */ +public final class GeneratorTestSupport { + + private GeneratorTestSupport() { + // static helpers only + } + + /** A {@link SchemaReference} with no catalog and the given name. */ + public static SchemaReference schema(String name) { + return new SchemaReference(Optional.empty(), name); + } + + public static TableReference table(String schemaName, String name) { + return new TableReference(Optional.of(schema(schemaName)), name, TableReference.TYPE_TABLE); + } + + public static TableReference table(String name) { + return new TableReference(Optional.empty(), name, TableReference.TYPE_TABLE); + } + + /** + * @param target table being upserted + * @param columns column names in insert order; first is the key + */ + public static MergeGenerator.UpsertSpec upsertSpec(TableReference target, String... columns) { + if (columns.length < 1) { + throw new IllegalArgumentException("at least one column (the key) required"); + } + List all = List.of(columns); + List key = List.of(columns[0]); + List update = all.subList(1, all.size()); + return new MergeGenerator.UpsertSpec(target, key, all, update); + } + + public static MergeGenerator.UpsertSpec upsertSpecDoNothing(TableReference target, String... columns) { + if (columns.length < 1) { + throw new IllegalArgumentException("at least one column (the key) required"); + } + return new MergeGenerator.UpsertSpec(target, List.of(columns[0]), List.of(columns), List.of()); + } +} diff --git a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java new file mode 100644 index 0000000..3b0726e --- /dev/null +++ b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/RoundTripAssertions.java @@ -0,0 +1,102 @@ +/* + * 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.testsupport; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +/** + * ResultSet assertion helpers shared by per-engine generator round-trip tests. + * Each helper opens its own Statement/ResultSet so the calling test can pass + * the connection directly. + */ +public final class RoundTripAssertions { + + private RoundTripAssertions() { + // static helpers only + } + + /** + * Run {@code selectIdSql}, count rows, and assert the first {@code id} value. + * + * @param conn open connection + * @param selectIdSql query whose first column is an integer id + * @param expectedCount row count expected + * @param expectedFirstId integer value expected in the first row + * @throws SQLException on database access error + */ + public static void assertSelectIdRowCountAndFirst(Connection conn, String selectIdSql, int expectedCount, + int expectedFirstId) throws SQLException { + try (Statement s = conn.createStatement(); ResultSet rs = s.executeQuery(selectIdSql)) { + int count = 0; + int firstId = -1; + while (rs.next()) { + if (count == 0) + firstId = rs.getInt(1); + count++; + } + assertThat(count).as("row count").isEqualTo(expectedCount); + assertThat(firstId).as("first id").isEqualTo(expectedFirstId); + } + } + + /** + * Execute {@code dml} and assert the returned update count. + * + * @param conn open connection + * @param dml INSERT/UPDATE/DELETE/MERGE statement + * @param expectedAffected expected affected row count from + * {@link Statement#executeUpdate(String)} + * @param description AssertJ description for failure reporting + * @throws SQLException on database access error + */ + public static void assertExecuteUpdateAffected(Connection conn, String dml, int expectedAffected, + String description) throws SQLException { + try (Statement s = conn.createStatement()) { + assertThat(s.executeUpdate(dml)).as(description).isEqualTo(expectedAffected); + } + } + + /** + * Run {@code selectSql} and assert the first column of the first row equals + * {@code expected}. + * + * @param conn open connection + * @param selectSql query whose first column is a String + * @param expected expected String value + * @throws SQLException on database access error + */ + public static void assertFirstStringEquals(Connection conn, String selectSql, String expected) throws SQLException { + try (Statement s = conn.createStatement(); ResultSet rs = s.executeQuery(selectSql)) { + assertThat(rs.next()).as("at least one row").isTrue(); + assertThat(rs.getString(1)).isEqualTo(expected); + } + } + + /** + * Run {@code selectSql} and assert the first column of the first row equals + * {@code expected}. + * + * @param conn open connection + * @param selectSql query whose first column is an int + * @param expected expected int value + * @throws SQLException on database access error + */ + public static void assertFirstIntEquals(Connection conn, String selectSql, int expected) throws SQLException { + try (Statement s = conn.createStatement(); ResultSet rs = s.executeQuery(selectSql)) { + assertThat(rs.next()).as("at least one row").isTrue(); + assertThat(rs.getInt(1)).isEqualTo(expected); + } + } +} diff --git a/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/package-info.java b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/package-info.java new file mode 100644 index 0000000..7f541d3 --- /dev/null +++ b/dialect/db/test-support/src/main/java/org/eclipse/daanse/sql/dialect/db/testsupport/package-info.java @@ -0,0 +1,12 @@ +/* + * 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 + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.dialect.db.testsupport; diff --git a/dialect/pom.xml b/dialect/pom.xml new file mode 100644 index 0000000..d147999 --- /dev/null +++ b/dialect/pom.xml @@ -0,0 +1,65 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.pom.parent + 0.0.7 + + + org.eclipse.daanse.sql.dialect + ${revision} + pom + Eclipse Daanse SQL Dialect + + + 0.0.1-SNAPSHOT + 2.0.9 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + + + + ossrh + Sonatype Nexus Snapshots + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + Database dialect framework providing extensible support for + multiple database systems. Contains API definitions and concrete dialect + implementations for 25+ database systems including PostgreSQL, MySQL, + Oracle, SQL Server, H2, and more. + + api + db + + diff --git a/docs/dialect-migration/01-inventory-and-scope.md b/docs/dialect-migration/01-inventory-and-scope.md new file mode 100644 index 0000000..6a32c67 --- /dev/null +++ b/docs/dialect-migration/01-inventory-and-scope.md @@ -0,0 +1,110 @@ + + +# 01 – Inventar & Umfang + +Zurück zum [Master-Plan](README.md). + +Dieses Dokument listet **alle** Module beider Repositories, klassifiziert sie und grenzt den +Umfang der jetzigen Migration exakt ab. + +--- + +## 1. Repository `org.eclipse.daanse.jdbc.db` (Quelle) + +- Build: Maven-Multi-Modul, Parent `org.eclipse.daanse:org.eclipse.daanse.pom.parent:0.0.7` +- Group: `org.eclipse.daanse`, Version `${revision}` = `0.0.1-SNAPSHOT` +- **Java: 21** +- OSGi-Metadaten via `bnd-maven-plugin` (vom Parent) + DS-Annotationen (`@Component`); **keine** + `bnd.bnd`- und **keine** `META-INF/services`-Dateien. + +Top-Level-Module (5): `api`, `record`, `impl`, `dialect`, `importer`. + +### 1.1 Klassifizierung + +| Modul | Paket-Wurzel | Klasse | Umzug? | +|---|---|---|---| +| `api` | `org.eclipse.daanse.jdbc.db.api` (`.meta/.schema/.sql/.type`) | Metadaten-/Schema-/Typ-/SQL-Modell (Interfaces) | **→ sql** | +| `record` | `org.eclipse.daanse.jdbc.db.record` (`.meta/.schema`) | Record-Impls der `api`-Interfaces | **→ sql** | +| `dialect/api` | `org.eclipse.daanse.jdbc.db.dialect.api` (`.capability/.generator/.type`) | Dialekt-API | **→ sql** | +| `dialect/db/common` | `…dialect.db.common` | Dialekt-Basis (`AbstractJdbcDialect`, `AnsiDialect`, Jdbc*) | **→ sql** | +| `dialect/db/test-support` | `…dialect.db.testsupport` | Test-Helfer (`GeneratorTestSupport`, `RoundTripAssertions`) | **→ sql** | +| `dialect/db/` (aktiv) | `…dialect.db.` | Konkrete Dialekte | **→ sql** (Untermenge, s. §2) | +| `dialect/db/` (inaktiv) | `…dialect.db.` | Konkrete Dialekte | bleibt (spätere Welle) | +| `dialect/db/configurable` | `…dialect.db.configurable` | Laufzeit-konfigurierbarer Dialekt | bleibt (inaktiv) | +| `impl` | `org.eclipse.daanse.jdbc.db.impl` | Metadaten-Engine (`DatabaseServiceImpl`) — **nicht ETL** | **bleibt** (wird sql-Konsument) | +| `importer` + `importer/csv` | `…importer.csv.api/.impl` | **ETL** (CSV-Import) | **bleibt** (Randbedingung) | + +> **Warum `impl` bleibt, obwohl es kein ETL ist:** `impl` ist die JDBC-`DatabaseMetaData`-Engine und +> bildet die Laufzeit-Introspektion ab. Sie wird nach dem Umzug reiner Downstream-Konsument von +> `sql` (siehe [02](02-dependency-and-layering.md)). Ihr Verbleib erzeugt keinen Zyklus, da `sql` +> nach dem Umzug keine `jdbc.db`-Abhängigkeit mehr hat. + +--- + +## 2. Dialekt-Umfang: aktive Untermenge + +Der Reactor `dialect/db/pom.xml` aktiviert derzeit **10** Module; alle übrigen (~27) liegen +auskommentiert auf der Platte. + +**Aktiv (migrieren jetzt):** + +``` +common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite +``` + +- `common` und `test-support` sind Infrastruktur und Voraussetzung für alle Dialekte. +- Einzige Inter-Dialekt-Kante innerhalb der Untermenge: **`mariadb → mysql`** (beide aktiv → in sich + geschlossen). Alle anderen aktiven Dialekte hängen nur an `common` + `api`. +- Die vom SQL-Repo heute konsumierten Dialekte (`common`, `h2`, `mysql`, `mssqlserver`) sind + vollständig enthalten. + +**Inaktiv (bleiben vorerst, spätere Welle):** + +``` +configurable access clickhouse db2 derby firebird googlebigquery greenplum hive hsqldb +impala infobright informix ingres interbase luciddb monetdb neoview netezza nuodb +opensearch pdidataservice redshift snowflake sqlstream sybase teradata vectorwise vertica +``` + +> **Achtung – redshift/snowflake:** Bei diesen beiden Modulen sind Verzeichnisname und +> enthaltenes Paket/Klasse gekreuzt (siehe [07](07-anomalies-and-risks.md)). Beide sind **nicht** in +> der aktiven Untermenge, daher jetzt nicht betroffen; beim Nachziehen zwingend korrigieren. + +**Vollständige Dialekt-Liste (alle 37 konkreten DB-Module):** access, clickhouse, db2 (zwei +Dialektklassen: `Db2Dialect`, `Db2OldAs400Dialect`), derby, duckdb, firebird, googlebigquery, +greenplum, h2, hive, hsqldb, impala, infobright, informix, ingres, interbase, luciddb, mariadb, +monetdb, mssqlserver, mysql, neoview, netezza, nuodb, opensearch, oracle, pdidataservice, +postgresql, redshift, snowflake, sqlite, sqlstream, sybase, teradata, vectorwise, vertica. Plus +`AnsiDialect` (in `common`) und `ConfigurableDialect` (in `configurable`). + +--- + +## 3. Repository `org.eclipse.daanse.sql` (Ziel) + +- Build: Maven-Multi-Modul, gleicher Parent `pom.parent:0.0.7`, Group `org.eclipse.daanse`, + `${revision}` = `0.0.1-SNAPSHOT`. **Java: 17** (wird auf 21 angehoben). +- Bestehende Modulgruppen: + +| Gruppe | Leaf-Module | Rolle | +|---|---|---| +| `deparser` | `api`, `jsqlparser` | Dialekt-bewusste SQL-Erzeugung aus geparsten Statements | +| `guard` | `api`, `jsqltranspiler` | SQL-Sicherheit/-Validierung | +| `statement` | `api`, `impl`, `demo` | Dialekt-bewusster, domänenfreier Query-Builder | + +Diese Gruppen **konsumieren** heute die externen `jdbc.db.dialect.*`- und `jdbc.db.api`-Artefakte; +nach der Migration werden sie reaktor-intern verdrahtet (siehe [05](05-consumer-rewiring.md)). + +--- + +## 4. Umfang-Prüfliste + +- [ ] Jedes der 5 `jdbc.db`-Top-Level-Module ist genau einer Kategorie (umziehen | bleibt) zugeordnet. +- [ ] Aktive Dialekt-Untermenge = exakt die 10 im Reactor gelisteten Module. +- [ ] Inaktive Dialekte explizit als spätere Welle markiert. +- [ ] ETL (`importer/csv`) ausdrücklich ausgenommen. diff --git a/docs/dialect-migration/02-dependency-and-layering.md b/docs/dialect-migration/02-dependency-and-layering.md new file mode 100644 index 0000000..8486354 --- /dev/null +++ b/docs/dialect-migration/02-dependency-and-layering.md @@ -0,0 +1,119 @@ + + +# 02 – Abhängigkeits- & Schichtungsanalyse + +Zurück zum [Master-Plan](README.md). + +Dieses Dokument begründet, **warum** `api` + `record` mitziehen müssen, und zeigt, wie dadurch eine +zirkuläre Repo-Abhängigkeit vermieden wird. + +--- + +## 1. Ist-Abhängigkeiten (innerhalb `jdbc.db`) + +Kanten aus den Poms und Java-Importen (verifiziert): + +``` +dialect.api → api (compile; nutzt api.schema.*, api.sql.*, api.type.*) +dialect.db.common → dialect.api (compile) +dialect.db. → dialect.db.common, dialect.api (compile) +dialect.db. → record (compile; Schema-Record-Impls, z.B. h2/mysql/oracle/…) +dialect.db. → impl (TEST; Round-Trip-Tests, z.B. mysql/mariadb/mssql/oracle/postgresql/derby) +test-support → api, dialect.api (compile) + +impl → api, record, dialect.api (compile) +impl → dialect.db.common, dialect.db.h2 (TEST) +importer.csv → api, record, dialect.api (compile), → impl (runtime) [ETL] + +record → api (compile) +``` + +Inter-Dialekt-Kanten (compile), relevant für die Reihenfolge: + +``` +mariadb → mysql infobright → mysql +greenplum → postgresql netezza → postgresql (snowflake-Modul: Redshift-Code) → postgresql +impala → hive vectorwise → ingres sqlstream → luciddb +``` + +Innerhalb der **aktiven Untermenge** existiert nur `mariadb → mysql`. + +--- + +## 2. Das Zirkularitäts-Problem + +Zöge man **nur** den `dialect`-Teilbaum um und ließe `api`/`record` in `jdbc.db`, entstünde auf +**Repo-Ebene** ein Zyklus: + +``` +sql/dialect.api → jdbc.db/api (Dialekte brauchen das Metadaten-/Typ-Modell) +jdbc.db/impl → sql/dialect.api (Metadaten-Engine braucht die Dialekt-API) +jdbc.db/importer.csv → sql/dialect.api (ETL braucht die Dialekt-API) +``` + +→ `sql` hinge an `jdbc.db` **und** `jdbc.db` an `sql`. Das bricht die Release-Reihenfolge +(keine der beiden Snapshots ließe sich unabhängig bauen/publizieren) und ist unerwünscht. + +--- + +## 3. Auflösung: `api` + `record` ziehen mit + +Da das Metadaten-/Typ-Modell (`api`) und seine Records (`record`) **die einzigen** `jdbc.db`-Bausteine +sind, an denen der Dialekt-Code hängt, werden sie **mitgezogen**. Danach: + +``` +sql/api → (nur Drittanbieter / JDK) # keine jdbc.db-Kante +sql/record → sql/api +sql/dialect.api → sql/api +sql/dialect.db.* → sql/dialect.api, sql/dialect.db.common, sql/record + +jdbc.db/impl → sql/api, sql/record, sql/dialect.api (compile) # einseitig +jdbc.db/importer → sql/api, sql/record, sql/dialect.api (compile) # einseitig +``` + +**Ergebnis:** `sql` ist self-contained (keine `jdbc.db`-Abhängigkeit mehr); `jdbc.db` hängt nur noch +**einseitig** an `sql`. Saubere Schichtung: + +``` + ┌─────────────────────────────────────┐ + │ org.eclipse.daanse.jdbc.db │ (oben: JDBC-Laufzeit + ETL) + │ impl (Metadaten-Engine) │ + │ importer/csv (ETL) │ + └───────────────┬─────────────────────┘ + │ depends on (einseitig) + ┌───────────────▼─────────────────────┐ + │ org.eclipse.daanse.sql │ (unten: SQL-/Dialekt-Fundament) + │ dialect.db.* → dialect.api │ + │ record → api │ + │ guard / deparser / statement │ + └─────────────────────────────────────┘ +``` + +--- + +## 4. Externe (Drittanbieter-) Abhängigkeiten, die mitwandern + +- `org.slf4j:slf4j-api` — compile, in `common` und den meisten Dialekten. +- `biz.aQute.bnd:biz.aQute.bndlib` — compile, nur `oracle`. +- Pro-Dialekt **JDBC-Treiber + Testcontainers** — durchweg **Test-Scope** (z. B. `mysql-connector-j`, + `ojdbc11`, `testcontainers` mysql/oracle-xe/junit-jupiter, `mockito`, `assertj`). Diese + Versionen müssen ins SQL-Repo-`dependencyManagement` übernommen werden (siehe + [06](06-build-and-java21.md)). + +`api`/`record` selbst haben **keine** Laufzeit-Drittabhängigkeit außer dem JDK (und ggf. slf4j). + +--- + +## 5. Konsequenzen für die verbleibenden `jdbc.db`-Module + +- `impl`: Produktionscode (`DatabaseServiceImpl`, `CachingDatabaseService`) importiert **keine** + Dialekte, nur `api`/`record` + `dialect.api`. Nach dem Umzug zeigen diese Deps auf `sql.*`. + Die Round-Trip-**Tests** (`impl/sqlgen`) brauchen zusätzlich `sql.dialect.db.common/h2` (test). +- `importer/csv` (ETL): compile auf `sql.api`/`sql.record`/`sql.dialect.api`, runtime auf `impl`. +- Release-Reihenfolge: **`sql` zuerst** bauen/publizieren, dann `jdbc.db`. diff --git a/docs/dialect-migration/03-package-rename-map.md b/docs/dialect-migration/03-package-rename-map.md new file mode 100644 index 0000000..bc79a33 --- /dev/null +++ b/docs/dialect-migration/03-package-rename-map.md @@ -0,0 +1,96 @@ + + +# 03 – Paket- & Artefakt-Rename + +Zurück zum [Master-Plan](README.md). + +Alle mitgezogenen Module werden von `org.eclipse.daanse.jdbc.db.*` auf `org.eclipse.daanse.sql.*` +umbenannt — Java-Pakete, Maven-ArtifactIds und OSGi-`Bundle-SymbolicName` gleichermaßen. + +--- + +## 1. Java-Paket-Mapping + +| Alt | Neu | +|---|---| +| `org.eclipse.daanse.jdbc.db.api` | `org.eclipse.daanse.sql.api` | +| `org.eclipse.daanse.jdbc.db.api.meta` | `org.eclipse.daanse.sql.api.meta` | +| `org.eclipse.daanse.jdbc.db.api.schema` | `org.eclipse.daanse.sql.api.schema` | +| `org.eclipse.daanse.jdbc.db.api.sql` | `org.eclipse.daanse.sql.api.sql` | +| `org.eclipse.daanse.jdbc.db.api.type` | `org.eclipse.daanse.sql.api.type` | +| `org.eclipse.daanse.jdbc.db.record.meta` | `org.eclipse.daanse.sql.record.meta` | +| `org.eclipse.daanse.jdbc.db.record.schema` | `org.eclipse.daanse.sql.record.schema` | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | +| `org.eclipse.daanse.jdbc.db.dialect.api.capability` | `org.eclipse.daanse.sql.dialect.api.capability` | +| `org.eclipse.daanse.jdbc.db.dialect.api.generator` | `org.eclipse.daanse.sql.dialect.api.generator` | +| `org.eclipse.daanse.jdbc.db.dialect.api.type` | `org.eclipse.daanse.sql.dialect.api.type` | +| `org.eclipse.daanse.jdbc.db.dialect.db.common` | `org.eclipse.daanse.sql.dialect.db.common` | +| `org.eclipse.daanse.jdbc.db.dialect.db.testsupport` | `org.eclipse.daanse.sql.dialect.db.testsupport` | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | + +Regel: **Präfix `org.eclipse.daanse.jdbc.db` → `org.eclipse.daanse.sql`**, Suffix unverändert. +Betrifft `package`-Deklarationen, `import`-Anweisungen und `package-info.java` in **jeder** +mitgezogenen Datei — inkl. der Konsumenten (siehe [05](05-consumer-rewiring.md)). + +> Anmerkung: Das `api.meta`-Paket bildet JDBC-`DatabaseMetaData`-Introspektion ab und ist damit +> semantisch JDBC-nah. Für Konsistenz im SQL-Repo wird es dennoch nach `sql.api.meta` umbenannt +> (einheitliches Präfix). Ein späteres feineres Umbenennen bleibt möglich, ist aber **nicht** Teil +> dieser Migration. + +--- + +## 2. Maven-ArtifactId-Mapping + +ArtifactId spiegelt exakt die Paket-Wurzel (Repo-Konvention). + +| Alt | Neu | +|---|---| +| `org.eclipse.daanse.jdbc.db.api` | `org.eclipse.daanse.sql.api` | +| `org.eclipse.daanse.jdbc.db.record` | `org.eclipse.daanse.sql.record` | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | +| `org.eclipse.daanse.jdbc.db.dialect.db.common` | `org.eclipse.daanse.sql.dialect.db.common` | +| `org.eclipse.daanse.jdbc.db.dialect.db.test-support` | `org.eclipse.daanse.sql.dialect.db.test-support` | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | + +Die neuen Aggregatoren erhalten: +- `org.eclipse.daanse.sql.dialect` (aggregator, ersetzt `…jdbc.db.dialect`) +- `org.eclipse.daanse.sql.dialect.db` (aggregator, ersetzt `…jdbc.db.dialect.db`) + +Group-Id bleibt `org.eclipse.daanse`; Version bleibt `${revision}` = `0.0.1-SNAPSHOT`. + +--- + +## 3. OSGi / DS + +- **`Bundle-SymbolicName`** wird vom `bnd-maven-plugin` aus der ArtifactId abgeleitet → ändert sich + automatisch mit der ArtifactId. Keine manuelle `bnd.bnd`-Pflege nötig (es existieren keine). +- **DS-Komponenten:** Jeder Dialekt exportiert `@Component(service = DialectFactory.class, …)`. Der + vollqualifizierte `DialectFactory`-Typ ändert sich mit dem Paket-Rename; die Annotation selbst + bleibt unverändert, das generierte `OSGI-INF/*.xml` folgt automatisch beim Neubau. +- **Keine `META-INF/services`-Dateien** vorhanden — nichts an ServiceLoader-Registrierung anzupassen. + +--- + +## 4. Lizenz-Header & package-info + +- Jede Datei trägt den EPL-2.0-Header (Contributors „SmartCity Jena“, „Stefan Bischof (bipolis.org)“). + Header bleibt beim Umzug erhalten; nur das Copyright-Jahr bei inhaltlicher Änderung ergänzen. +- Jedes Leaf-Paket hat `package-info.java` — Paketname darin mit umbenennen. +- CI erzwingt Header (`.licenserc.yaml`, `license.templates`) und Javadoc + (`java_check_javadoc.yml`) — nach dem Rename müssen diese Checks grün bleiben. + +--- + +## 5. Mechanik (empfohlen) + +1. `git mv` der Modulverzeichnisse (Historie erhalten). +2. Verzeichnis-Umbenennung der Paketpfade (`.../jdbc/db/...` → `.../sql/...`). +3. Textersetzung `org.eclipse.daanse.jdbc.db` → `org.eclipse.daanse.sql` **nur** in mitgezogenen + Modulen und deren Konsumenten (nicht in `api.meta`-fremden Kontexten). Danach modulweise + `mvn -q -pl -am verify`. diff --git a/docs/dialect-migration/04-migration-procedure.md b/docs/dialect-migration/04-migration-procedure.md new file mode 100644 index 0000000..97203c5 --- /dev/null +++ b/docs/dialect-migration/04-migration-procedure.md @@ -0,0 +1,113 @@ + + +# 04 – Migrationsprozedur (Schritt für Schritt) + +Zurück zum [Master-Plan](README.md). + +Modulweiser Umzug in **Abhängigkeitsreihenfolge**. Jeder Schritt endet mit einem grünen Build, +bevor der nächste beginnt. + +--- + +## 0. Vorbereitung + +- Arbeitszweig in **beiden** Repos anlegen (z. B. `feat/dialect-migration`). +- Beide Repos liegen als Geschwister unter `…/git/daanse/`. +- Für Historie-Erhaltung `git mv` verwenden; alternativ `git filter-repo`, falls die volle + Datei-Historie über Repo-Grenzen gewünscht ist (optional, siehe [07](07-anomalies-and-risks.md)). + +--- + +## 1. Welle A – Fundament (`api`, `record`) + +Für **`api`** (dann analog `record`): + +1. `git mv jdbc.db/api sql/api` (Verzeichnis in den SQL-Reactor). +2. Paketpfade `src/main/java/org/eclipse/daanse/jdbc/db/api/…` → `…/sql/api/…` verschieben. +3. In allen Dateien `org.eclipse.daanse.jdbc.db.api` → `org.eclipse.daanse.sql.api` ersetzen + (`package`, `import`, `package-info`). +4. `api/pom.xml`: Parent auf den SQL-Root setzen, ArtifactId → `org.eclipse.daanse.sql.api`. +5. Root `sql/pom.xml`: `api` ergänzen. +6. `record` genauso; `record/pom.xml`-Dep `…jdbc.db.api` → `…sql.api` (reaktor-intern, + `${project.version}`). +7. `mvn -q -pl api,record -am verify`. + +--- + +## 2. Welle B – Dialekt-Kern + +Reihenfolge: **`dialect/api` → `dialect/db/common` → `dialect/db/test-support`.** + +1. Neue Aggregatoren anlegen: `sql/dialect/pom.xml` (packaging=pom, Parent=SQL-Root) und + `sql/dialect/db/pom.xml` (packaging=pom, Parent=`sql.dialect`). +2. `git mv jdbc.db/dialect/api sql/dialect/api`; Paket-Rename `…jdbc.db.dialect.api` → + `…sql.dialect.api`; Pom-Dep `…jdbc.db.api` → reaktor-intern `…sql.api`. +3. `git mv jdbc.db/dialect/db/common sql/dialect/db/common`; Rename; Dep → `sql.dialect.api`. +4. `git mv jdbc.db/dialect/db/test-support sql/dialect/db/test-support`; Rename; Deps → `sql.api`, + `sql.dialect.api`. +5. `sql.dialect` in Root-`` eintragen; `common`/`test-support` in `sql/dialect/db/pom.xml`. +6. `mvn -q -pl org.eclipse.daanse.sql.dialect.api,…common,…test-support -am verify`. + +--- + +## 3. Welle C – Konkrete Dialekte (aktive Untermenge) + +Für jedes von `duckdb, h2, mariadb, mssqlserver, mysql, oracle, postgresql, sqlite` +(**`mysql` vor `mariadb`**, da `mariadb → mysql`): + +1. `git mv jdbc.db/dialect/db/ sql/dialect/db/`. +2. Paket-Rename `…jdbc.db.dialect.db.` → `…sql.dialect.db.`. +3. `pom.xml`-Deps umstellen: + - compile: `…dialect.db.common` → `sql.dialect.db.common`, `…dialect.api` → `sql.dialect.api`, + `…record` → `sql.record`, `…api` → `sql.api` (alle reaktor-intern, `${project.version}`). + - test: `…jdbc.db.impl` → siehe unten (Cross-Repo!), `…dialect.db.test-support` → + `sql.dialect.db.test-support`; JDBC-Treiber/Testcontainers-Versionen aus dem Quell-Pom + übernehmen. +4. `` in `sql/dialect/db/pom.xml` eintragen. +5. `mvn -q -pl org.eclipse.daanse.sql.dialect.db. -am verify`. + +> **Test-Kante `dialect.db. → jdbc.db.impl`:** Einige Dialekt-Tests (mysql, mariadb, mssqlserver, +> oracle, postgresql, derby) nutzen `DatabaseServiceImpl`. Da `impl` in `jdbc.db` bleibt, würde dies +> eine **Cross-Repo-Test-Abhängigkeit** `sql → jdbc.db` erzeugen und den in +> [02](02-dependency-and-layering.md) aufgelösten Zyklus über die Hintertür wieder einführen. +> **Vorgabe:** Diese Round-Trip-Tests wandern mit `impl` **nicht** mit; sie werden entweder +> (a) auf `sql`-eigene Test-Fixtures umgestellt, oder (b) temporär als `@Disabled` markiert und in +> `jdbc.db` (wo `impl` liegt) als Integrationstests weitergeführt. Entscheidung pro Dialekt in +> [08](08-verification.md) dokumentieren. In der aktiven Untermenge betrifft das mysql, mariadb, +> mssqlserver, oracle, postgresql. + +--- + +## 4. Welle D – SQL-Konsumenten umstellen + +Siehe [05](05-consumer-rewiring.md) §1: `guard`, `deparser`, `statement`, `statement/demo` von den +externen `jdbc.db.*`-Deps auf die neuen reaktor-internen `sql.*`-Module umstellen, Imports anpassen. +Java-21-Anhebung des SQL-Roots (siehe [06](06-build-and-java21.md)). Danach `mvn -q verify` im +gesamten SQL-Reactor. + +--- + +## 5. Welle E – `jdbc.db`-Rückbau + +Siehe [05](05-consumer-rewiring.md) §2: In `jdbc.db` die umgezogenen Module aus dem Reactor entfernen +(`api`, `record`, `dialect` aus Root-``) und `impl`/`importer` auf die **publizierten** +`sql.*`-Snapshots umstellen. `mvn -q verify` im `jdbc.db`-Reactor (setzt voraus, dass `sql`-Snapshots +im lokalen/Sonatype-Repo verfügbar sind → `sql` zuerst `mvn install`). + +--- + +## 6. Reihenfolge-Kurzform + +``` +A: api → record +B: dialect.api → dialect.db.common → test-support +C: mysql → mariadb; duckdb, h2, mssqlserver, oracle, postgresql, sqlite (parallel) +D: SQL-Konsumenten (guard/deparser/statement) + Java 21 +E: jdbc.db-Rückbau (impl/importer auf sql.*-Snapshots) +``` diff --git a/docs/dialect-migration/05-consumer-rewiring.md b/docs/dialect-migration/05-consumer-rewiring.md new file mode 100644 index 0000000..eef0553 --- /dev/null +++ b/docs/dialect-migration/05-consumer-rewiring.md @@ -0,0 +1,102 @@ + + +# 05 – Konsumenten umverdrahten + +Zurück zum [Master-Plan](README.md). + +Nach dem Umzug müssen alle Module, die die Dialekte / `api` / `record` nutzen, ihre +Abhängigkeiten und Importe anpassen — im SQL-Repo auf **reaktor-intern**, im jdbc.db-Repo auf +**publizierte `sql.*`-Snapshots**. + +--- + +## 1. SQL-Repo-Konsumenten (reaktor-intern) + +Betroffene Module und ihre heutigen externen Deps: + +| Modul | Heutige externe Deps (`jdbc.db.*`) | Neu (reaktor-intern `sql.*`) | +|---|---|---| +| `statement/impl` | `dialect.db.common`, `dialect.db.mysql` | `sql.dialect.db.common`, `sql.dialect.db.mysql` | +| `statement/demo` | `dialect.db.common` (`AnsiDialect`), `dialect.db.mysql`, `dialect.db.mssqlserver`, H2 | `sql.dialect.db.common/mysql/mssqlserver` | +| `guard/jsqltranspiler` | `dialect.db.h2`, `dialect.db.common`, `deparser` | `sql.dialect.db.h2`, `sql.dialect.db.common` | +| `deparser/api` | `dialect.api` | `sql.dialect.api` | +| `deparser/jsqlparser` | `dialect.api` | `sql.dialect.api` | +| `statement/api` | `dialect.api` (`generator.KnownFunction`, `generator.StatementHint`), `api.schema.*`, `api.type.BestFitColumnType` | `sql.dialect.api.generator.*`, `sql.api.schema.*`, `sql.api.type.*` | + +**Pom-Änderung** (Beispiel `statement/impl`): externe Koordinaten mit fester Version durch +reaktor-interne mit `${project.version}` ersetzen: + +```xml + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.db.common + 0.0.1-SNAPSHOT + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${project.version} + +``` + +**Import-Änderung** (in allen `.java` der Konsumenten): `org.eclipse.daanse.jdbc.db.dialect` → +`org.eclipse.daanse.sql.dialect`, `org.eclipse.daanse.jdbc.db.api` → `org.eclipse.daanse.sql.api`. +Betroffene Klassen laut Analyse u. a.: +`DialectSqlRenderer`, `BasicDialect*DeParser`, `DialectDeparser`, `Expressions`, `SqlExpression`, +`SelectStatement(Builder)`, `TranspilerSqlGuard(Factory)`, `DeparserColumResolver`. + +--- + +## 2. jdbc.db-Repo-Konsumenten (publizierte `sql.*`-Snapshots) + +Nach dem Umzug sind `api`, `record`, `dialect.*` **nicht mehr im jdbc.db-Reactor**. Die verbleibenden +Module beziehen sie als externe Artefakte aus dem SQL-Repo. + +### 2.1 `impl` + +`impl/pom.xml` heute (compile): `…jdbc.db.api`, `…jdbc.db.record`, `…jdbc.db.dialect.api`; +(test): `…dialect.db.h2`, `…dialect.db.common`. → alle auf `…sql.*` (feste `0.0.1-SNAPSHOT`): + +```xml +org.eclipse.daanse + org.eclipse.daanse.sql.api0.0.1-SNAPSHOT +org.eclipse.daanse + org.eclipse.daanse.sql.record0.0.1-SNAPSHOT +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api0.0.1-SNAPSHOT + +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h20.0.1-SNAPSHOTtest +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common0.0.1-SNAPSHOTtest +``` + +Imports in `DatabaseServiceImpl`, `CachingDatabaseService` und den Tests entsprechend auf `sql.*`. + +### 2.2 `importer/csv` (ETL — bleibt) + +`importer/csv/pom.xml` heute (compile): `…jdbc.db.api`, `…jdbc.db.dialect.api`, `…jdbc.db.record`, +`…io.fs.watcher.api`, `de.siegmar:fastcsv`; (runtime): `…jdbc.db.impl`, `…io.fs.watcher.watchservice`. +→ `api`/`record`/`dialect.api` auf `sql.*`; `fastcsv`, `io.fs.watcher.*`, `impl` bleiben unverändert. +Imports in `CsvDataImporter` (nutzt `Dialect`, `DialectFactory`) auf `sql.dialect.api`. + +### 2.3 Reactor-Bereinigung + +`jdbc.db/pom.xml`: aus `` entfernen: `api`, `record`, `dialect`. Verbleibend: `impl`, +`importer`. (Die inaktiven Dialekte lagen ohnehin auskommentiert und wandern in einer späteren Welle; +bis dahin bleiben ihre Verzeichnisse in `jdbc.db/dialect/db/`.) + +--- + +## 3. Build-Vorbedingung + +`impl`/`importer` bauen erst grün, wenn die `sql.*`-Snapshots verfügbar sind → im SQL-Repo zuerst +`mvn -q install`, danach `jdbc.db` bauen. Siehe [08](08-verification.md). diff --git a/docs/dialect-migration/06-build-and-java21.md b/docs/dialect-migration/06-build-and-java21.md new file mode 100644 index 0000000..8541ef1 --- /dev/null +++ b/docs/dialect-migration/06-build-and-java21.md @@ -0,0 +1,118 @@ + + +# 06 – Build, Reactor & Java-21-Anhebung + +Zurück zum [Master-Plan](README.md). + +--- + +## 1. Java-Version anheben (17 → 21) + +Das SQL-Repo baut heute mit Java 17 (`sql/pom.xml`), der Dialekt-Code stammt aus einem Java-21-Repo. +Im SQL-Root-Pom: + +```xml + + ... + 21 + 21 + ${java.version} + ${java.version} + +``` + +Wirkung: gesamtes SQL-Repo kompiliert/testet auf 21. Toolchain/CI muss ein JDK 21 bereitstellen +(`.github/workflows/build_deploy.yml` prüfen; die `java_check_javadoc.yml`-Prüfung ebenfalls auf 21). + +--- + +## 2. Neue Aggregator-Poms + +**`sql/dialect/pom.xml`** (ersetzt `jdbc.db/dialect/pom.xml`): + +```xml + + org.eclipse.daanse + org.eclipse.daanse.sql + ${revision} + +org.eclipse.daanse.sql.dialect +pom + + api + db + +``` + +**`sql/dialect/db/pom.xml`** (packaging=pom, Parent `org.eclipse.daanse.sql.dialect`), ``: + +``` +common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite +``` + +**Root `sql/pom.xml`** `` erweitern: + +```xml + + api + record + dialect + guard + deparser + statement + +``` + +Reihenfolge in `` ist unkritisch (Maven sortiert nach Abhängigkeiten), Lesbarkeit halber +aber Fundament zuerst. + +--- + +## 3. dependencyManagement erweitern + +Aus den migrierten Modulen benötigte Versionen ins SQL-Root-`dependencyManagement` bzw. in die +jeweiligen Modul-Poms übernehmen (heute im SQL-Root nur slf4j/mockito/assertj vorhanden): + +- **Test-Treiber & Container** (Test-Scope, pro Dialekt): `mysql-connector-j`, `mariadb-java-client`, + `mssql-jdbc`, `ojdbc11`, `postgresql`, `org.xerial:sqlite-jdbc`, `org.duckdb:duckdb_jdbc`, + `com.h2database:h2`; `org.testcontainers` (mysql, mariadb, mssqlserver, oracle-xe, postgresql, + junit-jupiter). +- **JUnit 5** (`junit-jupiter-api/engine`) — Versionen aus den Quell-Poms (z. B. 5.12.2) + vereinheitlichen. +- **assertj** — SQL-Root hat 3.24.2, einige jdbc.db-Module nutzen 3.26.0 → auf eine Version + vereinheitlichen. +- **`biz.aQute.bnd:biz.aQute.bndlib`** — compile-Dep von `oracle`. + +Die genauen Versionen aus den jeweiligen `dialect/db//pom.xml` der Quelle übernehmen; im Zweifel +die höhere/neuere wählen und zentral im `dependencyManagement` pinnen. + +--- + +## 4. Parent-Pom & Snapshot-Repo (unverändert) + +- Parent bleibt `org.eclipse.daanse:org.eclipse.daanse.pom.parent:0.0.7` (liefert bnd-/flatten-Plugins). +- `${revision}` = `0.0.1-SNAPSHOT`, `.flattened-pom.xml`-Mechanik unverändert. +- Snapshot-Repo Sonatype `central.sonatype.com/repository/maven-snapshots` unverändert. +- OSGi: keine `bnd.bnd`-Dateien; DS-`@Component`-getrieben → `Bundle-SymbolicName` folgt der + ArtifactId automatisch. + +--- + +## 5. Build-Kommandos + +```bash +# SQL-Repo vollständig bauen + lokal installieren (Voraussetzung für jdbc.db) +cd org.eclipse.daanse.sql && mvn -q clean install + +# Einzelnes Modul inkl. Abhängigkeiten +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am verify + +# jdbc.db gegen die frisch installierten sql.*-Snapshots +cd ../org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` diff --git a/docs/dialect-migration/07-anomalies-and-risks.md b/docs/dialect-migration/07-anomalies-and-risks.md new file mode 100644 index 0000000..19ec6b5 --- /dev/null +++ b/docs/dialect-migration/07-anomalies-and-risks.md @@ -0,0 +1,90 @@ + + +# 07 – Anomalien & Risiken + +Zurück zum [Master-Plan](README.md). + +--- + +## 1. redshift/snowflake-Vertauschung (⚠ Datenintegrität) + +Die beiden Maven-Module `dialect/db/redshift` und `dialect/db/snowflake` sind **gekreuzt** befüllt +(durch Lesen der `package`-Deklarationen verifiziert): + +| Modulverzeichnis / ArtifactId | Enthaltenes Paket + Klasse | +|---|---| +| `dialect/db/redshift` (`…dialect.db.redshift`) | `…dialect.db.snowflake` · `SnowflakeDialect` / `SnowflakeDialectFactory` | +| `dialect/db/snowflake` (`…dialect.db.snowflake`) | `…dialect.db.redshift` · `RedshiftDialect` / `RedshiftDialectFactory` | + +- Beide Module sind **inaktiv** und **nicht** Teil der jetzigen aktiven Untermenge → aktuell **kein** + Handlungsbedarf. +- **Beim späteren Nachziehen zwingend korrigieren:** entweder Verzeichnis/ArtifactId oder + Paket/Klasse angleichen, sodass Snowflake-Code im `snowflake`-Modul und Redshift-Code im + `redshift`-Modul liegt. Ein rein mechanischer Umzug nach Verzeichnisname würde die + Fehlbenennung zementieren. +- Zusatzkante beachten: das `snowflake`-Modul (mit Redshift-Code) hängt an `postgresql`. + +--- + +## 2. Java 17 → 21 + +- Der Dialekt-Code ist für Java 21 geschrieben; das SQL-Repo wird angehoben (siehe + [06](06-build-and-java21.md)). Risiko gering, aber CI/Toolchain muss JDK 21 bereitstellen. +- Alle bestehenden SQL-Module (`guard`/`deparser`/`statement`) werden dadurch ebenfalls auf 21 + gebaut — kein bekanntes Hindernis, aber Regressionstest ist Teil der Abnahme. + +--- + +## 3. Cross-Repo-Test-Abhängigkeit `dialect.db. → jdbc.db.impl` + +Round-Trip-Tests einiger Dialekte (mysql, mariadb, mssqlserver, oracle, postgresql, derby) nutzen +`DatabaseServiceImpl` aus `impl`. Da `impl` in `jdbc.db` bleibt, darf diese Test-Kante **nicht** als +`sql → jdbc.db`-Dependency bestehen bleiben (würde den aufgelösten Zyklus reaktivieren). Optionen +(pro Dialekt in [08](08-verification.md) festhalten): + +1. **Umschreiben** auf SQL-repo-eigene Test-Fixtures (bevorzugt, wo mit vertretbarem Aufwand möglich). +2. **Temporär `@Disabled`** und die Integrationstests in `jdbc.db` (bei `impl`) weiterführen. + +--- + +## 4. Inaktive Dialekte (spätere Welle) + +~27 Module + `configurable` liegen auskommentiert in `jdbc.db/dialect/db/`. Sie bleiben vorerst dort. +Risiken beim Nachziehen: die redshift/snowflake-Vertauschung (§1) und Inter-Dialekt-Ketten +(`greenplum/netezza → postgresql`, `impala → hive`, `vectorwise → ingres`, `sqlstream → luciddb`, +`infobright → mysql`) — Reihenfolge beachten. + +--- + +## 5. Historie-Erhaltung (optional) + +`git mv` erhält die Historie innerhalb eines Repos. Für **volle** Datei-Historie über die +Repo-Grenze (jdbc.db → sql) wäre `git filter-repo` mit anschließendem Merge nötig — deutlich +aufwändiger. Empfehlung: `git mv` genügt; die Herkunft ist über diesen Dokumentensatz nachvollziehbar. + +--- + +## 6. `api.meta`-Benennung + +`api.meta` (JDBC-`DatabaseMetaData`-Introspektion) ist semantisch JDBC-nah, wird aber der Konsistenz +halber nach `sql.api.meta` umbenannt. Falls später eine sauberere Trennung „reines SQL-Modell“ vs. +„JDBC-Introspektion“ gewünscht ist, kann `api.meta` in einem Folgeschritt separiert werden — **nicht** +Teil dieser Migration. + +--- + +## 7. Risiko-Matrix (Kurzfassung) + +| Risiko | Eintritt | Wirkung | Gegenmaßnahme | +|---|---|---|---| +| redshift/snowflake falsch benannt | später | falsche Dialektauswahl zur Laufzeit | §1, beim Nachziehen korrigieren | +| Zyklus über Test-Kante reaktiviert | mittel | Build/Release blockiert | §3, Tests umschreiben/disablen | +| JDK-21-Toolchain fehlt in CI | gering | Build rot | CI-Workflow auf 21 stellen | +| Versions-Divergenz (assertj/junit) | gering | Build-Warnungen/-Fehler | zentral pinnen ([06](06-build-and-java21.md)) | +| Release-Reihenfolge vertauscht | mittel | jdbc.db baut nicht | `sql` zuerst installieren/publizieren | diff --git a/docs/dialect-migration/08-verification.md b/docs/dialect-migration/08-verification.md new file mode 100644 index 0000000..27efe9f --- /dev/null +++ b/docs/dialect-migration/08-verification.md @@ -0,0 +1,87 @@ + + +# 08 – Verifikation & Abnahme + +Zurück zum [Master-Plan](README.md). + +--- + +## 1. Build-Reihenfolge + +```bash +# 1) SQL-Repo bauen, testen, lokal installieren +cd org.eclipse.daanse.sql +mvn -q clean install + +# 2) jdbc.db gegen die frisch installierten sql.*-Snapshots +cd ../org.eclipse.daanse.jdbc.db +mvn -q clean verify +``` + +`sql` **muss** vor `jdbc.db` installiert sein (einseitige Abhängigkeit, siehe +[02](02-dependency-and-layering.md)). + +--- + +## 2. Testumfang + +- **SQL-Repo:** + - `statement/demo`-Integrationstests (H2 in-memory + MSSQL via Testcontainers) laufen grün. + - `guard/jsqltranspiler`-Tests (nutzen `sql.dialect.db.h2/common`) grün. + - `deparser`-Tests grün. + - Migrierte Dialekt-Unit-Tests (Generator-/Quoter-Tests) grün. + - Round-Trip-Tests mit Cross-Repo-`impl`-Abhängigkeit: pro Dialekt Status dokumentieren + (umgeschrieben | `@Disabled`, siehe [07](07-anomalies-and-risks.md) §3). +- **jdbc.db-Repo:** + - `impl`-Tests (DDL-Round-Trip) grün gegen `sql.dialect.db.h2/common`. + - `importer/csv`-ETL-Tests (`CsvDataLoaderTest`) grün. + +--- + +## 3. Strukturelle Kontrollen + +```bash +# Keine alten Dialekt-/api-Pakete mehr im SQL-Repo: +grep -rl "org.eclipse.daanse.jdbc.db" org.eclipse.daanse.sql/ --include=*.java # → leer + +# SQL-Repo referenziert keine externen jdbc.db.dialect-Artefakte mehr: +grep -rn "jdbc.db.dialect" org.eclipse.daanse.sql/ --include=pom.xml # → leer + +# jdbc.db enthält keine api/record/dialect-Module mehr im Reactor: +grep -nE "(api|record|dialect)" org.eclipse.daanse.jdbc.db/pom.xml # → leer +``` + +--- + +## 4. Definition of Done — Dokumentensatz (dieser Ordner) + +- [ ] `README.md` + `01`–`08` vorhanden, gegenseitig verlinkt, konsistent. +- [ ] Split-Tabelle deckt jedes Modul beider Repos genau einmal ab. +- [ ] Rename-Mapping vollständig (jedes mitgezogene Paket + ArtifactId hat ein Ziel). +- [ ] Migrationsreihenfolge respektiert den Abhängigkeitsgraph. +- [ ] Zirkularitätsauflösung nachvollziehbar begründet. +- [ ] Anomalien (redshift/snowflake, Java 21, Cross-Repo-Tests) erfasst. + +## 5. Definition of Done — Code-Umsetzung (spätere Phase) + +- [ ] `mvn clean install` im SQL-Repo grün (Java 21). +- [ ] SQL-Repo hat **keine** externe `jdbc.db.*`-Abhängigkeit mehr. +- [ ] `mvn clean verify` im jdbc.db-Repo grün gegen publizierte `sql.*`-Snapshots. +- [ ] `statement/demo`-Integrationstests (H2/MSSQL) laufen. +- [ ] Lizenz-Header- und Javadoc-CI-Checks in beiden Repos grün. +- [ ] Aktive Dialekt-Untermenge (10 Module) vollständig unter `sql/dialect/db/` gebaut. +- [ ] redshift/snowflake-Anomalie für die spätere Welle dokumentiert (nicht jetzt berührt). + +--- + +## 6. Rollback + +Da der Umzug in Arbeitszweigen beider Repos erfolgt und `git mv` genutzt wird, ist ein Rollback durch +Verwerfen der Zweige möglich. Keine der Änderungen wird gemergt, bevor beide Repos grün bauen und die +DoD erfüllt ist. diff --git a/docs/dialect-migration/09-minimal-split-analysis.md b/docs/dialect-migration/09-minimal-split-analysis.md new file mode 100644 index 0000000..f5943fe --- /dev/null +++ b/docs/dialect-migration/09-minimal-split-analysis.md @@ -0,0 +1,214 @@ + + +# 09 – Minimaler Schnitt: was `sql` wirklich aus `jdbc.db` braucht + +Zurück zum [Master-Plan](README.md). + +> 🌐 Sprache: **Deutsch** (diese Datei) · [Русский](ru/09-minimal-split-analysis.md) + +**Zweck.** Dies ist eine *zweite* Analyse, ergänzend zu Plan 1 (Wholesale-Umzug von `api`+`record`, +siehe [README](README.md) und [02](02-dependency-and-layering.md)). Sie beantwortet die Frage: *Wie +lässt sich der **minimale** Teil herauslösen, den `sql` aus `jdbc.db` braucht — wo die Dialekte ja +mitgebraucht werden?* Ausgelöst durch die Idee, Dialekte **statisch pro Version** fest einzubauen, +sodass kein JDBC-`record` zum Setzen der Eigenschaften nötig ist. + +Dieses Dokument **legt sich bewusst nicht** auf einen Weg fest. Es stellt die Fakten und vier +Optionen neutral mit Aufwand/Risiko dar. + +--- + +## 1. Ausgangsfrage + +Plan 1 verschiebt `api` + `record` **komplett** nach `sql`. Das ist mechanisch einfach, macht `sql` +aber zum Eigentümer des gesamten Metadaten-/Introspektionsmodells. Die Frage hier: geht es **kleiner** +— nur der Teil, den die Dialekte und der SQL-Eigencode tatsächlich benötigen? + +--- + +## 2. Was `sql` selbst braucht (sehr wenig) + +Der SQL-**Eigencode** (`guard`, `deparser`, `statement`) berührt aus `jdbc.db.api` nur **7 leichte +Blatt-Typen** — **kein** `record`, **kein** `api.meta`, keine Introspektion: + +| Paket | Typen | +|---|---| +| `api.type` | `Datatype`, `BestFitColumnType` | +| `api.sql` | `BitOperation`, `OrderedColumn` | +| `api.schema` (References) | `SchemaReference`, `TableReference`, `ColumnReference` | + +Diese kommen heute sogar nur **transitiv** über `dialect.api` herein (kein SQL-Pom deklariert +`jdbc.db.api` direkt). Sie sind eigenständig und geschlossen (`BestFitColumnType`/`Datatype` sind +reine Enums; `TableReference → SchemaReference → CatalogReference → Named`). + +**Das Problem ist nicht der SQL-Eigencode — es sind die Dialekte selbst.** + +--- + +## 3. Was die Dialekte mitziehen — die zwei Rollen + +Ein konkreter Dialekt vereint heute **zwei unabhängige Verantwortlichkeiten**: + +### Rolle A — Nativer Metadaten-Provider (~85–90 % der Kopplung) + +`Dialect extends … MetadataProvider`. Die 6 „großen" Dialekte überschreiben je 14–21 +`MetadataProvider`-Methoden (`getAllIndexInfo`, `getAllPrimaryKeys`, `getAllImportedKeys`, +`getAllTriggers`, `getAllSequences`, `getUniqueConstraints`, …), die **`information_schema` / +Katalogtabellen gegen eine Live-`Connection`** abfragen und **`record.schema`-Objekte als Rückgabe +konstruieren**. + +- `record.schema.*` und `api.meta.IndexInfo`/`IndexInfoItem` werden **ausschließlich** hierfür genutzt. +- Das ist **Introspektion** (Metadaten *lesen*), nicht SQL-Generierung. + +| Dialekt | MetadataProvider-Overrides | `record.schema`-Konstruktionen | +|---|---|---| +| oracle | 21 | 19 | +| mssqlserver | 19 | 16 | +| postgresql | 19 | 16 | +| mysql | 15 | 11 | +| mariadb | 15 | 12 | +| h2 | 14 | 11 | +| **duckdb / sqlite / common** | **0** | **0** | + +### Rolle B — DDL-Konsument (~10–15 % der Kopplung) + +Die `DdlGenerator`-API nimmt `api.schema`-Typen als **Eingabe-Vokabular**: + +```java +default String createTable(TableReference table, List columns, + PrimaryKey primaryKey, boolean ifNotExists) { … } +default String alterTableAddColumn(TableReference table, ColumnDefinition column) { … } +default String createTrigger(String name, Trigger.TriggerTiming timing, + Trigger.TriggerEvent event, TableReference table, …) { … } +``` + +- Der Großteil liegt als `default`-Methoden **in der geteilten API** (`DdlGenerator`), nicht in den + Dialekten. Nur mssqlserver/oracle/mysql überschreiben einige mit `api.schema`-Eingaben. +- Berührt **nie** `record` oder `api.meta` — nur `api.schema` als Parameter. + +### Rolle C — Konfiguration/Capabilities: **keine** Metadaten-Kopplung + +Weder `record` noch `api.meta` noch die schweren `api.schema`-Definitionstypen werden für +Konfiguration/Fähigkeiten genutzt (siehe §5). + +--- + +## 4. Der `sealed`-Blocker und die geschlossene `api.schema`-Insel + +Warum lässt sich `api.schema` nicht auf „ein paar References" verkleinern: + +- `SchemaObject` ist `sealed … permits TableDefinition, ViewDefinition, MaterializedView, Sequence, + Function, Procedure, Trigger, UserDefinedType` (8). +- `Constraint` ist `sealed … permits PrimaryKey, ImportedKey, UniqueConstraint, CheckConstraint` (4). + +Sobald ein Generator `Trigger`, `TableDefinition` oder `PrimaryKey` anfasst (Rolle B tut das), zwingt +die versiegelte Hülle **das gesamte `api.schema`-Paket** (39 Typen), gemeinsam zu kompilieren. + +**Gute Nachricht:** `api.schema` importiert **kein** anderes api-Paket (nicht `meta`, nicht `sql`, +nicht `type`). Es ist eine **geschlossene Insel** — schwer, aber als Ganzes sauber verschiebbar, +ohne die Introspektion (`api.meta`, top-level) mitzuziehen. + +--- + +## 5. Konfiguration/Versionierung ist bereits entkoppelt + +Die Idee „fest pro Version einbauen" trifft auf ein System, das **schon** so gebaut ist: + +- **`DialectInitData`** ist ein schlankes `record` (nur Primitive: `productVersion`, Major/Minor, + Quote-Zeichen, Keywords, ResultSet-Styles). Es hängt **nicht** an `api.schema`/`record`. Es gibt + `ansiDefaults()` (rein statisch, ohne JDBC) und Wither (`withVersion(maj,min)`, …). +- **Capabilities** sind statische Boolean-Records mit Presets (`DdlCapabilities.full()/.minimal()`, + `AggregateCapabilities.none()`, …). Nie aus Live-Metadaten zur Query-Zeit abgeleitet. +- **Versions-Verzweigung** ist bereits statisch — zwei etablierte Muster: + - **Subklasse pro Variante:** `Db2OldAs400Dialect extends Db2Dialect` (überschreibt + `allowsFromQuery()→false`), eigener `@DialectName("DB2_OLD_AS400")`. + - **In-Methode:** `dialectVersion.isUnknownOrAtLeast(8,0)` (mysql/oracle/postgresql/mssql/mariadb/sqlite). + +Damit ist die **SQL-Generierung schon vollständig von Live-Metadaten entkoppelt** — sie liest nur +Konstanten + einen `int` `dialectVersion`, den man ohne Connection setzen kann. Die einzige +Metadaten-Kopplung ist Rolle A (das *Ausliefern* von Metadaten). + +--- + +## 6. Optionen (neutral, mit Aufwand/Risiko) + +### O1 — Rollen-Split: MetadataProvider-Rolle herauslösen + +`Dialect extends MetadataProvider` auflösen; die je ~7–21 Lademethoden der 6 großen Dialekte in +Begleiter-Klassen (`MetadataProvider implements MetadataProvider`) verschieben, die in `jdbc.db` +bei der Introspektion bleiben (OSGi-verdrahtet). Der SQL-seitige Dialekt behält alles andere. + +- **Ergebnis:** Dialekte importieren **kein** `record`/`api.meta` mehr. SQL-Kern = Dialekt-SQL-Gen + + `api.schema` (Insel) + `api.type` + `api.sql`. `record`, `api.meta`, Introspektion, `impl`, + `importer` bleiben in `jdbc.db`. +- **Aufwand:** mittel — 6 Dialekte anfassen; `Dialect`-SPI-Bruch (die `getAll*`-Defaults wandern in + die separate `MetadataProvider`-SPI). duckdb/sqlite/common unverändert. +- **Risiko:** OSGi-Verdrahtung Dialekt ↔ MetadataProvider muss neu gefügt werden; Konsumenten von + `dialect.getAllIndexInfo(...)` müssen auf den separaten Provider umschwenken. + +### O2 — Rollen-Split + feste Pro-Version-Subklassen + +Wie O1, zusätzlich die `dialectVersion`-Verzweigung durch **feste Pro-Version-Klassen** ersetzen +(Muster `Db2OldAs400Dialect`), Eigenschaften vollständig einkompiliert — **kein** +`DialectInitData`-Snapshot mehr nötig. + +- **Ergebnis:** maximale Entkopplung; Dialekt komplett statisch, ohne jede Laufzeit-Metadaten. +- **Aufwand:** hoch — pro DB mehrere Versionsklassen; Auswahl-/Factory-Logik nach Version. +- **Risiko:** Klassen-Explosion; Versions-Auswahl muss robust sein (Produktname/Version → Klasse). + +### O3 — Closure-Schnitt ohne Refactor + +Kein API-Bruch. Dialekte + ihre transitive Hülle nach SQL: `dialect.*`, `api.type`, `api.sql`, das +ganze `api.schema`, `api.meta.IndexInfo(+Item)`, `MetadataProvider`, `record.schema`. In `jdbc.db` +bleibt der reine Introspektions-Motor (`DatabaseService`, `MetaDataQueries`, `SnapshotBuilder`, +`meta` minus IndexInfo, `record.meta`, `impl`, `importer`). + +- **Ergebnis:** kleiner als Wholesale (der Motor bleibt), aber `record.schema` + `MetadataProvider` + wandern mit. Einweg `jdbc.db → sql`, kein Zyklus, keine API-Änderung. +- **Aufwand:** mittel — `api`/`record` müssen **gesplittet** werden (schema/type/sql/IndexInfo → SQL; + Rest bleibt). +- **Risiko:** Paket `api`/`record` über zwei Repos geteilt; Split-Grenze sauber ziehen. + +### O4 — Wholesale (= Plan 1) + +`api` + `record` komplett nach SQL. Siehe [README](README.md)/[02](02-dependency-and-layering.md). + +- **Ergebnis:** einfachste Mechanik; SQL besitzt das gesamte Metadaten-/Introspektionsmodell. +- **Aufwand:** gering–mittel; **Risiko:** gering; aber **nicht** „minimal". + +--- + +## 7. Vergleich + +| | O1 Rollen-Split | O2 + Versionsklassen | O3 Closure-Schnitt | O4 Wholesale | +|---|---|---|---|---| +| SQL bekommt `record` | nein | nein | `record.schema` | ganz | +| SQL bekommt `api.meta` | nein | nein | nur IndexInfo | ganz | +| SQL bekommt `api.schema` | ganz (Insel) | ganz (Insel) | ganz (Insel) | ganz | +| `Dialect extends MetadataProvider` | **aufgelöst** | aufgelöst | bleibt | bleibt | +| API-Bruch | ja (SPI) | ja (SPI) | nein | nein | +| Betroffene Dialekte | 6 | 6 + Versionsklassen | 0 (nur Deps) | 0 | +| `api`/`record` gesplittet | ja | ja | ja | nein | +| SQL-Kerngröße | **minimal** | **minimal** | mittel | groß | +| Aufwand | mittel | hoch | mittel | gering–mittel | + +**Gemeinsam allen Optionen:** `api.type` + `api.sql` gehen immer nach SQL (reine Blätter, nur von +Dialekten genutzt); `impl` + `importer/csv` (ETL) bleiben immer in `jdbc.db`; Ergebnis ist stets +Einweg `jdbc.db → sql` ohne Zyklus. + +--- + +## 8. Bezug zu Plan 1 + +Plan 1 ([README](README.md)) entspricht **O4**. Diese Analyse zeigt, dass **O1/O2** einen echt +minimalen SQL-Kern liefern (der Idee „fest pro Version, kein JDBC-record" folgend), zum Preis eines +`Dialect`-SPI-Bruchs und Anfassens der 6 großen Dialekte; **O3** ist der Mittelweg ohne API-Bruch. +Die Wahl ist bewusst offen gelassen. + +> **Vollständiger Plan für O1/O2:** siehe [`minimal-split/`](minimal-split/README.md) — der +> ausführbare Schritt-für-Schritt-Plan für den Rollen-Split (Rolle A aus den Dialekten herauslösen). diff --git a/docs/dialect-migration/README.md b/docs/dialect-migration/README.md new file mode 100644 index 0000000..1f30408 --- /dev/null +++ b/docs/dialect-migration/README.md @@ -0,0 +1,144 @@ + + +# Migration: Dialekte + Dialekt-API von `jdbc.db` nach `sql` + +> 🌐 Sprache: **Deutsch** (diese Datei) · [Русский](ru/README.md) + +**Status:** Plan / Spezifikation (Code-Umzug noch nicht ausgeführt) +**Betroffene Repos:** `org.eclipse.daanse.sql` (Ziel) · `org.eclipse.daanse.jdbc.db` (Quelle) + +Dieser Ordner ist der **Master-Plan** für die Verlagerung des Datenbank-Dialekt-Stacks aus dem +Repository `org.eclipse.daanse.jdbc.db` in das Repository `org.eclipse.daanse.sql`. Er beschreibt +*was* umzieht, *warum*, *in welcher Reihenfolge* und *wie* — als ausführbare Arbeitsanweisung für +die spätere Umsetzung. + +--- + +## 1. Motivation + +Das SQL-Repo (`guard`, `deparser`, `statement`) ist bereits **dialekt-bewusst**, definiert die +Dialekt-Abstraktion aber nicht selbst. Es konsumiert sie heute als **externe Maven-Artefakte** aus +`org.eclipse.daanse.jdbc.db`: + +- `org.eclipse.daanse.jdbc.db.dialect.api` (`Dialect`, `IdentifierQuotingPolicy`, `generator.*`) +- `org.eclipse.daanse.jdbc.db.dialect.db.common` (`AnsiDialect`) +- `org.eclipse.daanse.jdbc.db.dialect.db.h2` / `.mysql` / `.mssqlserver` +- das Metadaten-/Typ-Modell `…jdbc.db.api.schema.*` und `…api.type.BestFitColumnType` + +**Ziel:** Das SQL-Repo soll den **kompletten SQL-Generierungs- und Dialekt-Stack besitzen**. Der +Dialekt-Code, die Dialekt-API und das von ihnen benötigte Metadaten-/Typ-Modell (`api` + `record`) +ziehen nach `sql` um. `jdbc.db` behält nur die reine **Metadaten-Introspektions-Engine** (`impl`) +und die **ETL-Schicht** (`importer/csv`) und wird zum **einseitigen Downstream-Konsumenten** von +`sql`. + +**Randbedingung:** Rein ETL-basierte Bestandteile werden *nicht* nach `sql` übernommen. + +--- + +## 2. Getroffene Grundsatzentscheidungen + +| Thema | Entscheidung | Detaildokument | +|---|---|---| +| Paket-Namen | Alle mitgezogenen Module werden auf `org.eclipse.daanse.sql.*` umbenannt | [03](03-package-rename-map.md) | +| Geteilte API-Grenze | `api` + `record` ziehen **mit** um → Einweg-Schichtung, kein Repo-Zyklus | [02](02-dependency-and-layering.md) | +| Dialekt-Umfang | Nur die im Reactor **aktive** Untermenge (10 Module) | [01](01-inventory-and-scope.md) | +| Java-Version | SQL-Repo wird von Java **17 → 21** angehoben | [06](06-build-and-java21.md) | + +--- + +## 3. Split-Überblick + +### Zieht nach `org.eclipse.daanse.sql` um (+ Paket-Rename) + +| Quelle in `jdbc.db` | Ziel-Artefakt in `sql` | Rolle | +|---|---|---| +| `api` | `org.eclipse.daanse.sql.api` | Metadaten-/Schema-/Typ-/SQL-Modell | +| `record` | `org.eclipse.daanse.sql.record` | Record-Implementierungen dazu | +| `dialect/api` | `org.eclipse.daanse.sql.dialect.api` | Dialekt-API (`Dialect`, `DialectFactory`, `generator`, `capability`, `type`) | +| `dialect/db/common` | `org.eclipse.daanse.sql.dialect.db.common` | `AbstractJdbcDialect`, `AnsiDialect`, Jdbc*-Generatoren/Quoter | +| `dialect/db/test-support` | `org.eclipse.daanse.sql.dialect.db.test-support` | Test-Helfer | +| `dialect/db/{duckdb,h2,mariadb,mssqlserver,mysql,oracle,postgresql,sqlite}` | `org.eclipse.daanse.sql.dialect.db.` | 8 aktive konkrete Dialekte | + +### Bleibt in `org.eclipse.daanse.jdbc.db` (wird Konsument von `sql`) + +| Modul | Rolle | Neue Abhängigkeit | +|---|---|---| +| `impl` | Metadaten-Introspektions-Engine (`DatabaseServiceImpl`, `CachingDatabaseService`) | compile→ `sql.api`, `sql.record`, `sql.dialect.api`; test→ `sql.dialect.db.common/h2` | +| `importer` + `importer/csv` | **ETL** (CSV-Import) | compile→ `sql.api`, `sql.record`, `sql.dialect.api`; runtime→ `impl` | +| `dialect/db/*` (inaktiv, ~27) | vorerst geparkt | spätere Migrationswelle | + +--- + +## 4. Ziel-Reactor-Struktur im SQL-Repo + +``` +org.eclipse.daanse.sql/ +├── pom.xml # Root-Aggregator: um api, record, dialect erweitern; Java 21 +├── api/ # NEU (leaf) +├── record/ # NEU (leaf) +├── dialect/ # NEU (Aggregator, packaging=pom) +│ ├── api/ # leaf +│ └── db/ # Aggregator, packaging=pom +│ ├── common/ test-support/ +│ ├── duckdb/ h2/ mariadb/ mssqlserver/ +│ └── mysql/ oracle/ postgresql/ sqlite/ +├── deparser/ # bestehend – Dialekt-Deps von extern → reaktor-intern +├── guard/ # bestehend – dito +└── statement/ # bestehend – dito +``` + +--- + +## 5. Migrationswellen (Reihenfolge) + +1. **Welle A – Fundament:** `api` → `record` nach `sql` (Rename, Reactor-Eintrag). +2. **Welle B – Dialekt-Kern:** `dialect/api` → `dialect/db/common` → `test-support`. +3. **Welle C – Konkrete Dialekte:** die 8 aktiven DB-Module (`mysql` vor `mariadb`). +4. **Welle D – SQL-Konsumenten:** `guard`/`deparser`/`statement` von externen Deps auf + reaktor-interne Module umstellen; Java-21-Anhebung. +5. **Welle E – jdbc.db-Rückbau:** `impl`/`importer` auf publizierte `sql.*`-Snapshots umstellen, + umgezogene Module aus dem `jdbc.db`-Reactor entfernen. + +Jede Welle ist für sich baubar (`mvn -q verify`), bevor die nächste beginnt. + +--- + +## 6. Detaildokumente + +| # | Dokument | Inhalt | +|---|---|---| +| 01 | [Inventar & Umfang](01-inventory-and-scope.md) | Vollständige Modulliste, Klassifizierung, aktiv/inaktiv | +| 02 | [Abhängigkeit & Schichtung](02-dependency-and-layering.md) | Graph, Zirkularität + Auflösung, Ziel-Layering | +| 03 | [Paket- & Artefakt-Rename](03-package-rename-map.md) | Vollständiges Old→New-Mapping | +| 04 | [Migrationsprozedur](04-migration-procedure.md) | Schritt-für-Schritt, modulweise | +| 05 | [Konsumenten umverdrahten](05-consumer-rewiring.md) | SQL- und jdbc.db-seitige Pom-/Import-Änderungen | +| 06 | [Build & Java 21](06-build-and-java21.md) | Reactor-Poms, Versionen, Java-Anhebung | +| 07 | [Anomalien & Risiken](07-anomalies-and-risks.md) | redshift/snowflake-Swap, Java, spätere Wellen | +| 08 | [Verifikation & Abnahme](08-verification.md) | Build-Befehle, Testumfang, DoD | +| 09 | [Minimaler Schnitt (2. Analyse)](09-minimal-split-analysis.md) | Alternative zum Wholesale: was `sql` wirklich braucht, Rollen-Split, 4 Optionen | +| — | [**Minimal-Split — vollständiger Plan**](minimal-split/README.md) | Ausführbarer Plan für den Rollen-Split (O1/O2) als Alternative zu 01–08 | + +> **Hinweis:** Die Dokumente 01–08 beschreiben den **Wholesale-Umzug** (Plan 1). Dokument **09** ist +> eine ergänzende *zweite Analyse* zum **minimalen** Schnitt; der Ordner +> [`minimal-split/`](minimal-split/README.md) enthält dazu den **vollständigen ausführbaren Plan** +> (Rollen-Split). Die beiden Pläne sind Alternativen — nicht beide umsetzen. + +--- + +## 7. Definition of Done (Dokumentensatz) + +- [ ] Alle 9 Dokumente vorhanden, untereinander verlinkt, konsistent. +- [ ] Split-Tabelle deckt **jedes** Modul beider Repos genau einmal ab. +- [ ] Rename-Mapping vollständig (jedes mitgezogene Paket + ArtifactId hat ein Ziel). +- [ ] Migrationsreihenfolge respektiert den Abhängigkeitsgraph (keine Vorwärtsverweise). +- [ ] Auflösung der Zirkularität nachvollziehbar begründet. + +**DoD der späteren Code-Umsetzung:** `mvn -q verify` grün in beiden Repos; SQL-Repo baut ohne +externe `jdbc.db.dialect.*`-Deps; `jdbc.db` baut gegen publizierte `sql.*`-Snapshots; +`statement/demo`-Integrationstests (H2/MSSQL) laufen. diff --git a/docs/dialect-migration/minimal-split/01-role-separation.md b/docs/dialect-migration/minimal-split/01-role-separation.md new file mode 100644 index 0000000..3d97f52 --- /dev/null +++ b/docs/dialect-migration/minimal-split/01-role-separation.md @@ -0,0 +1,132 @@ + + +# 01 – Rollen-Trennung: `Dialect` ↔ `MetadataProvider` + +Zurück zum [Minimal-Split-Plan](README.md). + +Dies ist der **Kern-Refactor**. Er läuft vollständig **innerhalb `jdbc.db`** und ist dort grün baubar, +bevor irgendetwas ins SQL-Repo umzieht. Erst wenn diese Entkopplung steht, wird der Dialekt schlank +genug für den minimalen Umzug. + +--- + +## 1. Ausgangslage (belegt) + +- `dialect/api/.../Dialect.java`: `public interface Dialect extends IdentifierQuoter, LiteralQuoter, + DialectCapabilitiesProvider, TypeMapper, MetadataProvider`. Über `MetadataProvider` (aus `api`) *ist* + jeder Dialekt ein Metadaten-Provider. +- `MetadataProvider` (`api/.../MetadataProvider.java`): ~40 `default`-Methoden (Rückgabe + `Optional.empty()`/`List.of()`); importiert ~25 `api.schema.*` + `api.meta.IndexInfo`/`TypeInfo`. +- Die 6 großen Dialekte (h2, mariadb, mssqlserver, mysql, oracle, postgresql) überschreiben je 14–21 + dieser Methoden mit echten `information_schema`-Abfragen und bauen `record.schema`-Objekte. +- `duckdb`, `sqlite`, `common`, alle Factories: **keine** Overrides (erben die leeren Defaults). +- **Aufrufer sind bereits provider-parametrisiert:** `DatabaseServiceImpl` nimmt `MetadataProvider` + als **Argument** und ruft darauf `getAllIndexInfo/getAllPrimaryKeys/...` auf — es referenziert + `Dialect` **nicht**. Beispiele: + ```java + public MetaInfo createMetaInfo(Connection connection, MetadataProvider metadataProvider) { … } + Optional> providerIndexes = provider.getAllIndexInfo(connection, null, null); + Optional> providerPKs = provider.getAllPrimaryKeys(connection, null, null); + ``` + +**Folgerung:** Motor und Dialekt sind faktisch schon getrennt; nur die Vererbung verschweißt sie. + +--- + +## 2. Zielbild + +``` +Dialect (SQL-Generierung) MetadataProvider (Introspektion) + IdentifierQuoter getAllIndexInfo(...) + LiteralQuoter getAllPrimaryKeys(...) + DialectCapabilitiesProvider getAllImportedKeys(...) + TypeMapper ... + (KEIN extends MetadataProvider) ▲ + │ implementiert + MetadataProvider (neu, bleibt in jdbc.db) +``` + +- `Dialect` verliert `extends MetadataProvider` und alle geerbten `getAll*`-Defaults. +- Pro großem Dialekt entsteht `MetadataProvider implements MetadataProvider` mit den extrahierten + Methoden (unverändertes Verhalten). Diese Klassen bleiben bei der Introspektion in `jdbc.db`. +- `MetadataProvider.EMPTY` bleibt der Default für Dialekte ohne native Metadaten (duckdb/sqlite/…). + +--- + +## 3. Schritte (innerhalb `jdbc.db`) + +### 3.1 Begleiter-Klassen extrahieren (6×) + +Für jeden von `h2, mariadb, mssqlserver, mysql, oracle, postgresql`: + +1. Neue Klasse `dialect/metadata//.../MetadataProvider.java`, + `implements org.eclipse.daanse.jdbc.db.api.MetadataProvider`. +2. Die überschriebenen `getAll*`/`get*`-Methoden **aus der Dialektklasse dorthin verschieben** + (mitsamt privaten Helfern wie `readImportedKey`, `PkBuilder`, `resolveSchema`, `mapMySqlIndexType`). +3. Der Dialektklasse bleiben nur SQL-Generierung, Quoting, Capabilities, TypeMapper. +4. Imports bereinigen: die Dialektklasse importiert danach **kein** `record.*` und **kein** + `api.meta.*` mehr (Kontrolle: `grep` muss leer sein). Die `MetadataProvider`-Klasse trägt + diese Importe jetzt. + +> Verbleibende `api.schema`-Importe in der Dialektklasse (Rolle B, nur mssqlserver/oracle/mysql) sind +> **beabsichtigt** — sie sind DDL-Eingabe-Vokabular und wandern später als geschlossene Insel mit +> nach SQL. + +### 3.2 Vererbung auflösen + +`dialect/api/.../Dialect.java`: `MetadataProvider` aus der `extends`-Liste entfernen; Import +`org.eclipse.daanse.jdbc.db.api.MetadataProvider` streichen. `BestFitColumnType getType(...)` bleibt +(nutzt `api.type`, kein Metadaten-Modell). + +### 3.3 Bereitstellung/Verdrahtung der Begleiter (OSGi) + +Heute registriert jede Factory `@Component(service = DialectFactory.class, …)`; `DatabaseServiceImpl` +ist `@Component(service = DatabaseService.class)`. Nach dem Split: + +- `MetadataProvider` als eigene DS-Komponente veröffentlichen, getaggt mit `@DialectName("MYSQL")` + (analog zu den Factories), **oder** über eine `MetadataProviderFactory` parallel zur + `DialectFactory` bereitstellen. +- Aufrufer, die bisher einen `Dialect` als `MetadataProvider` durchreichten, holen jetzt den passenden + `MetadataProvider` (per `@DialectName`/Produktname), Fallback `MetadataProvider.EMPTY`. +- Details der Verdrahtung in [04](04-consumer-rewiring.md). + +### 3.4 Aufrufer umstellen + +- `DatabaseServiceImpl`: unverändert in der Signatur (`MetadataProvider`-Parameter). Nur die + **Bereitstellung** des Providers am Aufrufort ändert sich (nicht mehr „der Dialekt", sondern der + Begleiter). +- Tests, die bisher `service.createMetaInfo(conn, dialect)` aufriefen, rufen künftig + `service.createMetaInfo(conn, new MySqlMetadataProvider())` (o. ä.) auf. + +--- + +## 4. Aufwand & Umfang + +| Dialekt | zu extrahierende Methoden (ca.) | Begleiter nötig | +|---|---|---| +| oracle | 21 | ja | +| mssqlserver | 19 | ja | +| postgresql | 19 | ja | +| mariadb | 15 | ja | +| mysql | 15 | ja | +| h2 | 14 | ja | +| duckdb, sqlite | 0 | nein (EMPTY) | +| common, test-support | 0 | nein | + +Reiner Verschiebe-Refactor (Methoden + private Helfer), kein Verhaltensumbau. Der `Dialect`-SPI-Bruch +ist genau eine Zeile (`extends`-Liste) plus die veränderte Bereitstellung der Provider. + +--- + +## 5. Abnahme dieser Phase (nur `jdbc.db`) + +- [ ] `Dialect` ohne `extends MetadataProvider`; kein `MetadataProvider`-Import mehr in `Dialect.java`. +- [ ] 6 `MetadataProvider`-Klassen mit den vollständigen extrahierten Methoden. +- [ ] `grep -rE "import .*\.(record|api\.meta)\." dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main` → **leer**. +- [ ] `mvn -q verify` im `jdbc.db`-Reactor grün (inkl. Round-Trip-/MetaInfo-Tests gegen die Begleiter). diff --git a/docs/dialect-migration/minimal-split/02-target-structure.md b/docs/dialect-migration/minimal-split/02-target-structure.md new file mode 100644 index 0000000..ee25bb2 --- /dev/null +++ b/docs/dialect-migration/minimal-split/02-target-structure.md @@ -0,0 +1,116 @@ + + +# 02 – Zielstruktur & API-Split + +Zurück zum [Minimal-Split-Plan](README.md). + +Nach der Rollen-Trennung ([01](01-role-separation.md)) hängt der Dialekt-Code nur noch an einer +schmalen, geschlossenen Teilmenge von `api`. Dieses Dokument legt fest, **wie `api` gesplittet** wird +und welche Pakete/Module wohin gehören. + +--- + +## 1. Der `api`-Split + +`org.eclipse.daanse.jdbc.db.api` wird in zwei Artefakte getrennt: + +| Teil | Pakete | Ziel | +|---|---|---| +| **SQL-Modell** | `api.schema.*`, `api.type.*`, `api.sql.*` | → `org.eclipse.daanse.sql.api` | +| **Introspektion (Rest)** | `api.meta.*`, top-level `MetadataProvider`, `DatabaseService`, `MetaDataQueries`, `SnapshotBuilder` | bleibt `org.eclipse.daanse.jdbc.db.api` | + +**Warum diese Grenze sauber ist (belegt):** +- `api.schema` importiert **kein** anderes api-Paket → geschlossene `sealed`-Insel, wandert komplett. +- `api.type`/`api.sql` sind reine Blätter (nur Dialekte nutzen sie) → wandern. +- Der Introspektions-Rest importiert `api.schema` (jetzt in SQL) → Kante **`jdbc.db → sql`**, einseitig. + Konkret: `MetadataProvider`, `MetaDataQueries`, `SnapshotBuilder`, `StructureInfo` referenzieren + Schema-Typen — nach dem Split zeigen diese Referenzen auf `sql.api.schema` (erlaubt, kein Zyklus). + +`record` wird **nicht** gesplittet und **nicht** verschoben: `record.schema`/`record.meta` bleiben +komplett in `jdbc.db` (nur von den `MetadataProvider`-Begleitern und `impl` genutzt). `record` +implementiert `api.schema`-Interfaces (jetzt in SQL) → wiederum einseitig `jdbc.db → sql`. + +--- + +## 2. Paket- & Artefakt-Rename (nur SQL-Seite) + +| Alt | Neu | Bemerkung | +|---|---|---| +| `org.eclipse.daanse.jdbc.db.api.schema` | `org.eclipse.daanse.sql.api.schema` | ganze Insel | +| `org.eclipse.daanse.jdbc.db.api.type` | `org.eclipse.daanse.sql.api.type` | Blatt | +| `org.eclipse.daanse.jdbc.db.api.sql` | `org.eclipse.daanse.sql.api.sql` | Blatt | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | ohne `MetadataProvider`-Vererbung | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | ohne Rolle-A-Methoden | + +**Bleibt unverändert in `jdbc.db`:** `…api.meta`, `…api` (top-level Introspektion), `…record.*`, +`…impl`, `…importer.*`, und **neu** `…dialect.metadata.` (die Begleiter). + +ArtifactIds: +- neu: `org.eclipse.daanse.sql.api` (schema+type+sql), `org.eclipse.daanse.sql.dialect.api`, + `org.eclipse.daanse.sql.dialect.db.*`. +- bleibt: `org.eclipse.daanse.jdbc.db.api` (jetzt nur meta+Introspektion), `…record`, `…impl`, + `…importer.csv`, neu `…dialect.metadata` (o. ä. Sammelmodul der Begleiter). + +> **Namens-Hinweis:** Das verbleibende `jdbc.db.api` enthält nach dem Split nur noch +> Introspektion. Optional könnte es später zu `jdbc.db.introspection` umbenannt werden — **nicht** +> Teil dieses Plans. + +--- + +## 3. Modul-Layout + +**SQL-Repo (neu):** +``` +sql/ + api/ org.eclipse.daanse.sql.api (schema + type + sql) + dialect/ + api/ org.eclipse.daanse.sql.dialect.api + db/ + common/ test-support/ duckdb/ h2/ mariadb/ mssqlserver/ mysql/ oracle/ postgresql/ sqlite/ + deparser/ guard/ statement/ (bestehend) +``` + +**jdbc.db-Repo (nach Umbau):** +``` +jdbc.db/ + api/ org.eclipse.daanse.jdbc.db.api (nur meta + Introspektions-Interfaces) + record/ org.eclipse.daanse.jdbc.db.record (komplett) + impl/ org.eclipse.daanse.jdbc.db.impl + dialect/ + metadata/ org.eclipse.daanse.jdbc.db.dialect.metadata (NEU: MetadataProvider) + importer/csv/ org.eclipse.daanse.jdbc.db.importer.csv +``` + +--- + +## 4. Abhängigkeits-Kanten nach dem Umbau (Zielzustand) + +``` +sql.api → (JDK / slf4j) # keine jdbc.db-Kante +sql.dialect.api → sql.api +sql.dialect.db.* → sql.dialect.api, sql.dialect.db.common, sql.api + +jdbc.db.api (Rest) → sql.api.schema # Introspektion referenziert Schema-Modell +jdbc.db.record → sql.api # Records implementieren Schema-Interfaces +jdbc.db.dialect.metadata. → jdbc.db.api (MetadataProvider), jdbc.db.record, sql.api +jdbc.db.impl → jdbc.db.api, jdbc.db.record, (sql.api) +jdbc.db.importer.csv → jdbc.db.api, jdbc.db.record, sql.dialect.api (+ impl runtime) [ETL] +``` + +Alle `jdbc.db`-Kanten zeigen einseitig auf `sql.*`. `sql.*` hat **keine** `jdbc.db`-Kante → **kein +Zyklus**, `sql` unabhängig baubar/publizierbar. + +--- + +## 5. Minimaler SQL-Typ-Satz (Kontrolle) + +Der SQL-Eigencode (guard/deparser/statement) braucht weiterhin nur die 7 Blatt-Typen +(`Datatype`, `BestFitColumnType`, `BitOperation`, `OrderedColumn`, `SchemaReference`, `TableReference`, +`ColumnReference`) — jetzt aus `sql.api`. Die Dialekte fügen `api.schema` (Insel, DDL-Vokabular) hinzu. +**Kein** `record`, **kein** `api.meta` auf der SQL-Seite. diff --git a/docs/dialect-migration/minimal-split/03-migration-procedure.md b/docs/dialect-migration/minimal-split/03-migration-procedure.md new file mode 100644 index 0000000..c746a35 --- /dev/null +++ b/docs/dialect-migration/minimal-split/03-migration-procedure.md @@ -0,0 +1,96 @@ + + +# 03 – Migrationsprozedur (Schritt für Schritt) + +Zurück zum [Minimal-Split-Plan](README.md). + +Reihenfolge: **erst entkoppeln (in `jdbc.db`), dann splitten, dann verschieben.** Jede Welle endet mit +grünem Build. + +--- + +## Welle R — Rollen-Trennung (in `jdbc.db`) + +Vollständig in [01](01-role-separation.md) beschrieben. Kurzfassung: + +1. Pro großem Dialekt (h2, mariadb, mssqlserver, mysql, oracle, postgresql): Rolle-A-Methoden + + private Helfer in `dialect/metadata//.../MetadataProvider.java` verschieben. +2. `Dialect.java`: `extends MetadataProvider` entfernen, Import streichen. +3. Begleiter als OSGi-Komponenten bereitstellen; Aufrufer/Tests auf den Begleiter umstellen + ([04](04-consumer-rewiring.md)). +4. **Gate:** `mvn -q verify` in `jdbc.db` grün; Dialekt-Main frei von `record`/`api.meta`. + +> Nach Welle R ist der Refactor abgeschlossen und für sich nutzbar — der Repo-Umzug (Wellen S–D) kann +> unabhängig terminiert werden. + +--- + +## Welle S — API-Split (Vorbereitung des SQL-Modells) + +1. Neues Modul `sql/api` anlegen (Root-Reactor-Eintrag); Parent = SQL-Root. +2. Pakete `api.schema`, `api.type`, `api.sql` aus `jdbc.db/api` **herauslösen**: + - `git mv` der drei Paketbäume nach `sql/api/src/main/java/org/eclipse/daanse/sql/api/{schema,type,sql}`. + - Rename `org.eclipse.daanse.jdbc.db.api.{schema,type,sql}` → `org.eclipse.daanse.sql.api.{…}` in + allen mitgezogenen Dateien. +3. Im verbleibenden `jdbc.db/api` (nur `api.meta` + top-level Introspektion): Importe der drei + ausgelagerten Pakete auf `org.eclipse.daanse.sql.api.*` umstellen; Pom-Dep auf `sql.api` (extern, + `0.0.1-SNAPSHOT`) ergänzen. +4. `record` und `impl`: Importe von `…jdbc.db.api.{schema,type,sql}` → `…sql.api.*`; Pom-Dep auf + `sql.api` ergänzen. +5. **Gate:** `sql/api` baut isoliert (`mvn -q -pl org.eclipse.daanse.sql.api -am verify`); `jdbc.db` + baut gegen den `sql.api`-Snapshot. + +> `record` wird **nicht** verschoben — nur seine Importe auf `sql.api` umgebogen. + +--- + +## Welle D — Dialekt-Umzug nach SQL + +Reihenfolge: `sql.dialect.api` → `sql.dialect.db.common` → `test-support` → konkrete Dialekte +(`mysql` vor `mariadb`). + +1. `git mv jdbc.db/dialect/api sql/dialect/api`; Rename `…jdbc.db.dialect.api` → `…sql.dialect.api`; + Pom-Dep auf `sql.api` (reaktor-intern). `Dialect` ist bereits ohne `MetadataProvider` (Welle R). +2. `git mv jdbc.db/dialect/db/common sql/dialect/db/common`; Rename; Dep → `sql.dialect.api`. +3. `test-support` analog. +4. Aktive Dialekte (`duckdb, h2, mariadb, mssqlserver, mysql, oracle, postgresql, sqlite`): + `git mv` nach `sql/dialect/db/`; Rename `…dialect.db.` → `…sql.dialect.db.`; Pom-Deps + → `sql.dialect.api`, `sql.dialect.db.common`, `sql.api` (reaktor-intern). **Kein** `record`-Dep, + **kein** `api.meta`-Dep mehr (dank Welle R). +5. Aggregatoren `sql/dialect/pom.xml`, `sql/dialect/db/pom.xml` + Root-`` ([06](06-build-and-verification.md)). +6. **Gate:** SQL-Reactor baut; Dialekt-Module haben keine `jdbc.db`-Deps. + +> Die `MetadataProvider`-Begleiter bleiben in `jdbc.db/dialect/metadata` — sie ziehen **nicht** mit. + +--- + +## Welle C — Konsumenten & jdbc.db-Rückbau + +Siehe [04](04-consumer-rewiring.md): SQL-Konsumenten (guard/deparser/statement) auf reaktor-intern; +`jdbc.db`-Konsumenten (`impl`, `importer`, `dialect.metadata`) auf publizierte `sql.*`-Snapshots; +`dialect/api`+`dialect/db/aktiv` aus dem `jdbc.db`-Reactor entfernen (Begleiter-Modul bleibt). + +--- + +## Welle V — Per-Version-Härtung (optional) + +Siehe [05](05-per-version-hardening.md). Kann jederzeit nach Welle D erfolgen und ist unabhängig. + +--- + +## Reihenfolge-Kurzform + +``` +R: Rollen-Trennung (jdbc.db) → Gate: jdbc.db grün, Dialekt-Main ohne record/api.meta +S: api-Split (schema/type/sql → sql.api) +D: dialect.api + aktive dialect.db.* → sql +C: Konsumenten + jdbc.db-Rückbau +V: (optional) Per-Version-Härtung +B: Build/Java21/Verifikation (durchgehend) +``` diff --git a/docs/dialect-migration/minimal-split/04-consumer-rewiring.md b/docs/dialect-migration/minimal-split/04-consumer-rewiring.md new file mode 100644 index 0000000..f8336b0 --- /dev/null +++ b/docs/dialect-migration/minimal-split/04-consumer-rewiring.md @@ -0,0 +1,96 @@ + + +# 04 – Konsumenten umverdrahten & OSGi + +Zurück zum [Minimal-Split-Plan](README.md). + +Drei Konsumenten-Gruppen sind betroffen: (1) SQL-Eigencode, (2) `jdbc.db`-Module, (3) die +Bereitstellung/Verdrahtung der neuen `MetadataProvider`-Begleiter. + +--- + +## 1. SQL-Repo-Konsumenten (reaktor-intern) + +Wie beim Wholesale-Plan ([../05-consumer-rewiring.md](../05-consumer-rewiring.md) §1), aber die +Blatt-Typen kommen jetzt aus `sql.api` statt `jdbc.db.api`: + +| Modul | Neu (reaktor-intern) | +|---|---| +| `statement/api` | `sql.dialect.api.generator.*`, `sql.api.schema.{Schema,Table,Column}Reference`, `sql.api.type.*`, `sql.api.sql.*` | +| `statement/impl` | `sql.dialect.db.common`, `sql.dialect.db.mysql`, `sql.api.*` | +| `statement/demo` | `sql.dialect.db.common/mysql/mssqlserver`, `sql.api.*` | +| `guard/jsqltranspiler` | `sql.dialect.db.h2`, `sql.dialect.db.common` | +| `deparser/api`, `deparser/jsqlparser` | `sql.dialect.api` | + +Import-Ersetzung: `org.eclipse.daanse.jdbc.db.dialect` → `org.eclipse.daanse.sql.dialect`; +`org.eclipse.daanse.jdbc.db.api.{schema,type,sql}` → `org.eclipse.daanse.sql.api.{…}`. Der SQL-Code +berührt **nie** `record`/`api.meta` → keine weiteren Änderungen. + +--- + +## 2. `jdbc.db`-Konsumenten (publizierte `sql.*`-Snapshots) + +### 2.1 `impl` (Introspektions-Motor) + +- Importe `…jdbc.db.api.{schema,type,sql}` → `…sql.api.*` (Welle S). +- Behält `…jdbc.db.api` (meta + `MetadataProvider`/`DatabaseService`) **lokal** und `…record` **lokal**. +- Pom: Dep auf `sql.api` (extern). **Kein** `dialect.*`-Dep mehr im Produktionscode (Motor kennt nur + `MetadataProvider`). +- Tests, die bisher einen `Dialect` als Provider durchreichten, nutzen jetzt den passenden + `MetadataProvider` (aus `jdbc.db.dialect.metadata`) oder `MetadataProvider.EMPTY`. + +### 2.2 `jdbc.db.dialect.metadata.` (neu — die Begleiter) + +- Pom: compile-Dep auf `jdbc.db.api` (MetadataProvider/meta), `jdbc.db.record`, und `sql.api` (Schema- + Interfaces, die die Records implementieren). Runtime/DS nach Bedarf. +- Enthält je `MetadataProvider` (aus Welle R). + +### 2.3 `importer/csv` (ETL) + +- Importe `…jdbc.db.api.{schema,type}` → `…sql.api.*`; `…dialect.api.Dialect`/`DialectFactory` → + `…sql.dialect.api.*`. `record`/`impl`/`fastcsv`/`io.fs.watcher.*` unverändert. +- **Hinweis:** `CsvDataImporter` nutzt `Dialect` nur für Quoting/DDL (Rolle B) — nach dem Split + weiterhin gültig. Falls es zusätzlich Metadaten *liest*, dafür künftig einen `MetadataProvider` + verwenden. + +### 2.4 Reactor-Bereinigung `jdbc.db/pom.xml` + +- Aus `` entfernen: `dialect` (die aktive `api`+`db`-Untermenge, jetzt in SQL). +- Ergänzen: `dialect/metadata` (Begleiter-Sammelmodul). +- `api`, `record`, `impl`, `importer` bleiben. + +--- + +## 3. OSGi-Verdrahtung der Provider + +**Ist-Zustand:** Jede `DialectFactory` ist `@Component(service = DialectFactory.class)`, getaggt per +`@DialectName("MYSQL")`. `DatabaseServiceImpl` (`@Component(service = DatabaseService.class)`) nimmt +`MetadataProvider` als **Methoden-Parameter** — der Provider wird also am Aufrufort beschafft, nicht +injiziert. + +**Ziel-Zustand:** Der `MetadataProvider`-Begleiter wird als eigener Service veröffentlicht und +auflösbar gemacht: + +- Variante A (empfohlen): `MetadataProvider` als `@Component(service = MetadataProvider.class)` + mit `@DialectName("MYSQL")`. Ein Aufrufer, der Metadaten für ein Produkt braucht, filtert die + `MetadataProvider`-Services nach `@DialectName`/Produktname; Fallback `MetadataProvider.EMPTY`. +- Variante B: eine `MetadataProviderFactory` analog zur `DialectFactory` + (`MetadataProvider create(DialectInitData)`), parallel registriert. + +Wichtig: `Dialect` und `MetadataProvider` sind jetzt **getrennte Services**. Wer bisher einen +`Dialect` sowohl zur SQL-Generierung als auch als Metadaten-Provider nutzte, hält künftig **zwei** +Referenzen (den Dialekt aus `sql`, den Provider aus `jdbc.db`), beide über denselben +`@DialectName`/Produktnamen auflösbar. + +--- + +## 4. Build-Vorbedingung + +`jdbc.db` (inkl. Begleiter) baut erst grün, wenn `sql.api` + `sql.dialect.*`-Snapshots verfügbar sind: +im SQL-Repo zuerst `mvn -q install`, dann `jdbc.db`. Siehe [06](06-build-and-verification.md). diff --git a/docs/dialect-migration/minimal-split/05-per-version-hardening.md b/docs/dialect-migration/minimal-split/05-per-version-hardening.md new file mode 100644 index 0000000..87f0a8e --- /dev/null +++ b/docs/dialect-migration/minimal-split/05-per-version-hardening.md @@ -0,0 +1,83 @@ + + +# 05 – Per-Version-Härtung (optional, Option O2) + +Zurück zum [Minimal-Split-Plan](README.md). + +**Optional.** Diese Phase ist für den minimalen Schnitt **nicht erforderlich** — nach der +Rollen-Trennung ([01](01-role-separation.md)) ist der Dialekt bereits frei von `record`/`api.meta`. +Sie setzt die ursprüngliche Idee „**Dialekte fest pro Version einbauen, ohne JDBC-Record zum Setzen +der Eigenschaften**" konsequent um und entkoppelt den Dialekt zusätzlich von jeglicher Laufzeit- +Metadaten-Beschaffung. + +--- + +## 1. Ausgangslage (belegt) + +Die SQL-Generierung liest heute nur Konstanten + einen `int` `dialectVersion` (aus `DialectInitData`). +Zwei Versions-Muster existieren bereits: + +- **Subklasse pro Variante:** `Db2OldAs400Dialect extends Db2Dialect` (überschreibt + `allowsFromQuery()→false`), eigener `@DialectName("DB2_OLD_AS400")`. +- **In-Methode:** `dialectVersion.isUnknownOrAtLeast(8,0)` (mysql/oracle/postgresql/mssql/mariadb/sqlite). + +`DialectInitData` trägt neben der Version noch metadaten-abgeleitete Felder (`readOnly`, +`supportedResultSetStyles`, `sqlKeywordsLower`, `maxColumnNameLength`), die eine fest verdrahtete +Variante als Konstanten liefern könnte. + +--- + +## 2. Ziel + +Statt zur Laufzeit `DialectInitData.fromConnection(...)` zu lesen, feste Pro-Version-Klassen mit +**einkompilierten** Eigenschaften — die `DialectFactory` wählt allein anhand von Produktname + Version +die passende Klasse; **kein** `DatabaseMetaData`-Zugriff mehr nötig. + +``` +MySqlDialect (Basis, gemeinsame SQL-Generierung) + ├─ MySql57Dialect → dialectVersion=(5,7), Capabilities/Keywords als Konstanten + └─ MySql8Dialect → dialectVersion=(8,0), rekursive CTE, Percentile, … +``` + +--- + +## 3. Schritte (pro DB, exemplarisch MySQL) + +1. Die in-Methode-Verzweigungen (`isUnknownOrAtLeast(8,0)`) identifizieren und in **feste + Überschreibungen** je Versionsklasse überführen (z. B. `cteGenerator()` in `MySql8Dialect` liefert + direkt die rekursive Variante). +2. Metadaten-abgeleitete Felder als Konstanten in die Versionsklasse ziehen (Quote-Zeichen, + `maxColumnNameLength`, ggf. Keyword-Set). +3. `MySqlDialectFactory.createDialect(DialectInitData init)` wählt anhand `init.version()` / + `init.productVersion()` die konkrete Klasse (Fallback: neueste bekannte Version bei UNKNOWN). +4. `DialectInitData` bleibt als Träger für Produktname/Version bestehen, kann aber vollständig + **offline** erzeugt werden (`ansiDefaults().withVersion(8,0).withQuoteIdentifierString("`")`). + +--- + +## 4. Abwägung + +| Vorteil | Nachteil | +|---|---| +| Dialekt komplett statisch, kein Laufzeit-`DatabaseMetaData` | Klassen-Explosion (mehrere Versionen je DB) | +| Testbar ohne Live-DB (feste Varianten) | Versions-Auswahllogik muss robust sein | +| Verhalten pro Version explizit sichtbar | Pflegeaufwand bei neuen DB-Versionen | + +**Empfehlung zur Reihenfolge:** Erst den Rollen-Split ([01](01-role-separation.md)) + Umzug +abschließen (liefert bereits den minimalen Kern). Die Per-Version-Härtung danach **inkrementell pro +DB** durchführen — sie ist unabhängig und nicht Voraussetzung des Splits. + +--- + +## 5. Abnahme (optional) + +- [ ] Pro gehärteter DB: Versions-spezifische Klassen mit konstanten Eigenschaften; keine + `isUnknownOrAtLeast`-Verzweigung mehr im heißen Pfad. +- [ ] `DialectFactory` wählt korrekt anhand Produktname/Version; UNKNOWN → definierter Fallback. +- [ ] Dialekt-Unit-Tests laufen **ohne** Live-DB (nur `ansiDefaults().withVersion(...)`). diff --git a/docs/dialect-migration/minimal-split/06-build-and-verification.md b/docs/dialect-migration/minimal-split/06-build-and-verification.md new file mode 100644 index 0000000..00295d5 --- /dev/null +++ b/docs/dialect-migration/minimal-split/06-build-and-verification.md @@ -0,0 +1,108 @@ + + +# 06 – Build, Java 21 & Verifikation + +Zurück zum [Minimal-Split-Plan](README.md). + +--- + +## 1. Java 21 & Poms + +Wie beim Wholesale-Plan ([../06-build-and-java21.md](../06-build-and-java21.md)): + +- SQL-Root-Pom `java.version`/`java.release` auf **21**. +- Neue Aggregatoren `sql/dialect/pom.xml` (packaging=pom) + `sql/dialect/db/pom.xml` mit + ``: `common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite`. +- Root-`` um `api`, `dialect` ergänzen (**kein** `record` — es bleibt in `jdbc.db`). +- `dependencyManagement` um Test-Treiber/Testcontainers/JUnit aus den Dialekt-Poms erweitern. +- Parent `pom.parent:0.0.7`, `${revision}=0.0.1-SNAPSHOT`, Snapshot-Repo unverändert. + +**Unterschied zum Wholesale-Plan:** `sql/record` gibt es **nicht** — `record` bleibt komplett in +`jdbc.db`. `sql/api` enthält nur `schema`+`type`+`sql`. + +--- + +## 2. Build-Reihenfolge + +```bash +# 1) SQL-Repo bauen + installieren (Voraussetzung für jdbc.db) +cd org.eclipse.daanse.sql && mvn -q clean install + +# 2) jdbc.db gegen die frischen sql.*-Snapshots (inkl. api-Split, Begleiter, impl, importer) +cd ../org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +`sql` **muss** vor `jdbc.db` installiert sein (einseitige Kante `jdbc.db → sql`). + +--- + +## 3. Verifikation + +### 3.1 Phase R (Rollen-Trennung, nur `jdbc.db`, vor jedem Umzug) + +```bash +cd org.eclipse.daanse.jdbc.db +# Dialekt-Main frei von record/api.meta: +grep -rE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main # → leer +# Dialect nicht mehr MetadataProvider: +grep -n "extends" dialect/api/src/main/java/org/eclipse/daanse/jdbc/db/dialect/api/Dialect.java # kein MetadataProvider +mvn -q verify # inkl. Round-Trip-/MetaInfo-Tests gegen die MetadataProvider +``` + +### 3.2 Struktur nach dem Umzug + +```bash +# SQL-Repo ohne jdbc.db-Referenzen: +grep -rl "org.eclipse.daanse.jdbc.db" org.eclipse.daanse.sql/ --include=*.java # → leer +# SQL-Seite ohne record/api.meta: +grep -rn "sql.record\|api\.meta" org.eclipse.daanse.sql/ --include=*.java # → leer +# SQL-Repo ohne externe jdbc.db-Deps: +grep -rn "jdbc.db" org.eclipse.daanse.sql/ --include=pom.xml # → leer +# jdbc.db behält record/impl/importer/dialect.metadata: +grep -nE "(api|record|impl|importer|dialect)" org.eclipse.daanse.jdbc.db/pom.xml +``` + +### 3.3 Tests + +- **SQL:** `statement/demo` (H2/MSSQL via Testcontainers), `guard/jsqltranspiler`, `deparser`, + Dialekt-Unit-Tests (Generator/Quoter) — grün. +- **jdbc.db:** `impl`-Tests (Introspektion + MetaInfo gegen die Begleiter), `importer/csv`-ETL-Tests + — grün. + +--- + +## 4. Risiken + +| Risiko | Eintritt | Wirkung | Gegenmaßnahme | +|---|---|---|---| +| `Dialect`-SPI-Bruch trifft externe Nutzer | mittel | Kompilierfehler bei Konsumenten von `dialect.getAll*` | Rolle A war intern (Motor nutzt `MetadataProvider`-Param); Begleiter bereitstellen; migrieren | +| OSGi-Auflösung Dialekt ↔ Provider | mittel | Provider zur Laufzeit nicht gefunden | `@DialectName`-Tagging beibehalten; Fallback `MetadataProvider.EMPTY` | +| `api`-Split zieht falsche Grenze | gering | Zyklus/Kompilierfehler | Grenze belegt: `api.schema` importiert kein anderes api-Paket | +| Round-Trip-Tests hingen am Dialekt-als-Provider | mittel | Testfehler nach Split | Tests auf Begleiter umstellen ([04](04-consumer-rewiring.md) §2.1) | +| Release-Reihenfolge | mittel | `jdbc.db` baut nicht | `sql` zuerst installieren/publizieren | + +--- + +## 5. Definition of Done + +**Refactor (Phase R):** +- [ ] `Dialect` ohne `extends MetadataProvider`; `jdbc.db` grün. +- [ ] 6 `MetadataProvider`-Begleiter vollständig; Dialekt-Main ohne `record`/`api.meta`. + +**Split & Umzug:** +- [ ] `sql.api` = nur schema/type/sql; **kein** `sql.record`. +- [ ] SQL-Repo baut ohne `jdbc.db`-Deps (Java 21); SQL-Seite ohne `record`/`api.meta`. +- [ ] `jdbc.db` baut gegen `sql.*`-Snapshots; `record`, `api.meta`, `impl`, `importer`, + `dialect.metadata` bleiben dort. +- [ ] Einweg `jdbc.db → sql`, kein Zyklus. +- [ ] `statement/demo`-Integrationstests (H2/MSSQL) + `impl`/`importer`-Tests grün. +- [ ] Lizenz-Header-/Javadoc-CI in beiden Repos grün. + +**Optional (Phase V):** siehe [05](05-per-version-hardening.md). diff --git a/docs/dialect-migration/minimal-split/IMPLEMENTATION-GUIDE.md b/docs/dialect-migration/minimal-split/IMPLEMENTATION-GUIDE.md new file mode 100644 index 0000000..37fab4e --- /dev/null +++ b/docs/dialect-migration/minimal-split/IMPLEMENTATION-GUIDE.md @@ -0,0 +1,712 @@ + + +# Umsetzungs-Anleitung (Schritt für Schritt) — Minimal-Split + Dialekt-Versionen + +Zurück zum [Minimal-Split-Plan](README.md). + +> 🌐 Sprache: **Deutsch** (diese Datei) · [Русский](../ru/minimal-split/IMPLEMENTATION-GUIDE.md) + +**Für wen?** Diese Anleitung ist für einen **Junior-Entwickler** geschrieben. Jeder Schritt sagt dir +**genau**, in welchem **Repo**, welchem **Modul**, welcher **Datei** du **was** tust, mit +**Code-Beispielen** (vorher/nachher) und einem **Prüfbefehl**. Arbeite die Teile **in Reihenfolge** ab +und mache **nach jedem Checkpoint** einen Build. Wenn ein Checkpoint rot ist: **nicht weitermachen** — +erst reparieren. + +**Zwei Ziele in dieser Anleitung:** +1. **Minimal-Split** (Teile 1–4): den Dialekt-Stack minimal aus `jdbc.db` nach `sql` holen. +2. **Dialekt-Versionen** (Teil 5): Dialekte fest pro Version bauen (deine Idee, „kein JDBC-Record zum + Setzen der Eigenschaften"). + +Am Ende (Teil 6) **baut und läuft alles** in beiden Repos. + +--- + +## Legende / Konventionen + +- **Repo `jdbc.db`** = `/home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db` +- **Repo `sql`** = `/home/stbischof/git/daanse/org.eclipse.daanse.sql` +- Paket-Präfix alt: `org.eclipse.daanse.jdbc.db.…` — neu (SQL-Seite): `org.eclipse.daanse.sql.…` +- „Prüfbefehl" heißt: im jeweiligen Repo-Verzeichnis ausführen. +- **Wichtig:** Immer `git mv` benutzen (nicht kopieren+löschen) — das erhält die Git-Historie. + +--- + +# Teil 0 — Vorbereitung + +### Schritt 0.1 — Beide Repos, Arbeitszweige + +**Repo:** beide. **Aktion:** je einen Branch anlegen. + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && git checkout -b feat/minimal-split +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && git checkout -b feat/minimal-split +``` + +### Schritt 0.2 — Ausgangs-Build (muss grün sein, bevor du etwas änderst) + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean install +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install +``` + +> Wenn hier schon etwas rot ist, liegt das **nicht** an dir — kläre das zuerst mit dem Team. + +### Schritt 0.3 — Merke dir die 3 Dialekt-Gruppen + +- **6 „große" Dialekte** (haben Metadaten-Rolle A): `h2, mariadb, mssqlserver, mysql, oracle, postgresql` +- **2 „kleine" Dialekte** (keine Rolle A): `duckdb, sqlite` +- **Infrastruktur:** `common, test-support` + +Nur die **6 großen** bekommen in Teil 1 einen Begleiter. + +--- + +# Teil 1 — Phase R: Rollen-Trennung (komplett im Repo `jdbc.db`) + +**Ziel:** Aus jedem großen Dialekt die „Metadaten-lesen"-Methoden (Rolle A) in eine eigene +Begleiter-Klasse verschieben, und `Dialect` von `MetadataProvider` entkoppeln. **Es zieht noch nichts +um** — alles bleibt in `jdbc.db`, muss aber danach grün bauen. + +### Schritt 1.1 — Neues Modul `dialect/metadata` anlegen + +**Repo:** `jdbc.db`. **Aktion:** neuen Ordner + Aggregator-Pom. + +Datei **neu**: `dialect/metadata/pom.xml` +```xml + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect + ${revision} + ../pom.xml + + org.eclipse.daanse.jdbc.db.dialect.metadata + pom + Eclipse Daanse JDBC DB Dialect Metadata Providers + + mysql + mariadb + h2 + mssqlserver + oracle + postgresql + + +``` + +Datei **ändern**: `dialect/pom.xml` → in `` `metadata` ergänzen: +```xml + + api + db + metadata + +``` + +### Schritt 1.2 — Leaf-Pom für den ersten Begleiter (MySQL) + +**Repo:** `jdbc.db`. **Datei neu:** `dialect/metadata/mysql/pom.xml` +```xml + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.metadata + ${revision} + ../pom.xml + + org.eclipse.daanse.jdbc.db.dialect.metadata.mysql + Eclipse Daanse JDBC DB Metadata Provider MySQL + + org.slf4jslf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.api + ${revision} + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.record + ${revision} + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.api + ${revision} + + + +``` + +### Schritt 1.3 — Begleiter-Klasse `MySqlMetadataProvider` anlegen + +**Repo:** `jdbc.db`. **Datei neu:** +`dialect/metadata/mysql/src/main/java/org/eclipse/daanse/jdbc/db/dialect/metadata/mysql/MySqlMetadataProvider.java` + +Kopiere **den EPL-Header** aus einer bestehenden Datei an den Anfang. Dann: + +```java +package org.eclipse.daanse.jdbc.db.dialect.metadata.mysql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.daanse.jdbc.db.api.MetadataProvider; +import org.eclipse.daanse.jdbc.db.api.meta.IndexInfo; +import org.eclipse.daanse.jdbc.db.api.meta.IndexInfoItem; +import org.eclipse.daanse.jdbc.db.api.schema.ColumnReference; +import org.eclipse.daanse.jdbc.db.api.schema.ImportedKey; +import org.eclipse.daanse.jdbc.db.api.schema.PrimaryKey; +import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference; +import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.jdbc.db.dialect.api.DialectName; +import org.eclipse.daanse.jdbc.db.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.jdbc.db.record.schema.IndexInfoRecord; +import org.eclipse.daanse.jdbc.db.record.schema.PrimaryKeyRecord; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** + * MySQL-native metadata provider. Extracted from MySqlDialect (role A) so that + * the SQL-generation dialect no longer depends on record/api.meta. + */ +@Component(service = MetadataProvider.class, scope = ServiceScope.SINGLETON) +@DialectName("MYSQL") +public class MySqlMetadataProvider implements MetadataProvider { + + // ---- HIER: die aus MySqlDialect verschobenen getAll*/get*-Methoden ---- + // Beispiel: getAllPrimaryKeys (1:1 aus MySqlDialect übernommen) + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + // ... ebenso getAllImportedKeys, getAllExportedKeys, getAllIndexInfo, + // getUniqueConstraints, getAllTriggers, getAllSequences (alle aus MySqlDialect) ... + + // ---- HIER: die privaten Helfer, die diese Methoden brauchen ---- + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) return schema; + String catalog = connection.getCatalog(); + if (catalog != null) return catalog; + return connection.getSchema() != null ? connection.getSchema() : ""; + } + + private static IndexInfoItem.IndexType mapMySqlIndexType(String indexType) { + if (indexType == null) return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + return switch (indexType.toUpperCase()) { + case "HASH" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + // Builder für zusammengesetzte Primärschlüssel — 1:1 aus MySqlDialect + private static final class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; this.constraintName = constraintName; this.schemaName = schemaName; + } + void addColumn(String c) { columns.add(c); } + PrimaryKey build() { + TableReference table = new TableReference( + Optional.of(new SchemaReference(schemaName)), tableName); + List cols = new ArrayList<>(); + for (String c : columns) cols.add(new ColumnReference(Optional.of(table), c)); + return new PrimaryKeyRecord(table, cols, Optional.ofNullable(constraintName)); + } + } +} +``` + +> **Wichtig:** Nimm die Methoden-Rümpfe **wörtlich** aus `MySqlDialect.java`. Verschiebe **auch** alle +> privaten Helfer, die nur von diesen Methoden benutzt werden (`readImportedKey`, `mapTriggerTiming`, +> `mapTriggerEvent`, `mapReferentialAction`, `mapMySqlIndexType`, `resolveSchema`, `PkBuilder` …). + +### Schritt 1.4 — Die verschobenen Methoden aus `MySqlDialect` entfernen + +**Repo:** `jdbc.db`. **Datei:** +`dialect/db/mysql/src/main/java/.../mysql/MySqlDialect.java` + +1. Alle `@Override`-Methoden löschen, die du nach `MySqlMetadataProvider` kopiert hast + (`getAllPrimaryKeys`, `getAllIndexInfo`, `getAllImportedKeys`, …). +2. Die nur dort benutzten privaten Helfer löschen (`PkBuilder`, `resolveSchema`, `mapMySqlIndexType`, …). +3. Die jetzt unbenutzten Imports entfernen — insbesondere alle + `import org.eclipse.daanse.jdbc.db.record.*` und `import org.eclipse.daanse.jdbc.db.api.meta.*`. + +**Prüfung (muss leer sein):** +```bash +grep -nE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/mysql/src/main/java/org/eclipse/daanse/jdbc/db/dialect/db/mysql/MySqlDialect.java +``` + +> **Achtung Rolle B:** `import …api.schema.*` (z. B. `TableReference`, `ColumnDefinition`) **bleiben** +> in der Dialektklasse, wenn sie in DDL-Methoden benutzt werden — die sind erlaubt und ziehen später +> als Insel mit nach SQL. + +### Schritt 1.5 — Für die 5 übrigen großen Dialekte wiederholen + +**Repo:** `jdbc.db`. Wiederhole Schritt 1.2–1.4 für: `h2, mariadb, mssqlserver, oracle, postgresql`. +Jeweils `dialect/metadata//…/MetadataProvider.java` mit `@DialectName("")`. Die +`@DialectName`-Werte findest du in der jeweiligen `DialectFactory` (z. B. `H2`, `MARIADB`, +`MSSQLSERVER`, `ORACLE`, `POSTGRESQL`). + +> `duckdb`, `sqlite`, `common`: **nichts tun** — sie haben keine Rolle-A-Methoden. + +### Schritt 1.6 — `Dialect` von `MetadataProvider` entkoppeln + +**Repo:** `jdbc.db`. **Datei:** `dialect/api/src/main/java/.../dialect/api/Dialect.java` + +Vorher: +```java +import org.eclipse.daanse.jdbc.db.api.MetadataProvider; +... +public interface Dialect + extends IdentifierQuoter, LiteralQuoter, DialectCapabilitiesProvider, TypeMapper, MetadataProvider { +``` +Nachher: +```java +// (Import gelöscht) +public interface Dialect + extends IdentifierQuoter, LiteralQuoter, DialectCapabilitiesProvider, TypeMapper { +``` + +`BestFitColumnType getType(ResultSetMetaData, int)` und die `api.type`-Importe **bleiben**. + +### Schritt 1.7 — Aufrufer & Tests auf den Begleiter umstellen + +**Repo:** `jdbc.db`. + +- **`DatabaseServiceImpl`** (`impl/.../DatabaseServiceImpl.java`) ändert sich **nicht** in der + Signatur — es nimmt `MetadataProvider` als Parameter (`createMetaInfo(Connection, MetadataProvider)`, + `getCatalogs(Connection, MetadataProvider)`). Nur die **Aufrufstelle** übergibt jetzt den Begleiter. +- **Tests**, die bisher einen Dialekt als Provider durchreichten, ändern die eine Zeile: + +Vorher: +```java +MetaInfo mi = service.createMetaInfo(conn, new MySqlDialect(init)); +``` +Nachher: +```java +MetaInfo mi = service.createMetaInfo(conn, new MySqlMetadataProvider()); +``` + +> Dialekte ohne native Metadaten (duckdb/sqlite): `MetadataProvider.EMPTY` verwenden. + +### ✅ Checkpoint 1 (Repo `jdbc.db` muss grün sein) + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db +# Dialekt-Main frei von record/api.meta: +grep -rE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main # → leer +# Dialect nicht mehr MetadataProvider: +grep -n "extends" dialect/api/src/main/java/org/eclipse/daanse/jdbc/db/dialect/api/Dialect.java +mvn -q clean verify # muss grün sein +``` + +Wenn grün: **Phase R ist fertig.** Committe: `git commit -am "Phase R: split MetadataProvider role out of dialects"`. + +--- + +# Teil 2 — Phase S: `api`-Split (schema/type/sql → neues `sql.api`) + +**Ziel:** Nur die vom Dialekt gebrauchten Pakete (`api.schema`, `api.type`, `api.sql`) werden zu einem +neuen SQL-Modul. Der Introspektions-Rest (`api.meta`, `MetadataProvider`, `DatabaseService`, …) bleibt +in `jdbc.db`. + +### Schritt 2.1 — Modul `sql/api` anlegen + +**Repo:** `sql`. **Datei neu:** `api/pom.xml` +```xml + + + org.eclipse.daanse + org.eclipse.daanse.sql + ${revision} + + org.eclipse.daanse.sql.api + Eclipse Daanse SQL API (schema/type/sql model) + + org.slf4jslf4j-api + + +``` +**Datei ändern:** `pom.xml` (SQL-Root) → in `` `api` an den Anfang setzen: +```xml + + api + guard + deparser + statement + +``` + +### Schritt 2.2 — Pakete verschieben (`git mv`) + umbenennen + +**Repo:** beide (Quelle `jdbc.db`, Ziel `sql`). Für jedes der 3 Pakete (`schema`, `type`, `sql`): + +```bash +# Beispiel für 'schema' – analog für 'type' und 'sql' +SRC=/home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db/api/src/main/java/org/eclipse/daanse/jdbc/db/api/schema +DST=/home/stbischof/git/daanse/org.eclipse.daanse.sql/api/src/main/java/org/eclipse/daanse/sql/api/schema +mkdir -p "$(dirname "$DST")" +git -C /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db mv "$SRC" "$DST" # wenn im selben Repo; sonst normales mv + git add in beiden +``` + +> **Achtung:** `git mv` funktioniert nur innerhalb eines Repos. Über Repo-Grenzen: Ordner mit `mv` +> verschieben, dann in `jdbc.db` `git rm -r` (alten Pfad) und in `sql` `git add` (neuer Pfad). Die +> Historie geht dabei über die Grenze verloren — das ist ok (siehe Wholesale-Plan +> [../07-anomalies-and-risks.md](../07-anomalies-and-risks.md) §5). + +Dann in **allen** verschobenen Dateien den Paketnamen ersetzen: +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql/api +grep -rl "org.eclipse.daanse.jdbc.db.api.schema" . | xargs sed -i \ + 's/org\.eclipse\.daanse\.jdbc\.db\.api\.schema/org.eclipse.daanse.sql.api.schema/g' +# ebenso .type und .sql +``` +Und die `package-info.java` in jedem der drei Pakete anpassen. + +### Schritt 2.3 — Rest von `jdbc.db/api` auf `sql.api` zeigen lassen + +**Repo:** `jdbc.db`. Das verbleibende `api`-Modul (jetzt nur `api.meta` + top-level Introspektion) +importiert Schema-Typen. Diese Importe umbiegen: +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db/api +grep -rl "org.eclipse.daanse.jdbc.db.api.\(schema\|type\|sql\)" . | xargs sed -i \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.schema/org.eclipse.daanse.sql.api.schema/g' \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.type/org.eclipse.daanse.sql.api.type/g' \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.sql/org.eclipse.daanse.sql.api.sql/g' +``` +**Datei:** `api/pom.xml` → Dependency auf das neue SQL-Modul ergänzen: +```xml + + org.eclipse.daanse + org.eclipse.daanse.sql.api + 0.0.1-SNAPSHOT + +``` + +### Schritt 2.4 — `record`, `impl`, Begleiter auf `sql.api` umstellen + +**Repo:** `jdbc.db`, Module `record`, `impl`, `dialect/metadata/*`. Gleiche `sed`-Ersetzung der +`api.{schema,type,sql}`-Importe wie in 2.3; in jedem `pom.xml` die `sql.api`-Dependency ergänzen. + +### ✅ Checkpoint 2 + +```bash +# SQL-api baut isoliert: +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q -pl org.eclipse.daanse.sql.api -am install +# jdbc.db baut gegen den frischen sql.api-Snapshot: +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` +Committen, wenn grün. + +--- + +# Teil 3 — Phase D: Dialekte nach `sql` verschieben + +**Ziel:** `dialect.api` (jetzt ohne MetadataProvider) + `common` + `test-support` + die 8 aktiven +Dialekte nach `sql`. Sie brauchen jetzt **kein** `record` und **kein** `api.meta` mehr. + +### Schritt 3.1 — Aggregatoren im SQL-Repo + +**Repo:** `sql`. **Dateien neu:** `dialect/pom.xml` und `dialect/db/pom.xml` (packaging=pom). Muster +siehe [06-build-and-verification.md](06-build-and-verification.md) §1. Root-`` um `dialect` +ergänzen. + +### Schritt 3.2 — `dialect.api` verschieben + umbenennen + +**Repo:** beide. Ordner `jdbc.db/dialect/api` → `sql/dialect/api`. Paket-Rename +`org.eclipse.daanse.jdbc.db.dialect.api` → `org.eclipse.daanse.sql.dialect.api`. **Pom:** Parent auf +`org.eclipse.daanse.sql.dialect`, Dependency `jdbc.db.api` → `org.eclipse.daanse.sql.api` +(reaktor-intern, `${project.version}`). + +### Schritt 3.3 — `common` und `test-support` verschieben + +**Repo:** beide. Wie 3.2; Deps → `sql.dialect.api` / `sql.api`. + +### Schritt 3.4 — Die 8 aktiven Dialekte verschieben (mysql VOR mariadb) + +**Repo:** beide. Reihenfolge: `mysql`, dann `mariadb` (weil `mariadb → mysql`), dann `duckdb, h2, +mssqlserver, oracle, postgresql, sqlite` in beliebiger Reihenfolge. + +Pro Dialekt: +1. Ordner `jdbc.db/dialect/db/` → `sql/dialect/db/`. +2. Paket-Rename `…dialect.db.` → `…sql.dialect.db.`; Schema-Importe `…jdbc.db.api.schema` → + `…sql.api.schema` (usw.). +3. **Pom umschreiben** — Beispiel `mysql` (vorher/nachher der Kern-Deps): + +Vorher (`dialect/db/mysql/pom.xml`): +```xml +org.eclipse.daanse.jdbc.db.dialect.api${revision} +org.eclipse.daanse.jdbc.db.dialect.db.common${revision} +org.eclipse.daanse.jdbc.db.record${revision} +org.eclipse.daanse.jdbc.db.impl${revision}test +``` +Nachher (`sql/dialect/db/mysql/pom.xml`): +```xml +org.eclipse.daanse.sql.dialect.api${project.version} +org.eclipse.daanse.sql.dialect.db.common${project.version} +org.eclipse.daanse.sql.api${project.version} + + +``` + +> Der `record`- und der `impl`(test)-Dependency **entfallen**, weil die Metadaten-Rolle (die diese +> brauchte) in Teil 1 nach `jdbc.db/dialect/metadata` gewandert ist. Falls ein Dialekt-Test noch +> `impl` braucht (Round-Trip), verschiebe diesen Test mit nach `jdbc.db/dialect/metadata/` oder +> markiere ihn temporär `@Disabled` (siehe [01-role-separation.md](01-role-separation.md)). + +4. In `sql/dialect/db/pom.xml` `` eintragen. + +### ✅ Checkpoint 3 + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql +# Kein jdbc.db mehr in den Dialekt-Pakten: +grep -rl "org.eclipse.daanse.jdbc.db" dialect/ --include=*.java # → leer +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am install # baut inkl. Abhängigkeiten +``` + +--- + +# Teil 4 — Phase C: Konsumenten umverdrahten + `jdbc.db`-Rückbau + +### Schritt 4.1 — SQL-Konsumenten reaktor-intern + +**Repo:** `sql`, Module `guard`, `deparser`, `statement`. In deren `pom.xml` die externen +`jdbc.db.dialect.*`-Deps durch die reaktor-internen `sql.dialect.*` ersetzen; Java-Importe +`org.eclipse.daanse.jdbc.db.dialect` → `org.eclipse.daanse.sql.dialect`, `…jdbc.db.api.{schema,type,sql}` +→ `…sql.api.*`. Details: [04-consumer-rewiring.md](04-consumer-rewiring.md) §1. + +### Schritt 4.2 — Java 21 im SQL-Repo + +**Repo:** `sql`. **Datei:** `pom.xml` (Root) → `java.version`/`java.release` auf `21` (siehe +[06-build-and-verification.md](06-build-and-verification.md) §1). + +### Schritt 4.3 — `jdbc.db` auf publizierte `sql.*`-Snapshots + +**Repo:** `jdbc.db`, Module `impl`, `importer/csv`, `dialect/metadata/*`. In den `pom.xml`: +Dialekt-/Schema-Deps auf `sql.*` (extern, `0.0.1-SNAPSHOT`). **Datei:** `jdbc.db/pom.xml` → aus +`` das (jetzt umgezogene) `dialect`-Aggregat entfernen bzw. auf `dialect/metadata` reduzieren. + +### ✅ Checkpoint 4 (Reihenfolge beachten!) + +```bash +# 1) SQL zuerst installieren: +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install +# 2) jdbc.db gegen sql.*-Snapshots: +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +**Damit ist der Minimal-Split fertig.** Teil 5 ist die optionale Dialekt-Versionen-Erweiterung. + +--- + +# Teil 5 — Phase V: Dialekt-Versionen fest einbauen (deine Idee, O2) + +**Ziel:** Statt zur Laufzeit die Version aus `DialectInitData` auszuwerten (`isUnknownOrAtLeast(8,0)`), +gibt es **feste Klassen pro Version** mit einkompilierten Eigenschaften. Beispiel MySQL. **Repo:** `sql`, +Modul `sql/dialect/db/mysql`. + +### Schritt 5.1 — Basisklasse identifizieren + +`MySqlDialect` bleibt die **gemeinsame Basis** (SQL-Generierung, Quoting). Heute verzweigt sie intern: +```java +// MySqlDialect.java (heute) +public CteGenerator cteGenerator() { + boolean supports = DialectVersion.UNKNOWN.equals(dialectVersion) || dialectVersion.atLeast(8, 0); + ... +} +public boolean requiresOrderByAlias() { return dialectVersion.isUnknownOrAtLeast(5, 7); } +``` + +### Schritt 5.2 — Zwei Versionsklassen anlegen + +**Datei neu:** `…/mysql/MySql8Dialect.java` +```java +package org.eclipse.daanse.sql.dialect.db.mysql; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; + +/** MySQL 8.x — recursive CTE, ORDER BY alias required, percentile functions. */ +public class MySql8Dialect extends MySqlDialect { + public MySql8Dialect(DialectInitData init) { + super(init); + } + @Override public boolean requiresOrderByAlias() { return true; } // fest, statt Version-Check + // cteGenerator(): rekursive Variante — hier fest aktivieren + // supportsPercentileCont()/Disc(): true +} +``` +**Datei neu:** `…/mysql/MySql57Dialect.java` +```java +package org.eclipse.daanse.sql.dialect.db.mysql; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; + +/** MySQL 5.7 — no recursive CTE, ORDER BY alias required, no percentile. */ +public class MySql57Dialect extends MySqlDialect { + public MySql57Dialect(DialectInitData init) { + super(init); + } + @Override public boolean requiresOrderByAlias() { return true; } + // cteGenerator(): NICHT-rekursive Variante + // supportsPercentileCont()/Disc(): false +} +``` + +> Ziehe die heutigen `dialectVersion.isUnknownOrAtLeast(...)`-Verzweigungen aus `MySqlDialect` heraus +> und mache sie in den Versionsklassen zu **festen** `@Override`-Rückgaben. Was versionsunabhängig ist, +> bleibt in `MySqlDialect`. + +### Schritt 5.3 — Factory wählt anhand der Version + +**Datei:** `…/mysql/MySqlDialectFactory.java` + +Vorher: +```java +@Override +public Function getConstructorFunction() { + return MySqlDialect::new; +} +``` +Nachher (Auswahl statt fixem Konstruktor): +```java +@Override +public Dialect createDialect(DialectInitData init) { + if (MySqlDialect.looksLikeInfobright(init)) { + throw new IllegalStateException("Snapshot looks like Infobright; use InfobrightDialectFactory"); + } + // Version bestimmt die konkrete Klasse; UNKNOWN → neueste bekannte (8.x) + var v = init.version(); + if (v.atLeast(8, 0) || v.equals(org.eclipse.daanse.sql.dialect.api.DialectVersion.UNKNOWN)) { + return new MySql8Dialect(init); + } + return new MySql57Dialect(init); +} +``` +`getConstructorFunction()` wird dann nicht mehr gebraucht (oder liefert die Basis für Kompatibilität). + +### Schritt 5.4 — Unit-Test ohne Live-DB + +**Datei neu:** `…/mysql/src/test/java/.../MySql8DialectTest.java` +```java +@Test +void mysql8_requiresOrderByAlias_true_offline() { + var init = DialectInitData.ansiDefaults() + .withQuoteIdentifierString("`") + .withVersion(8, 0); // rein statisch, KEINE Connection nötig + var d = new MySql8Dialect(init); + assertThat(d.requiresOrderByAlias()).isTrue(); +} +``` + +### ✅ Checkpoint 5 + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am verify # Versionsklassen + Offline-Tests grün +``` + +> Wiederhole 5.1–5.4 für weitere DBs nur nach Bedarf — jede DB ist unabhängig. + +--- + +# Teil 6 — Endabnahme: alles baut und läuft + +### Schritt 6.1 — Voller Build in korrekter Reihenfolge + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install # SQL zuerst! +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +### Schritt 6.2 — Struktur-Kontrollen (alle müssen „leer" liefern) + +```bash +# SQL-Repo enthält keine jdbc.db-Referenzen mehr: +grep -rl "org.eclipse.daanse.jdbc.db" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=*.java +# SQL-Seite ohne record/api.meta: +grep -rn "sql\.record\|api\.meta" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=*.java +# SQL-Poms ohne jdbc.db-Deps: +grep -rn "jdbc.db" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=pom.xml +``` + +### Schritt 6.3 — Wichtige Tests + +- **SQL:** `statement/demo` (H2 + MSSQL via Testcontainers), `guard`, `deparser`, MySQL-Versionstests. +- **jdbc.db:** `impl` (Introspektion + MetaInfo gegen die neuen Begleiter), `importer/csv` (ETL). + +### Schritt 6.4 — Abschluss-Checkliste + +- [ ] Beide Repos bauen grün (SQL zuerst installiert). +- [ ] SQL-Repo hat **keine** `jdbc.db`-Abhängigkeit; SQL-Dialekt-Code ohne `record`/`api.meta`. +- [ ] `Dialect` erbt nicht mehr von `MetadataProvider`; 6 Begleiter in `jdbc.db/dialect/metadata`. +- [ ] MySQL-Versionsklassen (`MySql8Dialect`/`MySql57Dialect`) + Offline-Tests grün. +- [ ] `statement/demo`-Integrationstests laufen; `impl`/`importer`-Tests grün. +- [ ] Lizenz-Header-/Javadoc-CI in beiden Repos grün. + +**Wenn alle Haken sitzen: fertig.** Split + Dialekt-Versionen sind umgesetzt und alles läuft. + +--- + +## Anhang — Wenn ein Build rot wird (Junior-Hilfe) + +| Symptom | Wahrscheinliche Ursache | Fix | +|---|---|---| +| `cannot find symbol: class PkBuilder` in `MySqlDialect` | Helfer gelöscht, aber noch referenziert | Referenzen prüfen; Helfer gehört jetzt in `MySqlMetadataProvider` | +| `package org.eclipse.daanse.jdbc.db.api.schema does not exist` (SQL-Seite) | Import nicht umbenannt | `sed`-Ersetzung auf `sql.api.schema` erneut laufen lassen | +| `record`/`api.meta` immer noch im Dialekt-Import | Rolle-A-Methode übersehen | `grep` aus Checkpoint 1; restliche Methode verschieben | +| `jdbc.db` findet `sql.api` nicht | SQL nicht installiert | im SQL-Repo `mvn -q clean install`, **dann** `jdbc.db` | +| OSGi: Provider zur Laufzeit `EMPTY` | `@DialectName` fehlt/falsch am Begleiter | `@DialectName("MYSQL")` prüfen (gleicher Wert wie Factory) | +| Zyklus-/Reactor-Fehler | Modul in falscher Reihenfolge/`` fehlt | Reihenfolge R→S→D einhalten; Aggregator-`` prüfen | diff --git a/docs/dialect-migration/minimal-split/README.md b/docs/dialect-migration/minimal-split/README.md new file mode 100644 index 0000000..efd765b --- /dev/null +++ b/docs/dialect-migration/minimal-split/README.md @@ -0,0 +1,140 @@ + + +# Vollständiger Plan: Minimaler Schnitt (Rollen-Split) + +**Status:** Plan / Spezifikation (Code-Umzug noch nicht ausgeführt) +**Verhältnis:** Umsetzung von **Option O1 (+ optional O2)** aus der 2. Analyse +([../09-minimal-split-analysis.md](../09-minimal-split-analysis.md)). Alternative zum Wholesale-Plan +([../README.md](../README.md)). + +Dieser Ordner ist der **ausführbare Master-Plan** für den *minimalen* Schnitt: statt `api`+`record` +komplett zu verschieben (Wholesale, Plan 1), wird die **Metadaten-Ausliefer-Rolle aus den Dialekten +herausgelöst**, sodass der SQL-Kern minimal wird und `record` + `api.meta` + der Introspektions-Motor +in `jdbc.db` bleiben. + +--- + +## 1. Kernidee + +Ein konkreter Dialekt trägt heute **zwei Rollen** (Beleg: [../09](../09-minimal-split-analysis.md)): + +- **Rolle A – Nativer Metadaten-Provider:** `Dialect extends MetadataProvider`; die 6 großen + Dialekte überschreiben je 14–21 Methoden (`getAllIndexInfo`, `getAllPrimaryKeys`, …), die + `information_schema` lesen und `record.schema`-Objekte bauen. → **Introspektion, gehört zu `jdbc.db`.** +- **Rolle B – SQL/DDL-Generierung:** Quoting, Generatoren, Capabilities, DDL/DML aus dem + `api.schema`-Vokabular. → **gehört zu `sql`.** + +**Entscheidender Hebel:** `DatabaseServiceImpl` (der Introspektions-Motor) konsumiert +`MetadataProvider` bereits als **Parameter** (`createMetaInfo(Connection, MetadataProvider)`, +`getCatalogs(Connection, MetadataProvider)` → ruft `provider.getAllIndexInfo(...)`). Er kennt **nicht** +den `Dialect`. Die Rollen sind also faktisch schon getrennt — nur die Vererbung +`Dialect extends MetadataProvider` verschweißt sie. + +**Der Plan:** Vererbung auflösen; Rolle-A-Methoden je Dialekt in eine eigenständige Begleiter-Klasse +`MetadataProvider implements MetadataProvider` verschieben, die in `jdbc.db` bleibt; den +schlanken SQL-Generierungs-Dialekt nach `sql`. + +--- + +## 2. Ergebnis: was zieht um / was bleibt + +**Nach `org.eclipse.daanse.sql` (minimaler Kern):** + +| Nach SQL | Inhalt | +|---|---| +| `sql.api` (schema/type/sql) | `api.schema.*` (geschlossene `sealed`-Insel), `api.type.*`, `api.sql.*` | +| `sql.dialect.api` | Dialekt-API **ohne** `extends MetadataProvider`; Generatoren, Capabilities, Quoter, TypeMapper | +| `sql.dialect.db.*` | Die 8 aktiven Dialekte **ohne** die Rolle-A-Methoden | + +**Bleibt in `org.eclipse.daanse.jdbc.db`:** + +| Bleibt | Inhalt | +|---|---| +| `jdbc.db.api` (Rest) | `api.meta.*`, `MetadataProvider`, `DatabaseService`, `MetaDataQueries`, `SnapshotBuilder` | +| `jdbc.db.record` | **komplett** (`record.schema` + `record.meta`) | +| `jdbc.db.impl` | Introspektions-Motor | +| `jdbc.db.dialect.metadata` (**neu**) | `MetadataProvider`-Begleiter (die herausgelösten Rolle-A-Methoden) | +| `jdbc.db.importer` | ETL | + +`api.type`/`api.sql` gehen immer nach SQL; `impl`/`importer` bleiben immer. Ergebnis: **Einweg +`jdbc.db → sql`**, kein Zyklus. + +> Vergleich der Größen: Gegenüber Wholesale (Plan 1) bleiben hier **`record` komplett**, **`api.meta`** +> und die Introspektions-Interfaces in `jdbc.db`. Nur `api.schema`+`type`+`sql` + Dialekte wandern. + +--- + +## 3. Zielstruktur im SQL-Repo + +``` +sql/ + api/ (neu — leaf: nur schema/type/sql) + dialect/ (neu — aggregator) + api/ (leaf — ohne MetadataProvider-Vererbung) + db/ (aggregator) + common/ test-support/ duckdb/ h2/ mariadb/ mssqlserver/ mysql/ oracle/ postgresql/ sqlite/ + deparser/ guard/ statement/ (bestehend — auf reaktor-intern umstellen) +``` + +Neu in `jdbc.db`: +``` +jdbc.db/ + dialect/metadata/ (neu — MetadataProvider-Begleiter, OSGi-Komponenten) + api/ record/ impl/ importer/ (bleiben; api gesplittet → siehe 02) +``` + +--- + +## 4. Phasen + +1. **Phase R – Rollen-Trennung** ([01](01-role-separation.md)): `Dialect extends MetadataProvider` + auflösen; je Dialekt Rolle-A-Methoden in `MetadataProvider` extrahieren; Aufrufer umstellen. + *Innerhalb `jdbc.db`, noch kein Repo-Umzug.* +2. **Phase S – API-Split** ([02](02-target-structure.md)): `api` in SQL-Modell (schema/type/sql) und + Introspektions-Rest (meta/top-level) trennen; Paket-Rename der SQL-Seite. +3. **Phase D – Dialekt-Umzug** ([03](03-migration-procedure.md)): `dialect.api` + aktive `dialect.db.*` + nach SQL (jetzt schlank, ohne Rolle A). +4. **Phase C – Konsumenten** ([04](04-consumer-rewiring.md)): SQL- und jdbc.db-Konsumenten + OSGi neu. +5. **Phase V – Per-Version-Härtung** (*optional*, [05](05-per-version-hardening.md)): O2. +6. **Phase B – Build/Verifikation** ([06](06-build-and-verification.md)): Java 21, Poms, Tests. + +Reihenfolge ist wichtig: **R vor S vor D** — erst entkoppeln (innerhalb `jdbc.db`, grün baubar), dann +splitten, dann verschieben. + +--- + +## 5. Detaildokumente + +| # | Dokument | Inhalt | +|---|---|---| +| 01 | [Rollen-Trennung](01-role-separation.md) | Der Kern-Refactor: `Dialect` ↔ `MetadataProvider` trennen, Begleiter-Klassen, Aufrufer | +| 02 | [Zielstruktur & API-Split](02-target-structure.md) | `api`-Split, Paket-/Modul-Layout, Rename-Mapping | +| 03 | [Migrationsprozedur](03-migration-procedure.md) | Schritt-für-Schritt, Wellen, Reihenfolge | +| 04 | [Konsumenten & OSGi](04-consumer-rewiring.md) | SQL-Konsumenten, `impl`/`importer`, OSGi-Verdrahtung | +| 05 | [Per-Version-Härtung (optional)](05-per-version-hardening.md) | O2: statische Pro-Version-Dialekte | +| 06 | [Build & Verifikation](06-build-and-verification.md) | Java 21, Poms, Testumfang, Risiken, DoD | +| ⭐ | [**Umsetzungs-Anleitung (Schritt für Schritt)**](IMPLEMENTATION-GUIDE.md) | Junior-tauglicher linearer Pfad mit Code-Beispielen: Split **und** Dialekt-Versionen, am Ende lauffähig | + +--- + +## 6. Definition of Done + +**Refactor (Phase R, in `jdbc.db` allein prüfbar):** +- [ ] `Dialect` erbt nicht mehr von `MetadataProvider`; `mvn -q verify` in `jdbc.db` grün. +- [ ] Für jeden der 6 großen Dialekte existiert `MetadataProvider` mit den extrahierten Methoden. +- [ ] `DatabaseServiceImpl`-Aufrufer erhalten den Begleiter-Provider (oder `MetadataProvider.EMPTY`). +- [ ] Round-Trip-/MetaInfo-Tests grün gegen die Begleiter. + +**Split & Umzug (Phasen S–B):** +- [ ] SQL-Repo baut mit `sql.api` (nur schema/type/sql) + `sql.dialect.*`, **ohne** `record`/`api.meta`. +- [ ] `jdbc.db` baut gegen publizierte `sql.*`-Snapshots; `record`, `api.meta`, `impl`, `importer`, + `dialect.metadata` bleiben dort. +- [ ] Einweg-Abhängigkeit `jdbc.db → sql` (kein `jdbc.db`-Dep im SQL-Repo). +- [ ] Dialekt-Main im SQL-Repo importiert **kein** `record` und **kein** `api.meta` mehr. +- [ ] `statement/demo`-Integrationstests (H2/MSSQL) grün. diff --git a/docs/dialect-migration/ru/01-inventory-and-scope.md b/docs/dialect-migration/ru/01-inventory-and-scope.md new file mode 100644 index 0000000..c92223c --- /dev/null +++ b/docs/dialect-migration/ru/01-inventory-and-scope.md @@ -0,0 +1,110 @@ + + +# 01 – Инвентаризация и объём + +Назад к [мастер-плану](README.md). + +Этот документ перечисляет **все** модули обоих репозиториев, классифицирует их и точно очерчивает +объём текущей миграции. + +--- + +## 1. Репозиторий `org.eclipse.daanse.jdbc.db` (источник) + +- Сборка: многомодульный Maven, родитель `org.eclipse.daanse:org.eclipse.daanse.pom.parent:0.0.7` +- Group: `org.eclipse.daanse`, версия `${revision}` = `0.0.1-SNAPSHOT` +- **Java: 21** +- Метаданные OSGi через `bnd-maven-plugin` (от родителя) + аннотации DS (`@Component`); **нет** файлов + `bnd.bnd` и **нет** `META-INF/services`. + +Модули верхнего уровня (5): `api`, `record`, `impl`, `dialect`, `importer`. + +### 1.1 Классификация + +| Модуль | Корень пакета | Класс | Переезд? | +|---|---|---|---| +| `api` | `org.eclipse.daanse.jdbc.db.api` (`.meta/.schema/.sql/.type`) | Модель метаданных/схемы/типов/SQL (интерфейсы) | **→ sql** | +| `record` | `org.eclipse.daanse.jdbc.db.record` (`.meta/.schema`) | Record-реализации интерфейсов `api` | **→ sql** | +| `dialect/api` | `org.eclipse.daanse.jdbc.db.dialect.api` (`.capability/.generator/.type`) | API диалектов | **→ sql** | +| `dialect/db/common` | `…dialect.db.common` | База диалектов (`AbstractJdbcDialect`, `AnsiDialect`, Jdbc*) | **→ sql** | +| `dialect/db/test-support` | `…dialect.db.testsupport` | Тестовые помощники (`GeneratorTestSupport`, `RoundTripAssertions`) | **→ sql** | +| `dialect/db/` (активный) | `…dialect.db.` | Конкретные диалекты | **→ sql** (подмножество, см. §2) | +| `dialect/db/` (неактивный) | `…dialect.db.` | Конкретные диалекты | остаётся (поздняя волна) | +| `dialect/db/configurable` | `…dialect.db.configurable` | Настраиваемый в рантайме диалект | остаётся (неактивен) | +| `impl` | `org.eclipse.daanse.jdbc.db.impl` | Движок метаданных (`DatabaseServiceImpl`) — **не ETL** | **остаётся** (станет потребителем sql) | +| `importer` + `importer/csv` | `…importer.csv.api/.impl` | **ETL** (импорт CSV) | **остаётся** (ограничение) | + +> **Почему `impl` остаётся, хотя это не ETL:** `impl` — движок JDBC-`DatabaseMetaData`, +> реализующий рантайм-интроспекцию. После переезда он становится чистым нижестоящим потребителем +> `sql` (см. [02](02-dependency-and-layering.md)). Его сохранение не создаёт цикла, поскольку после +> переезда `sql` больше не зависит от `jdbc.db`. + +--- + +## 2. Объём диалектов: активное подмножество + +Реактор `dialect/db/pom.xml` сейчас активирует **10** модулей; все прочие (~27) лежат на диске +закомментированными. + +**Активные (мигрируют сейчас):** + +``` +common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite +``` + +- `common` и `test-support` — инфраструктура и предпосылка для всех диалектов. +- Единственное меж-диалектное ребро внутри подмножества: **`mariadb → mysql`** (оба активны → + подмножество замкнуто). Все остальные активные диалекты зависят только от `common` + `api`. +- Диалекты, потребляемые SQL-репозиторием сегодня (`common`, `h2`, `mysql`, `mssqlserver`), + полностью включены. + +**Неактивные (пока остаются, поздняя волна):** + +``` +configurable access clickhouse db2 derby firebird googlebigquery greenplum hive hsqldb +impala infobright informix ingres interbase luciddb monetdb neoview netezza nuodb +opensearch pdidataservice redshift snowflake sqlstream sybase teradata vectorwise vertica +``` + +> **Внимание — redshift/snowflake:** у этих двух модулей имя каталога и содержащийся пакет/класс +> перекрещены (см. [07](07-anomalies-and-risks.md)). Оба **не** входят в активное подмножество, поэтому +> сейчас не затрагиваются; при последующем переносе обязательно исправить. + +**Полный список диалектов (все 37 конкретных модулей БД):** access, clickhouse, db2 (два класса +диалекта: `Db2Dialect`, `Db2OldAs400Dialect`), derby, duckdb, firebird, googlebigquery, greenplum, +h2, hive, hsqldb, impala, infobright, informix, ingres, interbase, luciddb, mariadb, monetdb, +mssqlserver, mysql, neoview, netezza, nuodb, opensearch, oracle, pdidataservice, postgresql, +redshift, snowflake, sqlite, sqlstream, sybase, teradata, vectorwise, vertica. Плюс `AnsiDialect` +(в `common`) и `ConfigurableDialect` (в `configurable`). + +--- + +## 3. Репозиторий `org.eclipse.daanse.sql` (цель) + +- Сборка: многомодульный Maven, тот же родитель `pom.parent:0.0.7`, group `org.eclipse.daanse`, + `${revision}` = `0.0.1-SNAPSHOT`. **Java: 17** (повышается до 21). +- Существующие группы модулей: + +| Группа | Leaf-модули | Роль | +|---|---|---| +| `deparser` | `api`, `jsqlparser` | Генерация SQL с учётом диалекта из разобранных инструкций | +| `guard` | `api`, `jsqltranspiler` | Безопасность/валидация SQL | +| `statement` | `api`, `impl`, `demo` | Построитель запросов с учётом диалекта, независимый от домена | + +Эти группы сегодня **потребляют** внешние артефакты `jdbc.db.dialect.*` и `jdbc.db.api`; после +миграции они переключаются на внутренние связи реактора (см. [05](05-consumer-rewiring.md)). + +--- + +## 4. Контрольный список объёма + +- [ ] Каждый из 5 модулей верхнего уровня `jdbc.db` отнесён ровно к одной категории (переезд | остаётся). +- [ ] Активное подмножество диалектов = ровно 10 модулей, перечисленных в реакторе. +- [ ] Неактивные диалекты явно помечены как поздняя волна. +- [ ] ETL (`importer/csv`) явно исключён. diff --git a/docs/dialect-migration/ru/02-dependency-and-layering.md b/docs/dialect-migration/ru/02-dependency-and-layering.md new file mode 100644 index 0000000..4529f35 --- /dev/null +++ b/docs/dialect-migration/ru/02-dependency-and-layering.md @@ -0,0 +1,118 @@ + + +# 02 – Анализ зависимостей и слоистости + +Назад к [мастер-плану](README.md). + +Этот документ обосновывает, **почему** `api` + `record` должны переехать вместе, и показывает, как +это предотвращает циклическую зависимость между репозиториями. + +--- + +## 1. Текущие зависимости (внутри `jdbc.db`) + +Рёбра из pom-файлов и Java-импортов (проверено): + +``` +dialect.api → api (compile; использует api.schema.*, api.sql.*, api.type.*) +dialect.db.common → dialect.api (compile) +dialect.db. → dialect.db.common, dialect.api (compile) +dialect.db. → record (compile; record-реализации схемы, напр. h2/mysql/oracle/…) +dialect.db. → impl (TEST; round-trip-тесты, напр. mysql/mariadb/mssql/oracle/postgresql/derby) +test-support → api, dialect.api (compile) + +impl → api, record, dialect.api (compile) +impl → dialect.db.common, dialect.db.h2 (TEST) +importer.csv → api, record, dialect.api (compile), → impl (runtime) [ETL] + +record → api (compile) +``` + +Меж-диалектные рёбра (compile), важные для порядка: + +``` +mariadb → mysql infobright → mysql +greenplum → postgresql netezza → postgresql (модуль snowflake: код Redshift) → postgresql +impala → hive vectorwise → ingres sqlstream → luciddb +``` + +Внутри **активного подмножества** существует только `mariadb → mysql`. + +--- + +## 2. Проблема цикличности + +Если перенести **только** поддерево `dialect`, оставив `api`/`record` в `jdbc.db`, на **уровне +репозиториев** возникает цикл: + +``` +sql/dialect.api → jdbc.db/api (диалектам нужна модель метаданных/типов) +jdbc.db/impl → sql/dialect.api (движку метаданных нужен API диалектов) +jdbc.db/importer.csv → sql/dialect.api (ETL нужен API диалектов) +``` + +→ `sql` зависел бы от `jdbc.db` **и** `jdbc.db` от `sql`. Это ломает порядок релизов (ни один из +снапшотов нельзя собрать/опубликовать независимо) и нежелательно. + +--- + +## 3. Решение: `api` + `record` переезжают вместе + +Поскольку модель метаданных/типов (`api`) и её record-реализации (`record`) — **единственные** +кирпичики `jdbc.db`, от которых зависит код диалектов, они **переносятся вместе**. После этого: + +``` +sql/api → (только сторонние библиотеки / JDK) # нет ребра к jdbc.db +sql/record → sql/api +sql/dialect.api → sql/api +sql/dialect.db.* → sql/dialect.api, sql/dialect.db.common, sql/record + +jdbc.db/impl → sql/api, sql/record, sql/dialect.api (compile) # односторонне +jdbc.db/importer → sql/api, sql/record, sql/dialect.api (compile) # односторонне +``` + +**Итог:** `sql` самодостаточен (больше нет зависимости от `jdbc.db`); `jdbc.db` зависит только +**односторонне** от `sql`. Чистая слоистость: + +``` + ┌─────────────────────────────────────┐ + │ org.eclipse.daanse.jdbc.db │ (сверху: JDBC-рантайм + ETL) + │ impl (движок метаданных) │ + │ importer/csv (ETL) │ + └───────────────┬─────────────────────┘ + │ зависит (односторонне) + ┌───────────────▼─────────────────────┐ + │ org.eclipse.daanse.sql │ (снизу: фундамент SQL/диалектов) + │ dialect.db.* → dialect.api │ + │ record → api │ + │ guard / deparser / statement │ + └─────────────────────────────────────┘ +``` + +--- + +## 4. Внешние (сторонние) зависимости, переезжающие вместе + +- `org.slf4j:slf4j-api` — compile, в `common` и большинстве диалектов. +- `biz.aQute.bnd:biz.aQute.bndlib` — compile, только `oracle`. +- Пер-диалектные **JDBC-драйверы + Testcontainers** — сплошь **test-scope** (напр., `mysql-connector-j`, + `ojdbc11`, `testcontainers` mysql/oracle-xe/junit-jupiter, `mockito`, `assertj`). Эти версии нужно + перенести в `dependencyManagement` SQL-репозитория (см. [06](06-build-and-java21.md)). + +Сами `api`/`record` **не** имеют сторонних рантайм-зависимостей, кроме JDK (и, возможно, slf4j). + +--- + +## 5. Последствия для оставшихся модулей `jdbc.db` + +- `impl`: продакшн-код (`DatabaseServiceImpl`, `CachingDatabaseService`) **не** импортирует + диалекты, только `api`/`record` + `dialect.api`. После переезда эти зависимости указывают на + `sql.*`. Round-trip-**тесты** (`impl/sqlgen`) дополнительно требуют `sql.dialect.db.common/h2` (test). +- `importer/csv` (ETL): compile на `sql.api`/`sql.record`/`sql.dialect.api`, runtime на `impl`. +- Порядок релизов: сначала собрать/опубликовать **`sql`**, затем `jdbc.db`. diff --git a/docs/dialect-migration/ru/03-package-rename-map.md b/docs/dialect-migration/ru/03-package-rename-map.md new file mode 100644 index 0000000..f5cd827 --- /dev/null +++ b/docs/dialect-migration/ru/03-package-rename-map.md @@ -0,0 +1,94 @@ + + +# 03 – Переименование пакетов и артефактов + +Назад к [мастер-плану](README.md). + +Все переносимые модули переименовываются с `org.eclipse.daanse.jdbc.db.*` на +`org.eclipse.daanse.sql.*` — одинаково Java-пакеты, Maven-artifactId и OSGi-`Bundle-SymbolicName`. + +--- + +## 1. Сопоставление Java-пакетов + +| Старое | Новое | +|---|---| +| `org.eclipse.daanse.jdbc.db.api` | `org.eclipse.daanse.sql.api` | +| `org.eclipse.daanse.jdbc.db.api.meta` | `org.eclipse.daanse.sql.api.meta` | +| `org.eclipse.daanse.jdbc.db.api.schema` | `org.eclipse.daanse.sql.api.schema` | +| `org.eclipse.daanse.jdbc.db.api.sql` | `org.eclipse.daanse.sql.api.sql` | +| `org.eclipse.daanse.jdbc.db.api.type` | `org.eclipse.daanse.sql.api.type` | +| `org.eclipse.daanse.jdbc.db.record.meta` | `org.eclipse.daanse.sql.record.meta` | +| `org.eclipse.daanse.jdbc.db.record.schema` | `org.eclipse.daanse.sql.record.schema` | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | +| `org.eclipse.daanse.jdbc.db.dialect.api.capability` | `org.eclipse.daanse.sql.dialect.api.capability` | +| `org.eclipse.daanse.jdbc.db.dialect.api.generator` | `org.eclipse.daanse.sql.dialect.api.generator` | +| `org.eclipse.daanse.jdbc.db.dialect.api.type` | `org.eclipse.daanse.sql.dialect.api.type` | +| `org.eclipse.daanse.jdbc.db.dialect.db.common` | `org.eclipse.daanse.sql.dialect.db.common` | +| `org.eclipse.daanse.jdbc.db.dialect.db.testsupport` | `org.eclipse.daanse.sql.dialect.db.testsupport` | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | + +Правило: **префикс `org.eclipse.daanse.jdbc.db` → `org.eclipse.daanse.sql`**, суффикс без изменений. +Затрагивает объявления `package`, инструкции `import` и `package-info.java` в **каждом** переносимом +файле — включая потребителей (см. [05](05-consumer-rewiring.md)). + +> Примечание: пакет `api.meta` отражает интроспекцию JDBC-`DatabaseMetaData` и потому семантически +> близок к JDBC. Ради согласованности в SQL-репозитории он всё же переименовывается в `sql.api.meta` +> (единый префикс). Более тонкое разделение возможно позже, но **не** является частью этой миграции. + +--- + +## 2. Сопоставление Maven-artifactId + +ArtifactId точно отражает корень пакета (соглашение репозитория). + +| Старое | Новое | +|---|---| +| `org.eclipse.daanse.jdbc.db.api` | `org.eclipse.daanse.sql.api` | +| `org.eclipse.daanse.jdbc.db.record` | `org.eclipse.daanse.sql.record` | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | +| `org.eclipse.daanse.jdbc.db.dialect.db.common` | `org.eclipse.daanse.sql.dialect.db.common` | +| `org.eclipse.daanse.jdbc.db.dialect.db.test-support` | `org.eclipse.daanse.sql.dialect.db.test-support` | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | + +Новые агрегаторы получают: +- `org.eclipse.daanse.sql.dialect` (агрегатор, заменяет `…jdbc.db.dialect`) +- `org.eclipse.daanse.sql.dialect.db` (агрегатор, заменяет `…jdbc.db.dialect.db`) + +Group-Id остаётся `org.eclipse.daanse`; версия остаётся `${revision}` = `0.0.1-SNAPSHOT`. + +--- + +## 3. OSGi / DS + +- **`Bundle-SymbolicName`** выводится плагином `bnd-maven-plugin` из artifactId → меняется + автоматически вместе с artifactId. Ручное сопровождение `bnd.bnd` не требуется (их нет). +- **DS-компоненты:** каждый диалект экспортирует `@Component(service = DialectFactory.class, …)`. + Полностью квалифицированный тип `DialectFactory` меняется при переименовании пакета; сама аннотация + без изменений, генерируемый `OSGI-INF/*.xml` последует автоматически при пересборке. +- **Файлов `META-INF/services` нет** — регистрацию ServiceLoader править не нужно. + +--- + +## 4. Лицензионный заголовок и package-info + +- Каждый файл несёт заголовок EPL-2.0 (участники «SmartCity Jena», «Stefan Bischof (bipolis.org)»). + Заголовок сохраняется при переезде; год копирайта дополнять лишь при содержательном изменении. +- В каждом leaf-пакете есть `package-info.java` — имя пакета в нём тоже переименовать. +- CI требует заголовки (`.licenserc.yaml`, `license.templates`) и Javadoc + (`java_check_javadoc.yml`) — после переименования эти проверки должны остаться зелёными. + +--- + +## 5. Механика (рекомендуется) + +1. `git mv` каталогов модулей (сохранение истории). +2. Переименование каталогов путей пакетов (`.../jdbc/db/...` → `.../sql/...`). +3. Текстовая замена `org.eclipse.daanse.jdbc.db` → `org.eclipse.daanse.sql` **только** в переносимых + модулях и их потребителях. Затем помодульно `mvn -q -pl -am verify`. diff --git a/docs/dialect-migration/ru/04-migration-procedure.md b/docs/dialect-migration/ru/04-migration-procedure.md new file mode 100644 index 0000000..cf4be0d --- /dev/null +++ b/docs/dialect-migration/ru/04-migration-procedure.md @@ -0,0 +1,112 @@ + + +# 04 – Процедура миграции (пошагово) + +Назад к [мастер-плану](README.md). + +Помодульный переезд в **порядке зависимостей**. Каждый шаг завершается зелёной сборкой до начала +следующего. + +--- + +## 0. Подготовка + +- Создать рабочую ветку в **обоих** репозиториях (например, `feat/dialect-migration`). +- Оба репозитория лежат рядом в `…/git/daanse/`. +- Для сохранения истории использовать `git mv`; как альтернатива — `git filter-repo`, если нужна + полная файловая история через границу репозиториев (опционально, см. [07](07-anomalies-and-risks.md)). + +--- + +## 1. Волна A — фундамент (`api`, `record`) + +Для **`api`** (затем аналогично `record`): + +1. `git mv jdbc.db/api sql/api` (каталог в реактор SQL). +2. Переместить пути пакетов `src/main/java/org/eclipse/daanse/jdbc/db/api/…` → `…/sql/api/…`. +3. Во всех файлах заменить `org.eclipse.daanse.jdbc.db.api` → `org.eclipse.daanse.sql.api` + (`package`, `import`, `package-info`). +4. `api/pom.xml`: установить родителя на корень SQL, artifactId → `org.eclipse.daanse.sql.api`. +5. Корень `sql/pom.xml`: добавить `api`. +6. `record` точно так же; зависимость `record/pom.xml` `…jdbc.db.api` → `…sql.api` (внутри реактора, + `${project.version}`). +7. `mvn -q -pl api,record -am verify`. + +--- + +## 2. Волна B — ядро диалектов + +Порядок: **`dialect/api` → `dialect/db/common` → `dialect/db/test-support`.** + +1. Создать новые агрегаторы: `sql/dialect/pom.xml` (packaging=pom, родитель=корень SQL) и + `sql/dialect/db/pom.xml` (packaging=pom, родитель=`sql.dialect`). +2. `git mv jdbc.db/dialect/api sql/dialect/api`; переименование `…jdbc.db.dialect.api` → + `…sql.dialect.api`; зависимость pom `…jdbc.db.api` → внутри реактора `…sql.api`. +3. `git mv jdbc.db/dialect/db/common sql/dialect/db/common`; переименование; зависимость → + `sql.dialect.api`. +4. `git mv jdbc.db/dialect/db/test-support sql/dialect/db/test-support`; переименование; зависимости → + `sql.api`, `sql.dialect.api`. +5. Внести `sql.dialect` в корневой ``; `common`/`test-support` в `sql/dialect/db/pom.xml`. +6. `mvn -q -pl org.eclipse.daanse.sql.dialect.api,…common,…test-support -am verify`. + +--- + +## 3. Волна C — конкретные диалекты (активное подмножество) + +Для каждого из `duckdb, h2, mariadb, mssqlserver, mysql, oracle, postgresql, sqlite` +(**`mysql` перед `mariadb`**, т. к. `mariadb → mysql`): + +1. `git mv jdbc.db/dialect/db/ sql/dialect/db/`. +2. Переименование пакета `…jdbc.db.dialect.db.` → `…sql.dialect.db.`. +3. Переключить зависимости `pom.xml`: + - compile: `…dialect.db.common` → `sql.dialect.db.common`, `…dialect.api` → `sql.dialect.api`, + `…record` → `sql.record`, `…api` → `sql.api` (все внутри реактора, `${project.version}`). + - test: `…jdbc.db.impl` → см. ниже (кросс-репо!), `…dialect.db.test-support` → + `sql.dialect.db.test-support`; версии JDBC-драйверов/Testcontainers перенести из исходного pom. +4. Внести `` в `sql/dialect/db/pom.xml`. +5. `mvn -q -pl org.eclipse.daanse.sql.dialect.db. -am verify`. + +> **Тестовое ребро `dialect.db. → jdbc.db.impl`:** некоторые тесты диалектов (mysql, mariadb, +> mssqlserver, oracle, postgresql, derby) используют `DatabaseServiceImpl`. Поскольку `impl` остаётся +> в `jdbc.db`, это создало бы **кросс-репо тестовую зависимость** `sql → jdbc.db` и через чёрный ход +> вернуло бы цикл, устранённый в [02](02-dependency-and-layering.md). **Указание:** эти round-trip-тесты +> **не** переезжают вместе; их либо (a) переписывают на собственные тест-фикстуры `sql`, либо +> (b) временно помечают `@Disabled` и продолжают как интеграционные тесты в `jdbc.db` (где лежит +> `impl`). Решение по каждому диалекту фиксировать в [08](08-verification.md). В активном подмножестве +> это касается mysql, mariadb, mssqlserver, oracle, postgresql. + +--- + +## 4. Волна D — переключение потребителей SQL + +См. [05](05-consumer-rewiring.md) §1: переключить `guard`, `deparser`, `statement`, `statement/demo` +с внешних зависимостей `jdbc.db.*` на новые внутренние модули `sql.*` реактора, поправить импорты. +Повышение корня SQL до Java 21 (см. [06](06-build-and-java21.md)). Затем `mvn -q verify` для всего +реактора SQL. + +--- + +## 5. Волна E — демонтаж `jdbc.db` + +См. [05](05-consumer-rewiring.md) §2: в `jdbc.db` удалить переехавшие модули из реактора +(`api`, `record`, `dialect` из корневого ``) и переключить `impl`/`importer` на +**опубликованные** снапшоты `sql.*`. `mvn -q verify` реактора `jdbc.db` (предполагает, что снапшоты +`sql` доступны в локальном/Sonatype-репозитории → сначала `mvn install` для `sql`). + +--- + +## 6. Краткая форма порядка + +``` +A: api → record +B: dialect.api → dialect.db.common → test-support +C: mysql → mariadb; duckdb, h2, mssqlserver, oracle, postgresql, sqlite (параллельно) +D: потребители SQL (guard/deparser/statement) + Java 21 +E: демонтаж jdbc.db (impl/importer на снапшоты sql.*) +``` diff --git a/docs/dialect-migration/ru/05-consumer-rewiring.md b/docs/dialect-migration/ru/05-consumer-rewiring.md new file mode 100644 index 0000000..76caec9 --- /dev/null +++ b/docs/dialect-migration/ru/05-consumer-rewiring.md @@ -0,0 +1,102 @@ + + +# 05 – Перепривязка потребителей + +Назад к [мастер-плану](README.md). + +После переезда все модули, использующие диалекты / `api` / `record`, должны обновить свои +зависимости и импорты — в SQL-репозитории на **внутренние реактора**, в jdbc.db — на +**опубликованные снапшоты `sql.*`**. + +--- + +## 1. Потребители в SQL-репозитории (внутри реактора) + +Затронутые модули и их сегодняшние внешние зависимости: + +| Модуль | Сегодняшние внешние зависимости (`jdbc.db.*`) | Новые (внутри реактора `sql.*`) | +|---|---|---| +| `statement/impl` | `dialect.db.common`, `dialect.db.mysql` | `sql.dialect.db.common`, `sql.dialect.db.mysql` | +| `statement/demo` | `dialect.db.common` (`AnsiDialect`), `dialect.db.mysql`, `dialect.db.mssqlserver`, H2 | `sql.dialect.db.common/mysql/mssqlserver` | +| `guard/jsqltranspiler` | `dialect.db.h2`, `dialect.db.common`, `deparser` | `sql.dialect.db.h2`, `sql.dialect.db.common` | +| `deparser/api` | `dialect.api` | `sql.dialect.api` | +| `deparser/jsqlparser` | `dialect.api` | `sql.dialect.api` | +| `statement/api` | `dialect.api` (`generator.KnownFunction`, `generator.StatementHint`), `api.schema.*`, `api.type.BestFitColumnType` | `sql.dialect.api.generator.*`, `sql.api.schema.*`, `sql.api.type.*` | + +**Изменение pom** (пример `statement/impl`): заменить внешние координаты с фиксированной версией на +внутренние реактора с `${project.version}`: + +```xml + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.db.common + 0.0.1-SNAPSHOT + + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${project.version} + +``` + +**Изменение импортов** (во всех `.java` потребителей): `org.eclipse.daanse.jdbc.db.dialect` → +`org.eclipse.daanse.sql.dialect`, `org.eclipse.daanse.jdbc.db.api` → `org.eclipse.daanse.sql.api`. +Затронутые классы по результатам анализа, среди прочего: +`DialectSqlRenderer`, `BasicDialect*DeParser`, `DialectDeparser`, `Expressions`, `SqlExpression`, +`SelectStatement(Builder)`, `TranspilerSqlGuard(Factory)`, `DeparserColumResolver`. + +--- + +## 2. Потребители в jdbc.db (опубликованные снапшоты `sql.*`) + +После переезда `api`, `record`, `dialect.*` **больше не в реакторе jdbc.db**. Оставшиеся модули +получают их как внешние артефакты из SQL-репозитория. + +### 2.1 `impl` + +`impl/pom.xml` сегодня (compile): `…jdbc.db.api`, `…jdbc.db.record`, `…jdbc.db.dialect.api`; +(test): `…dialect.db.h2`, `…dialect.db.common`. → все на `…sql.*` (фиксированная `0.0.1-SNAPSHOT`): + +```xml +org.eclipse.daanse + org.eclipse.daanse.sql.api0.0.1-SNAPSHOT +org.eclipse.daanse + org.eclipse.daanse.sql.record0.0.1-SNAPSHOT +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api0.0.1-SNAPSHOT + +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h20.0.1-SNAPSHOTtest +org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common0.0.1-SNAPSHOTtest +``` + +Импорты в `DatabaseServiceImpl`, `CachingDatabaseService` и тестах соответственно на `sql.*`. + +### 2.2 `importer/csv` (ETL — остаётся) + +`importer/csv/pom.xml` сегодня (compile): `…jdbc.db.api`, `…jdbc.db.dialect.api`, `…jdbc.db.record`, +`…io.fs.watcher.api`, `de.siegmar:fastcsv`; (runtime): `…jdbc.db.impl`, `…io.fs.watcher.watchservice`. +→ `api`/`record`/`dialect.api` на `sql.*`; `fastcsv`, `io.fs.watcher.*`, `impl` без изменений. +Импорты в `CsvDataImporter` (использует `Dialect`, `DialectFactory`) на `sql.dialect.api`. + +### 2.3 Очистка реактора + +`jdbc.db/pom.xml`: удалить из ``: `api`, `record`, `dialect`. Остаются: `impl`, `importer`. +(Неактивные диалекты и так были закомментированы и переедут в более поздней волне; до тех пор их +каталоги остаются в `jdbc.db/dialect/db/`.) + +--- + +## 3. Предусловие сборки + +`impl`/`importer` соберутся зелёными только когда доступны снапшоты `sql.*` → в SQL-репозитории +сначала `mvn -q install`, затем собирать `jdbc.db`. См. [08](08-verification.md). diff --git a/docs/dialect-migration/ru/06-build-and-java21.md b/docs/dialect-migration/ru/06-build-and-java21.md new file mode 100644 index 0000000..008fb20 --- /dev/null +++ b/docs/dialect-migration/ru/06-build-and-java21.md @@ -0,0 +1,117 @@ + + +# 06 – Сборка, реактор и повышение до Java 21 + +Назад к [мастер-плану](README.md). + +--- + +## 1. Повышение версии Java (17 → 21) + +SQL-репозиторий сегодня собирается с Java 17 (`sql/pom.xml`), а код диалектов происходит из репозитория +на Java 21. В корневом pom SQL: + +```xml + + ... + 21 + 21 + ${java.version} + ${java.version} + +``` + +Эффект: весь SQL-репозиторий компилируется/тестируется на 21. Toolchain/CI должны предоставлять JDK 21 +(проверить `.github/workflows/build_deploy.yml`; проверку `java_check_javadoc.yml` также на 21). + +--- + +## 2. Новые pom-агрегаторы + +**`sql/dialect/pom.xml`** (заменяет `jdbc.db/dialect/pom.xml`): + +```xml + + org.eclipse.daanse + org.eclipse.daanse.sql + ${revision} + +org.eclipse.daanse.sql.dialect +pom + + api + db + +``` + +**`sql/dialect/db/pom.xml`** (packaging=pom, родитель `org.eclipse.daanse.sql.dialect`), ``: + +``` +common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite +``` + +**Корень `sql/pom.xml`** расширить ``: + +```xml + + api + record + dialect + guard + deparser + statement + +``` + +Порядок в `` некритичен (Maven сортирует по зависимостям), но для читаемости — фундамент +первым. + +--- + +## 3. Расширение dependencyManagement + +Перенести необходимые версии из мигрируемых модулей в корневой `dependencyManagement` SQL или в +соответствующие pom модулей (сегодня в корне SQL есть только slf4j/mockito/assertj): + +- **Тест-драйверы и контейнеры** (test-scope, пер-диалектно): `mysql-connector-j`, + `mariadb-java-client`, `mssql-jdbc`, `ojdbc11`, `postgresql`, `org.xerial:sqlite-jdbc`, + `org.duckdb:duckdb_jdbc`, `com.h2database:h2`; `org.testcontainers` (mysql, mariadb, mssqlserver, + oracle-xe, postgresql, junit-jupiter). +- **JUnit 5** (`junit-jupiter-api/engine`) — версии из исходных pom (напр., 5.12.2) унифицировать. +- **assertj** — в корне SQL 3.24.2, часть модулей jdbc.db использует 3.26.0 → унифицировать до одной + версии. +- **`biz.aQute.bnd:biz.aQute.bndlib`** — compile-зависимость `oracle`. + +Точные версии брать из соответствующих `dialect/db//pom.xml` источника; при сомнении выбирать +более новую и централизованно фиксировать в `dependencyManagement`. + +--- + +## 4. Родительский pom и снапшот-репозиторий (без изменений) + +- Родитель остаётся `org.eclipse.daanse:org.eclipse.daanse.pom.parent:0.0.7` (даёт плагины bnd/flatten). +- `${revision}` = `0.0.1-SNAPSHOT`, механика `.flattened-pom.xml` без изменений. +- Снапшот-репозиторий Sonatype `central.sonatype.com/repository/maven-snapshots` без изменений. +- OSGi: файлов `bnd.bnd` нет; управляется DS-`@Component` → `Bundle-SymbolicName` следует за + artifactId автоматически. + +--- + +## 5. Команды сборки + +```bash +# Полная сборка SQL-репозитория + локальная установка (предусловие для jdbc.db) +cd org.eclipse.daanse.sql && mvn -q clean install + +# Отдельный модуль вместе с зависимостями +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am verify + +# jdbc.db против свежеустановленных снапшотов sql.* +cd ../org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` diff --git a/docs/dialect-migration/ru/07-anomalies-and-risks.md b/docs/dialect-migration/ru/07-anomalies-and-risks.md new file mode 100644 index 0000000..afd4d2c --- /dev/null +++ b/docs/dialect-migration/ru/07-anomalies-and-risks.md @@ -0,0 +1,89 @@ + + +# 07 – Аномалии и риски + +Назад к [мастер-плану](README.md). + +--- + +## 1. Путаница redshift/snowflake (⚠ целостность данных) + +Два Maven-модуля `dialect/db/redshift` и `dialect/db/snowflake` заполнены **перекрёстно** +(проверено чтением объявлений `package`): + +| Каталог модуля / ArtifactId | Содержащийся пакет + класс | +|---|---| +| `dialect/db/redshift` (`…dialect.db.redshift`) | `…dialect.db.snowflake` · `SnowflakeDialect` / `SnowflakeDialectFactory` | +| `dialect/db/snowflake` (`…dialect.db.snowflake`) | `…dialect.db.redshift` · `RedshiftDialect` / `RedshiftDialectFactory` | + +- Оба модуля **неактивны** и **не** входят в текущее активное подмножество → сейчас действий **не** + требуется. +- **При последующем переносе обязательно исправить:** согласовать либо каталог/artifactId, либо + пакет/класс так, чтобы код Snowflake был в модуле `snowflake`, а код Redshift — в модуле `redshift`. + Чисто механический перенос по имени каталога закрепил бы неверное именование. +- Учесть дополнительное ребро: модуль `snowflake` (с кодом Redshift) зависит от `postgresql`. + +--- + +## 2. Java 17 → 21 + +- Код диалектов написан под Java 21; SQL-репозиторий повышается (см. [06](06-build-and-java21.md)). + Риск невелик, но CI/toolchain должны предоставлять JDK 21. +- Все существующие модули SQL (`guard`/`deparser`/`statement`) будут собираться на 21 — + известных препятствий нет, но регрессионный тест входит в приёмку. + +--- + +## 3. Кросс-репо тестовая зависимость `dialect.db. → jdbc.db.impl` + +Round-trip-тесты некоторых диалектов (mysql, mariadb, mssqlserver, oracle, postgresql, derby) +используют `DatabaseServiceImpl` из `impl`. Поскольку `impl` остаётся в `jdbc.db`, это ребро +**не** должно сохраниться как зависимость `sql → jdbc.db` (реактивировало бы устранённый цикл). +Варианты (фиксировать по каждому диалекту в [08](08-verification.md)): + +1. **Переписать** на собственные тест-фикстуры SQL-репозитория (предпочтительно, где посильно). +2. **Временно `@Disabled`** и продолжить интеграционные тесты в `jdbc.db` (при `impl`). + +--- + +## 4. Неактивные диалекты (поздняя волна) + +~27 модулей + `configurable` лежат закомментированными в `jdbc.db/dialect/db/`. Пока остаются там. +Риски при переносе: путаница redshift/snowflake (§1) и меж-диалектные цепочки +(`greenplum/netezza → postgresql`, `impala → hive`, `vectorwise → ingres`, `sqlstream → luciddb`, +`infobright → mysql`) — учитывать порядок. + +--- + +## 5. Сохранение истории (опционально) + +`git mv` сохраняет историю внутри одного репозитория. Для **полной** файловой истории через границу +репозиториев (jdbc.db → sql) потребовался бы `git filter-repo` с последующим слиянием — существенно +трудозатратнее. Рекомендация: `git mv` достаточно; происхождение прослеживается через этот набор +документов. + +--- + +## 6. Именование `api.meta` + +`api.meta` (интроспекция JDBC-`DatabaseMetaData`) семантически близка к JDBC, но ради согласованности +переименовывается в `sql.api.meta`. Если позже понадобится более чистое разделение «чистая модель +SQL» vs «интроспекция JDBC», `api.meta` можно выделить отдельным шагом — **не** часть этой миграции. + +--- + +## 7. Матрица рисков (кратко) + +| Риск | Вероятность | Эффект | Мера | +|---|---|---|---| +| redshift/snowflake неверно названы | позже | неверный выбор диалекта в рантайме | §1, исправить при переносе | +| цикл реактивирован через тест-ребро | средняя | сборка/релиз заблокированы | §3, переписать/отключить тесты | +| нет JDK-21 toolchain в CI | низкая | сборка красная | перевести CI-workflow на 21 | +| расхождение версий (assertj/junit) | низкая | предупреждения/ошибки сборки | централизованно зафиксировать ([06](06-build-and-java21.md)) | +| перепутан порядок релизов | средняя | jdbc.db не собирается | сначала установить/опубликовать `sql` | diff --git a/docs/dialect-migration/ru/08-verification.md b/docs/dialect-migration/ru/08-verification.md new file mode 100644 index 0000000..7b96323 --- /dev/null +++ b/docs/dialect-migration/ru/08-verification.md @@ -0,0 +1,87 @@ + + +# 08 – Проверка и приёмка + +Назад к [мастер-плану](README.md). + +--- + +## 1. Порядок сборки + +```bash +# 1) собрать, протестировать и локально установить SQL-репозиторий +cd org.eclipse.daanse.sql +mvn -q clean install + +# 2) jdbc.db против свежеустановленных снапшотов sql.* +cd ../org.eclipse.daanse.jdbc.db +mvn -q clean verify +``` + +`sql` **обязан** быть установлен до `jdbc.db` (односторонняя зависимость, см. +[02](02-dependency-and-layering.md)). + +--- + +## 2. Объём тестов + +- **SQL-репозиторий:** + - Интеграционные тесты `statement/demo` (H2 in-memory + MSSQL через Testcontainers) зелёные. + - Тесты `guard/jsqltranspiler` (используют `sql.dialect.db.h2/common`) зелёные. + - Тесты `deparser` зелёные. + - Перенесённые unit-тесты диалектов (тесты генераторов/квотеров) зелёные. + - Round-trip-тесты с кросс-репо зависимостью от `impl`: статус по каждому диалекту + задокументирован (переписан | `@Disabled`, см. [07](07-anomalies-and-risks.md) §3). +- **jdbc.db:** + - Тесты `impl` (DDL round-trip) зелёные против `sql.dialect.db.h2/common`. + - ETL-тесты `importer/csv` (`CsvDataLoaderTest`) зелёные. + +--- + +## 3. Структурные проверки + +```bash +# В SQL-репозитории больше нет старых пакетов dialect/api: +grep -rl "org.eclipse.daanse.jdbc.db" org.eclipse.daanse.sql/ --include=*.java # → пусто + +# SQL-репозиторий больше не ссылается на внешние артефакты jdbc.db.dialect: +grep -rn "jdbc.db.dialect" org.eclipse.daanse.sql/ --include=pom.xml # → пусто + +# jdbc.db больше не содержит модулей api/record/dialect в реакторе: +grep -nE "(api|record|dialect)" org.eclipse.daanse.jdbc.db/pom.xml # → пусто +``` + +--- + +## 4. Определение готовности — набор документов (этот каталог) + +- [ ] `README.md` + `01`–`08` присутствуют, взаимно связаны, согласованы. +- [ ] Таблица разделения покрывает каждый модуль обоих репозиториев ровно один раз. +- [ ] Сопоставление переименований полное (у каждого переносимого пакета + artifactId есть цель). +- [ ] Порядок миграции соблюдает граф зависимостей. +- [ ] Устранение цикличности обосновано. +- [ ] Аномалии (redshift/snowflake, Java 21, кросс-репо тесты) зафиксированы. + +## 5. Определение готовности — реализация кода (поздняя фаза) + +- [ ] `mvn clean install` в SQL-репозитории зелёный (Java 21). +- [ ] SQL-репозиторий **не** имеет внешних зависимостей `jdbc.db.*`. +- [ ] `mvn clean verify` в jdbc.db зелёный против опубликованных снапшотов `sql.*`. +- [ ] Интеграционные тесты `statement/demo` (H2/MSSQL) проходят. +- [ ] CI-проверки лицензионных заголовков и Javadoc в обоих репозиториях зелёные. +- [ ] Активное подмножество диалектов (10 модулей) полностью собирается под `sql/dialect/db/`. +- [ ] Аномалия redshift/snowflake задокументирована для поздней волны (сейчас не затрагивается). + +--- + +## 6. Откат + +Поскольку переезд выполняется в рабочих ветках обоих репозиториев и используется `git mv`, откат +возможен отбрасыванием веток. Ни одно изменение не сливается до тех пор, пока оба репозитория не +собираются зелёными и не выполнено DoD. diff --git a/docs/dialect-migration/ru/09-minimal-split-analysis.md b/docs/dialect-migration/ru/09-minimal-split-analysis.md new file mode 100644 index 0000000..5a4c4ac --- /dev/null +++ b/docs/dialect-migration/ru/09-minimal-split-analysis.md @@ -0,0 +1,215 @@ + + +# 09 – Минимальный срез: что `sql` реально берёт из `jdbc.db` + +Назад к [мастер-плану](README.md). + +> 🌐 Язык: [Deutsch](../09-minimal-split-analysis.md) · **Русский** (этот файл) + +**Назначение.** Это *второй* анализ, дополняющий План 1 (полный перенос `api`+`record`, см. +[README](README.md) и [02](02-dependency-and-layering.md)). Он отвечает на вопрос: *как выделить +**минимальную** часть, которую `sql` берёт из `jdbc.db`, при том что диалекты тоже нужны?* Повод — +идея встроить диалекты **статически по версиям**, чтобы не требовался JDBC-`record` для установки +свойств. + +Документ **намеренно не** фиксирует один путь. Он нейтрально излагает факты и четыре варианта с +трудозатратами/рисками. + +--- + +## 1. Исходный вопрос + +План 1 переносит `api` + `record` **целиком** в `sql`. Механически просто, но делает `sql` +владельцем всей модели метаданных/интроспекции. Вопрос здесь: можно ли **меньше** — только то, что +реально нужно диалектам и собственному коду SQL? + +--- + +## 2. Что нужно самому `sql` (очень мало) + +Собственный код SQL (`guard`, `deparser`, `statement`) из `jdbc.db.api` затрагивает лишь **7 лёгких +листовых типов** — **никакого** `record`, **никакого** `api.meta`, никакой интроспекции: + +| Пакет | Типы | +|---|---| +| `api.type` | `Datatype`, `BestFitColumnType` | +| `api.sql` | `BitOperation`, `OrderedColumn` | +| `api.schema` (References) | `SchemaReference`, `TableReference`, `ColumnReference` | + +Сегодня они приходят даже только **транзитивно** через `dialect.api` (ни один pom SQL не объявляет +`jdbc.db.api` напрямую). Они самодостаточны и замкнуты (`BestFitColumnType`/`Datatype` — чистые +enum; `TableReference → SchemaReference → CatalogReference → Named`). + +**Проблема не в коде SQL — она в самих диалектах.** + +--- + +## 3. Что тянут диалекты — две роли + +Конкретный диалект сегодня совмещает **две независимые обязанности**: + +### Роль A — Нативный провайдер метаданных (~85–90 % связности) + +`Dialect extends … MetadataProvider`. 6 «больших» диалектов переопределяют по 14–21 метода +`MetadataProvider` (`getAllIndexInfo`, `getAllPrimaryKeys`, `getAllImportedKeys`, `getAllTriggers`, +`getAllSequences`, `getUniqueConstraints`, …), которые запрашивают **`information_schema` / +каталожные таблицы против живого `Connection`** и **конструируют объекты `record.schema` для +возврата**. + +- `record.schema.*` и `api.meta.IndexInfo`/`IndexInfoItem` используются **исключительно** для этого. +- Это **интроспекция** (*чтение* метаданных), а не генерация SQL. + +| Диалект | Переопределений MetadataProvider | Конструирований `record.schema` | +|---|---|---| +| oracle | 21 | 19 | +| mssqlserver | 19 | 16 | +| postgresql | 19 | 16 | +| mysql | 15 | 11 | +| mariadb | 15 | 12 | +| h2 | 14 | 11 | +| **duckdb / sqlite / common** | **0** | **0** | + +### Роль B — Потребитель DDL (~10–15 % связности) + +API `DdlGenerator` принимает типы `api.schema` как **входной словарь**: + +```java +default String createTable(TableReference table, List columns, + PrimaryKey primaryKey, boolean ifNotExists) { … } +default String alterTableAddColumn(TableReference table, ColumnDefinition column) { … } +default String createTrigger(String name, Trigger.TriggerTiming timing, + Trigger.TriggerEvent event, TableReference table, …) { … } +``` + +- Основная часть — `default`-методы **в общем API** (`DdlGenerator`), а не в диалектах. Только + mssqlserver/oracle/mysql переопределяют некоторые с входами `api.schema`. +- **Никогда** не затрагивает `record` или `api.meta` — только `api.schema` как параметры. + +### Роль C — Конфигурация/возможности: **нет** связности с метаданными + +Ни `record`, ни `api.meta`, ни тяжёлые типы-определения `api.schema` не используются для +конфигурации/возможностей (см. §5). + +--- + +## 4. Блокер `sealed` и замкнутый остров `api.schema` + +Почему `api.schema` нельзя сократить до «пары References»: + +- `SchemaObject` — `sealed … permits TableDefinition, ViewDefinition, MaterializedView, Sequence, + Function, Procedure, Trigger, UserDefinedType` (8). +- `Constraint` — `sealed … permits PrimaryKey, ImportedKey, UniqueConstraint, CheckConstraint` (4). + +Как только генератор затрагивает `Trigger`, `TableDefinition` или `PrimaryKey` (роль B), запечатанная +иерархия заставляет **весь пакет `api.schema`** (39 типов) компилироваться вместе. + +**Хорошая новость:** `api.schema` **не** импортирует другие пакеты api (ни `meta`, ни `sql`, ни +`type`). Это **замкнутый остров** — тяжёлый, но переносимый целиком, не таща за собой интроспекцию +(`api.meta`, верхний уровень). + +--- + +## 5. Конфигурация/версионирование уже развязаны + +Идея «встроить статически по версиям» попадает в систему, которая **уже** так устроена: + +- **`DialectInitData`** — лёгкий `record` (только примитивы: `productVersion`, major/minor, символ + кавычки, ключевые слова, стили ResultSet). Он **не** зависит от `api.schema`/`record`. Есть + `ansiDefaults()` (чисто статически, без JDBC) и withers (`withVersion(maj,min)`, …). +- **Возможности** — статические boolean-record с пресетами (`DdlCapabilities.full()/.minimal()`, + `AggregateCapabilities.none()`, …). Никогда не выводятся из живых метаданных во время запроса. +- **Ветвление по версии** уже статическое — два устоявшихся паттерна: + - **Подкласс на вариант:** `Db2OldAs400Dialect extends Db2Dialect` (переопределяет + `allowsFromQuery()→false`), собственный `@DialectName("DB2_OLD_AS400")`. + - **Внутри метода:** `dialectVersion.isUnknownOrAtLeast(8,0)` (mysql/oracle/postgresql/mssql/mariadb/sqlite). + +Таким образом **генерация SQL уже полностью развязана с живыми метаданными** — читает только +константы + один `int` `dialectVersion`, задаваемый без Connection. Единственная связь с +метаданными — роль A (*выдача* метаданных). + +--- + +## 6. Варианты (нейтрально, с трудозатратами/рисками) + +### O1 — Разделение ролей: вынести роль MetadataProvider + +Убрать `Dialect extends MetadataProvider`; методы-загрузчики (~7–21) 6 больших диалектов вынести в +классы-компаньоны (`MetadataProvider implements MetadataProvider`), остающиеся в `jdbc.db` при +интроспекции (связывание через OSGi). Диалект со стороны SQL сохраняет всё остальное. + +- **Результат:** диалекты больше **не** импортируют `record`/`api.meta`. Ядро SQL = SQL-генерация + диалекта + `api.schema` (остров) + `api.type` + `api.sql`. `record`, `api.meta`, интроспекция, + `impl`, `importer` остаются в `jdbc.db`. +- **Трудозатраты:** средние — затронуть 6 диалектов; слом SPI `Dialect` (дефолты `getAll*` уходят в + отдельный SPI `MetadataProvider`). duckdb/sqlite/common без изменений. +- **Риск:** нужно заново собрать OSGi-связывание диалект ↔ MetadataProvider; потребители + `dialect.getAllIndexInfo(...)` должны перейти на отдельный провайдер. + +### O2 — Разделение ролей + фиксированные подклассы по версиям + +Как O1, плюс заменить ветвление `dialectVersion` **фиксированными классами по версиям** (паттерн +`Db2OldAs400Dialect`), свойства полностью вкомпилированы — снапшот `DialectInitData` **больше не +нужен**. + +- **Результат:** максимальная развязка; диалект полностью статичен, без метаданных времени + выполнения. +- **Трудозатраты:** высокие — на каждую БД несколько классов версий; логика выбора/фабрики по версии. +- **Риск:** взрыв числа классов; выбор версии должен быть надёжным (имя/версия продукта → класс). + +### O3 — Срез по замыканию без рефакторинга + +Без слома API. Диалекты + их транзитивное замыкание в SQL: `dialect.*`, `api.type`, `api.sql`, весь +`api.schema`, `api.meta.IndexInfo(+Item)`, `MetadataProvider`, `record.schema`. В `jdbc.db` остаётся +чистый движок интроспекции (`DatabaseService`, `MetaDataQueries`, `SnapshotBuilder`, `meta` минус +IndexInfo, `record.meta`, `impl`, `importer`). + +- **Результат:** меньше, чем полный перенос (движок остаётся), но `record.schema` + `MetadataProvider` + переезжают. Одностороннее `jdbc.db → sql`, без цикла, без изменения API. +- **Трудозатраты:** средние — `api`/`record` нужно **разделить** (schema/type/sql/IndexInfo → SQL; + остальное остаётся). +- **Риск:** пакет `api`/`record` разделён между двумя репозиториями; провести границу аккуратно. + +### O4 — Полный перенос (= План 1) + +`api` + `record` целиком в SQL. См. [README](README.md)/[02](02-dependency-and-layering.md). + +- **Результат:** простейшая механика; SQL владеет всей моделью метаданных/интроспекции. +- **Трудозатраты:** низкие–средние; **риск:** низкий; но **не** «минимально». + +--- + +## 7. Сравнение + +| | O1 Разделение ролей | O2 + классы версий | O3 Срез по замыканию | O4 Полный перенос | +|---|---|---|---|---| +| SQL получает `record` | нет | нет | `record.schema` | целиком | +| SQL получает `api.meta` | нет | нет | только IndexInfo | целиком | +| SQL получает `api.schema` | целиком (остров) | целиком (остров) | целиком (остров) | целиком | +| `Dialect extends MetadataProvider` | **убрано** | убрано | остаётся | остаётся | +| Слом API | да (SPI) | да (SPI) | нет | нет | +| Затронутые диалекты | 6 | 6 + классы версий | 0 (только зависимости) | 0 | +| `api`/`record` разделены | да | да | да | нет | +| Размер ядра SQL | **минимальный** | **минимальный** | средний | большой | +| Трудозатраты | средние | высокие | средние | низкие–средние | + +**Общее для всех вариантов:** `api.type` + `api.sql` всегда уходят в SQL (чистые листья, используются +только диалектами); `impl` + `importer/csv` (ETL) всегда остаются в `jdbc.db`; результат всегда — +одностороннее `jdbc.db → sql` без цикла. + +--- + +## 8. Связь с Планом 1 + +План 1 ([README](README.md)) соответствует **O4**. Этот анализ показывает, что **O1/O2** дают +по-настоящему минимальное ядро SQL (следуя идее «статически по версиям, без JDBC-record») ценой слома +SPI `Dialect` и правки 6 больших диалектов; **O3** — компромисс без слома API. Выбор намеренно +оставлен открытым. + +> **Полный план для O1/O2:** см. [`minimal-split/`](minimal-split/README.md) — исполняемый +> пошаговый план разделения ролей (вынос роли A из диалектов). diff --git a/docs/dialect-migration/ru/README.md b/docs/dialect-migration/ru/README.md new file mode 100644 index 0000000..0ca82e3 --- /dev/null +++ b/docs/dialect-migration/ru/README.md @@ -0,0 +1,143 @@ + + +# Миграция: диалекты + API диалектов из `jdbc.db` в `sql` + +**Статус:** план / спецификация (перенос кода ещё не выполнен) +**Затронутые репозитории:** `org.eclipse.daanse.sql` (цель) · `org.eclipse.daanse.jdbc.db` (источник) + +Этот каталог — **мастер-план** переноса стека диалектов баз данных из репозитория +`org.eclipse.daanse.jdbc.db` в репозиторий `org.eclipse.daanse.sql`. Он описывает, *что* +переносится, *почему*, *в каком порядке* и *как* — как исполняемую рабочую инструкцию для +последующей реализации. + +> Немецкая версия (первоисточник): [`../README.md`](../README.md). + +--- + +## 1. Мотивация + +Репозиторий SQL (`guard`, `deparser`, `statement`) уже **знает о диалектах**, но сам абстракцию +диалекта не определяет. Сегодня он потребляет её как **внешние Maven-артефакты** из +`org.eclipse.daanse.jdbc.db`: + +- `org.eclipse.daanse.jdbc.db.dialect.api` (`Dialect`, `IdentifierQuotingPolicy`, `generator.*`) +- `org.eclipse.daanse.jdbc.db.dialect.db.common` (`AnsiDialect`) +- `org.eclipse.daanse.jdbc.db.dialect.db.h2` / `.mysql` / `.mssqlserver` +- модель метаданных/типов `…jdbc.db.api.schema.*` и `…api.type.BestFitColumnType` + +**Цель:** репозиторий SQL должен **владеть полным стеком генерации SQL и диалектов**. Код диалектов, +API диалектов и необходимая им модель метаданных/типов (`api` + `record`) переезжают в `sql`. +`jdbc.db` сохраняет только чистый **движок интроспекции метаданных** (`impl`) и **слой ETL** +(`importer/csv`) и становится **односторонним нижестоящим потребителем** `sql`. + +**Ограничение:** сугубо ETL-компоненты **не** переносятся в `sql`. + +--- + +## 2. Принятые ключевые решения + +| Тема | Решение | Детальный документ | +|---|---|---| +| Имена пакетов | Все переносимые модули переименовываются в `org.eclipse.daanse.sql.*` | [03](03-package-rename-map.md) | +| Общая граница API | `api` + `record` переезжают **вместе** → однонаправленная слоистость, без цикла | [02](02-dependency-and-layering.md) | +| Объём диалектов | Только **активное** подмножество в реакторе (10 модулей) | [01](01-inventory-and-scope.md) | +| Версия Java | Репозиторий SQL повышается с Java **17 → 21** | [06](06-build-and-java21.md) | + +--- + +## 3. Обзор разделения + +### Переезжает в `org.eclipse.daanse.sql` (+ переименование пакетов) + +| Источник в `jdbc.db` | Целевой артефакт в `sql` | Роль | +|---|---|---| +| `api` | `org.eclipse.daanse.sql.api` | Модель метаданных/схемы/типов/SQL | +| `record` | `org.eclipse.daanse.sql.record` | Record-реализации к ней | +| `dialect/api` | `org.eclipse.daanse.sql.dialect.api` | API диалектов (`Dialect`, `DialectFactory`, `generator`, `capability`, `type`) | +| `dialect/db/common` | `org.eclipse.daanse.sql.dialect.db.common` | `AbstractJdbcDialect`, `AnsiDialect`, генераторы/квотеры Jdbc* | +| `dialect/db/test-support` | `org.eclipse.daanse.sql.dialect.db.test-support` | Тестовые помощники | +| `dialect/db/{duckdb,h2,mariadb,mssqlserver,mysql,oracle,postgresql,sqlite}` | `org.eclipse.daanse.sql.dialect.db.` | 8 активных конкретных диалектов | + +### Остаётся в `org.eclipse.daanse.jdbc.db` (становится потребителем `sql`) + +| Модуль | Роль | Новая зависимость | +|---|---|---| +| `impl` | Движок интроспекции метаданных (`DatabaseServiceImpl`, `CachingDatabaseService`) | compile→ `sql.api`, `sql.record`, `sql.dialect.api`; test→ `sql.dialect.db.common/h2` | +| `importer` + `importer/csv` | **ETL** (импорт CSV) | compile→ `sql.api`, `sql.record`, `sql.dialect.api`; runtime→ `impl` | +| `dialect/db/*` (неактивные, ~27) | пока припаркованы | более поздняя волна миграции | + +--- + +## 4. Целевая структура реактора в репозитории SQL + +``` +org.eclipse.daanse.sql/ +├── pom.xml # корневой агрегатор: расширить (api, record, dialect); Java 21 +├── api/ # НОВОЕ (leaf) +├── record/ # НОВОЕ (leaf) +├── dialect/ # НОВОЕ (агрегатор, packaging=pom) +│ ├── api/ # leaf +│ └── db/ # агрегатор, packaging=pom +│ ├── common/ test-support/ +│ ├── duckdb/ h2/ mariadb/ mssqlserver/ +│ └── mysql/ oracle/ postgresql/ sqlite/ +├── deparser/ # существующее — зависимости диалектов извне → внутрь реактора +├── guard/ # существующее — то же +└── statement/ # существующее — то же +``` + +--- + +## 5. Волны миграции (порядок) + +1. **Волна A — фундамент:** `api` → `record` в `sql` (переименование, запись в реактор). +2. **Волна B — ядро диалектов:** `dialect/api` → `dialect/db/common` → `test-support`. +3. **Волна C — конкретные диалекты:** 8 активных модулей БД (`mysql` перед `mariadb`). +4. **Волна D — потребители SQL:** `guard`/`deparser`/`statement` переключить с внешних зависимостей + на внутренние модули реактора; повышение до Java 21. +5. **Волна E — демонтаж jdbc.db:** `impl`/`importer` переключить на опубликованные снапшоты `sql.*`, + удалить переехавшие модули из реактора `jdbc.db`. + +Каждая волна собирается отдельно (`mvn -q verify`) до начала следующей. + +--- + +## 6. Детальные документы + +| # | Документ | Содержание | +|---|---|---| +| 01 | [Инвентаризация и объём](01-inventory-and-scope.md) | Полный список модулей, классификация, активные/неактивные | +| 02 | [Зависимости и слоистость](02-dependency-and-layering.md) | Граф, цикл + его устранение, целевая слоистость | +| 03 | [Переименование пакетов/артефактов](03-package-rename-map.md) | Полное сопоставление старое→новое | +| 04 | [Процедура миграции](04-migration-procedure.md) | Пошагово, помодульно | +| 05 | [Перепривязка потребителей](05-consumer-rewiring.md) | Изменения pom/import в SQL и jdbc.db | +| 06 | [Сборка и Java 21](06-build-and-java21.md) | Pom-файлы реактора, версии, повышение Java | +| 07 | [Аномалии и риски](07-anomalies-and-risks.md) | Путаница redshift/snowflake, Java, поздние волны | +| 08 | [Проверка и приёмка](08-verification.md) | Команды сборки, объём тестов, DoD | +| 09 | [Минимальный срез (2-й анализ)](09-minimal-split-analysis.md) | Альтернатива полному переносу: что реально нужно `sql`, разделение ролей, 4 варианта | +| — | [**Минимальный срез — полный план**](minimal-split/README.md) | Исполняемый план разделения ролей (O1/O2) как альтернатива 01–08 | + +> **Примечание:** Документы 01–08 описывают **полный перенос** (План 1). Документ **09** — это +> дополнительный *второй анализ* **минимального** среза; каталог +> [`minimal-split/`](minimal-split/README.md) содержит по нему **полный исполняемый план** +> (разделение ролей). Эти два плана — альтернативы, реализовать оба не нужно. + +--- + +## 7. Определение готовности (набор документов) + +- [ ] Все 9 документов присутствуют, взаимно связаны, согласованы. +- [ ] Таблица разделения покрывает **каждый** модуль обоих репозиториев ровно один раз. +- [ ] Сопоставление переименований полное (у каждого переносимого пакета + artifactId есть цель). +- [ ] Порядок миграции соблюдает граф зависимостей (нет опережающих ссылок). +- [ ] Устранение цикличности обосновано. + +**DoD последующей реализации кода:** `mvn -q verify` зелёный в обоих репозиториях; SQL-репозиторий +собирается без внешних зависимостей `jdbc.db.dialect.*`; `jdbc.db` собирается против +опубликованных снапшотов `sql.*`; интеграционные тесты `statement/demo` (H2/MSSQL) проходят. diff --git a/docs/dialect-migration/ru/minimal-split/01-role-separation.md b/docs/dialect-migration/ru/minimal-split/01-role-separation.md new file mode 100644 index 0000000..581ec52 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/01-role-separation.md @@ -0,0 +1,129 @@ + + +# 01 – Разделение ролей: `Dialect` ↔ `MetadataProvider` + +Назад к [плану минимального среза](README.md). + +Это **ключевой рефакторинг**. Он выполняется целиком **внутри `jdbc.db`** и там собирается зелёным +ещё до переноса чего-либо в SQL-репозиторий. Только когда эта развязка готова, диалект становится +достаточно лёгким для минимального переноса. + +--- + +## 1. Исходное состояние (обосновано) + +- `dialect/api/.../Dialect.java`: `public interface Dialect extends IdentifierQuoter, LiteralQuoter, + DialectCapabilitiesProvider, TypeMapper, MetadataProvider`. Через `MetadataProvider` (из `api`) + каждый диалект *является* провайдером метаданных. +- `MetadataProvider` (`api/.../MetadataProvider.java`): ~40 `default`-методов (возврат + `Optional.empty()`/`List.of()`); импортирует ~25 `api.schema.*` + `api.meta.IndexInfo`/`TypeInfo`. +- 6 больших диалектов (h2, mariadb, mssqlserver, mysql, oracle, postgresql) переопределяют по 14–21 + из этих методов реальными запросами к `information_schema` и строят объекты `record.schema`. +- `duckdb`, `sqlite`, `common`, все фабрики: **нет** переопределений (наследуют пустые дефолты). +- **Вызывающие уже параметризованы провайдером:** `DatabaseServiceImpl` принимает `MetadataProvider` + как **аргумент** и вызывает на нём `getAllIndexInfo/getAllPrimaryKeys/...` — он **не** ссылается на + `Dialect`. Примеры: + ```java + public MetaInfo createMetaInfo(Connection connection, MetadataProvider metadataProvider) { … } + Optional> providerIndexes = provider.getAllIndexInfo(connection, null, null); + Optional> providerPKs = provider.getAllPrimaryKeys(connection, null, null); + ``` + +**Вывод:** движок и диалект фактически уже разделены; их сваривает только наследование. + +--- + +## 2. Целевая картина + +``` +Dialect (генерация SQL) MetadataProvider (интроспекция) + IdentifierQuoter getAllIndexInfo(...) + LiteralQuoter getAllPrimaryKeys(...) + DialectCapabilitiesProvider getAllImportedKeys(...) + TypeMapper ... + (НЕТ extends MetadataProvider) ▲ + │ реализует + MetadataProvider (новое, остаётся в jdbc.db) +``` + +- `Dialect` теряет `extends MetadataProvider` и все унаследованные дефолты `getAll*`. +- На каждый большой диалект создаётся `MetadataProvider implements MetadataProvider` с вынесенными + методами (поведение без изменений). Эти классы остаются при интроспекции в `jdbc.db`. +- `MetadataProvider.EMPTY` остаётся дефолтом для диалектов без нативных метаданных (duckdb/sqlite/…). + +--- + +## 3. Шаги (внутри `jdbc.db`) + +### 3.1 Вынести классы-компаньоны (6×) + +Для каждого из `h2, mariadb, mssqlserver, mysql, oracle, postgresql`: + +1. Новый класс `dialect/metadata//.../MetadataProvider.java`, + `implements org.eclipse.daanse.jdbc.db.api.MetadataProvider`. +2. Переопределённые методы `getAll*`/`get*` **перенести туда** (вместе с приватными помощниками: + `readImportedKey`, `PkBuilder`, `resolveSchema`, `mapMySqlIndexType`). +3. В классе диалекта остаются только генерация SQL, кавычки, возможности, TypeMapper. +4. Почистить импорты: класс диалекта после этого **не** импортирует `record.*` и `api.meta.*` + (проверка: `grep` должен быть пуст). Класс `MetadataProvider` теперь несёт эти импорты. + +> Оставшиеся импорты `api.schema` в классе диалекта (роль B, только mssqlserver/oracle/mysql) +> **намеренны** — это входной словарь DDL, он позже переезжает как замкнутый остров в SQL. + +### 3.2 Убрать наследование + +`dialect/api/.../Dialect.java`: убрать `MetadataProvider` из списка `extends`; удалить импорт +`org.eclipse.daanse.jdbc.db.api.MetadataProvider`. `BestFitColumnType getType(...)` остаётся +(использует `api.type`, не модель метаданных). + +### 3.3 Предоставление/связывание компаньонов (OSGi) + +Сегодня каждая фабрика регистрирует `@Component(service = DialectFactory.class, …)`; +`DatabaseServiceImpl` — `@Component(service = DatabaseService.class)`. После разделения: + +- Опубликовать `MetadataProvider` как отдельный DS-компонент с тегом `@DialectName("MYSQL")` + (по аналогии с фабриками), **или** предоставить через `MetadataProviderFactory` параллельно + `DialectFactory`. +- Вызывающие, которые ранее передавали `Dialect` как `MetadataProvider`, теперь берут нужный + `MetadataProvider` (по `@DialectName`/имени продукта), запасной — `MetadataProvider.EMPTY`. +- Детали связывания в [04](04-consumer-rewiring.md). + +### 3.4 Перевести вызывающих + +- `DatabaseServiceImpl`: сигнатуры без изменений (параметр `MetadataProvider`). Меняется только + **предоставление** провайдера в точке вызова (не «диалект», а компаньон). +- Тесты, ранее вызывавшие `service.createMetaInfo(conn, dialect)`, теперь вызывают + `service.createMetaInfo(conn, new MySqlMetadataProvider())` (и т. п.). + +--- + +## 4. Трудозатраты и объём + +| Диалект | методов к выносу (ориентир.) | нужен компаньон | +|---|---|---| +| oracle | 21 | да | +| mssqlserver | 19 | да | +| postgresql | 19 | да | +| mariadb | 15 | да | +| mysql | 15 | да | +| h2 | 14 | да | +| duckdb, sqlite | 0 | нет (EMPTY) | +| common, test-support | 0 | нет | + +Чистый рефакторинг-перенос (методы + приватные помощники), без перестройки поведения. Слом SPI +`Dialect` — ровно одна строка (список `extends`) плюс изменённое предоставление провайдеров. + +--- + +## 5. Приёмка этой фазы (только `jdbc.db`) + +- [ ] `Dialect` без `extends MetadataProvider`; нет импорта `MetadataProvider` в `Dialect.java`. +- [ ] 6 классов `MetadataProvider` с полностью вынесенными методами. +- [ ] `grep -rE "import .*\.(record|api\.meta)\." dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main` → **пусто**. +- [ ] `mvn -q verify` в реакторе `jdbc.db` зелёный (включая round-trip / MetaInfo-тесты против компаньонов). diff --git a/docs/dialect-migration/ru/minimal-split/02-target-structure.md b/docs/dialect-migration/ru/minimal-split/02-target-structure.md new file mode 100644 index 0000000..c641e20 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/02-target-structure.md @@ -0,0 +1,115 @@ + + +# 02 – Целевая структура и разделение API + +Назад к [плану минимального среза](README.md). + +После разделения ролей ([01](01-role-separation.md)) код диалектов зависит лишь от узкого замкнутого +подмножества `api`. Этот документ фиксирует, **как разделяется `api`** и какие пакеты/модули куда идут. + +--- + +## 1. Разделение `api` + +`org.eclipse.daanse.jdbc.db.api` разделяется на два артефакта: + +| Часть | Пакеты | Цель | +|---|---|---| +| **Модель SQL** | `api.schema.*`, `api.type.*`, `api.sql.*` | → `org.eclipse.daanse.sql.api` | +| **Интроспекция (остаток)** | `api.meta.*`, верхний уровень `MetadataProvider`, `DatabaseService`, `MetaDataQueries`, `SnapshotBuilder` | остаётся `org.eclipse.daanse.jdbc.db.api` | + +**Почему эта граница чиста (обосновано):** +- `api.schema` **не** импортирует другие пакеты api → замкнутый `sealed`-остров, переезжает целиком. +- `api.type`/`api.sql` — чистые листья (используют только диалекты) → переезжают. +- Остаток интроспекции импортирует `api.schema` (теперь в SQL) → ребро **`jdbc.db → sql`**, + одностороннее. Конкретно: `MetadataProvider`, `MetaDataQueries`, `SnapshotBuilder`, `StructureInfo` + ссылаются на типы схемы — после разделения эти ссылки указывают на `sql.api.schema` (разрешено, без + цикла). + +`record` **не** разделяется и **не** переносится: `record.schema`/`record.meta` остаются целиком в +`jdbc.db` (используются только компаньонами `MetadataProvider` и `impl`). `record` реализует +интерфейсы `api.schema` (теперь в SQL) → снова одностороннее `jdbc.db → sql`. + +--- + +## 2. Переименование пакетов/артефактов (только сторона SQL) + +| Старое | Новое | Примечание | +|---|---|---| +| `org.eclipse.daanse.jdbc.db.api.schema` | `org.eclipse.daanse.sql.api.schema` | весь остров | +| `org.eclipse.daanse.jdbc.db.api.type` | `org.eclipse.daanse.sql.api.type` | лист | +| `org.eclipse.daanse.jdbc.db.api.sql` | `org.eclipse.daanse.sql.api.sql` | лист | +| `org.eclipse.daanse.jdbc.db.dialect.api` | `org.eclipse.daanse.sql.dialect.api` | без наследования `MetadataProvider` | +| `org.eclipse.daanse.jdbc.db.dialect.db.` | `org.eclipse.daanse.sql.dialect.db.` | без методов роли A | + +**Остаётся без изменений в `jdbc.db`:** `…api.meta`, `…api` (верхний уровень интроспекции), +`…record.*`, `…impl`, `…importer.*`, и **новое** `…dialect.metadata.` (компаньоны). + +ArtifactId: +- новые: `org.eclipse.daanse.sql.api` (schema+type+sql), `org.eclipse.daanse.sql.dialect.api`, + `org.eclipse.daanse.sql.dialect.db.*`. +- остаются: `org.eclipse.daanse.jdbc.db.api` (теперь только meta+интроспекция), `…record`, `…impl`, + `…importer.csv`, новое `…dialect.metadata` (сборный модуль компаньонов). + +> **О наименовании:** оставшийся `jdbc.db.api` после разделения содержит только интроспекцию. +> Опционально позже его можно переименовать в `jdbc.db.introspection` — **не** часть этого плана. + +--- + +## 3. Раскладка модулей + +**SQL-репозиторий (новое):** +``` +sql/ + api/ org.eclipse.daanse.sql.api (schema + type + sql) + dialect/ + api/ org.eclipse.daanse.sql.dialect.api + db/ + common/ test-support/ duckdb/ h2/ mariadb/ mssqlserver/ mysql/ oracle/ postgresql/ sqlite/ + deparser/ guard/ statement/ (существует) +``` + +**Репозиторий jdbc.db (после переустройства):** +``` +jdbc.db/ + api/ org.eclipse.daanse.jdbc.db.api (только meta + интерфейсы интроспекции) + record/ org.eclipse.daanse.jdbc.db.record (целиком) + impl/ org.eclipse.daanse.jdbc.db.impl + dialect/ + metadata/ org.eclipse.daanse.jdbc.db.dialect.metadata (НОВОЕ: MetadataProvider) + importer/csv/ org.eclipse.daanse.jdbc.db.importer.csv +``` + +--- + +## 4. Рёбра зависимостей после переустройства (целевое состояние) + +``` +sql.api → (JDK / slf4j) # нет ребра к jdbc.db +sql.dialect.api → sql.api +sql.dialect.db.* → sql.dialect.api, sql.dialect.db.common, sql.api + +jdbc.db.api (остаток) → sql.api.schema # интроспекция ссылается на модель схемы +jdbc.db.record → sql.api # записи реализуют интерфейсы схемы +jdbc.db.dialect.metadata. → jdbc.db.api (MetadataProvider), jdbc.db.record, sql.api +jdbc.db.impl → jdbc.db.api, jdbc.db.record, (sql.api) +jdbc.db.importer.csv → jdbc.db.api, jdbc.db.record, sql.dialect.api (+ impl runtime) [ETL] +``` + +Все рёбра `jdbc.db` указывают односторонне на `sql.*`. У `sql.*` **нет** ребра к `jdbc.db` → **нет +цикла**, `sql` собирается/публикуется независимо. + +--- + +## 5. Минимальный набор типов SQL (проверка) + +Собственный код SQL (guard/deparser/statement) по-прежнему нуждается только в 7 листовых типах +(`Datatype`, `BestFitColumnType`, `BitOperation`, `OrderedColumn`, `SchemaReference`, `TableReference`, +`ColumnReference`) — теперь из `sql.api`. Диалекты добавляют `api.schema` (остров, словарь DDL). +**Никакого** `record`, **никакого** `api.meta` на стороне SQL. diff --git a/docs/dialect-migration/ru/minimal-split/03-migration-procedure.md b/docs/dialect-migration/ru/minimal-split/03-migration-procedure.md new file mode 100644 index 0000000..76ddef6 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/03-migration-procedure.md @@ -0,0 +1,100 @@ + + +# 03 – Процедура миграции (пошагово) + +Назад к [плану минимального среза](README.md). + +Порядок: **сначала развязать (в `jdbc.db`), потом разделить, потом переносить.** Каждая волна +завершается зелёной сборкой. + +--- + +## Волна R — разделение ролей (в `jdbc.db`) + +Полностью описано в [01](01-role-separation.md). Коротко: + +1. По каждому большому диалекту (h2, mariadb, mssqlserver, mysql, oracle, postgresql): методы роли A + + приватные помощники перенести в `dialect/metadata//.../MetadataProvider.java`. +2. `Dialect.java`: убрать `extends MetadataProvider`, удалить импорт. +3. Предоставить компаньоны как OSGi-компоненты; перевести вызывающих/тесты на компаньон + ([04](04-consumer-rewiring.md)). +4. **Gate:** `mvn -q verify` в `jdbc.db` зелёный; main-код диалектов свободен от `record`/`api.meta`. + +> После волны R рефакторинг завершён и полезен сам по себе — перенос репозитория (волны S–D) можно +> планировать независимо. + +--- + +## Волна S — разделение API (подготовка модели SQL) + +1. Создать новый модуль `sql/api` (запись в корневой реактор); родитель = корень SQL. +2. Вынести пакеты `api.schema`, `api.type`, `api.sql` из `jdbc.db/api`: + - `git mv` трёх деревьев пакетов в + `sql/api/src/main/java/org/eclipse/daanse/sql/api/{schema,type,sql}`. + - Переименование `org.eclipse.daanse.jdbc.db.api.{schema,type,sql}` → + `org.eclipse.daanse.sql.api.{…}` во всех перенесённых файлах. +3. В оставшемся `jdbc.db/api` (только `api.meta` + верхний уровень интроспекции): перевести импорты + трёх вынесенных пакетов на `org.eclipse.daanse.sql.api.*`; добавить в pom зависимость на `sql.api` + (внешнюю, `0.0.1-SNAPSHOT`). +4. `record` и `impl`: импорты `…jdbc.db.api.{schema,type,sql}` → `…sql.api.*`; добавить в pom + зависимость на `sql.api`. +5. **Gate:** `sql/api` собирается изолированно (`mvn -q -pl org.eclipse.daanse.sql.api -am verify`); + `jdbc.db` собирается против снапшота `sql.api`. + +> `record` **не** переносится — только его импорты переводятся на `sql.api`. + +--- + +## Волна D — перенос диалектов в SQL + +Порядок: `sql.dialect.api` → `sql.dialect.db.common` → `test-support` → конкретные диалекты +(`mysql` перед `mariadb`). + +1. `git mv jdbc.db/dialect/api sql/dialect/api`; переименование `…jdbc.db.dialect.api` → + `…sql.dialect.api`; зависимость pom на `sql.api` (внутри реактора). `Dialect` уже без + `MetadataProvider` (волна R). +2. `git mv jdbc.db/dialect/db/common sql/dialect/db/common`; переименование; зависимость → + `sql.dialect.api`. +3. `test-support` аналогично. +4. Активные диалекты (`duckdb, h2, mariadb, mssqlserver, mysql, oracle, postgresql, sqlite`): + `git mv` в `sql/dialect/db/`; переименование `…dialect.db.` → `…sql.dialect.db.`; + зависимости pom → `sql.dialect.api`, `sql.dialect.db.common`, `sql.api` (внутри реактора). + **Нет** зависимости от `record`, **нет** от `api.meta` (благодаря волне R). +5. Агрегаторы `sql/dialect/pom.xml`, `sql/dialect/db/pom.xml` + корневой `` + ([06](06-build-and-verification.md)). +6. **Gate:** реактор SQL собирается; модули диалектов не имеют зависимостей от `jdbc.db`. + +> Компаньоны `MetadataProvider` остаются в `jdbc.db/dialect/metadata` — они **не** переезжают. + +--- + +## Волна C — потребители и демонтаж jdbc.db + +См. [04](04-consumer-rewiring.md): потребители SQL (guard/deparser/statement) — на внутренние +реактора; потребители `jdbc.db` (`impl`, `importer`, `dialect.metadata`) — на опубликованные снапшоты +`sql.*`; убрать `dialect/api`+`dialect/db/активные` из реактора `jdbc.db` (модуль компаньонов остаётся). + +--- + +## Волна V — усиление по версиям (опционально) + +См. [05](05-per-version-hardening.md). Может быть выполнена в любой момент после волны D и независима. + +--- + +## Краткая форма порядка + +``` +R: разделение ролей (jdbc.db) → Gate: jdbc.db зелёный, main диалектов без record/api.meta +S: разделение api (schema/type/sql → sql.api) +D: dialect.api + активные dialect.db.* → sql +C: потребители + демонтаж jdbc.db +V: (опционально) усиление по версиям +B: сборка/Java21/проверка (сквозная) +``` diff --git a/docs/dialect-migration/ru/minimal-split/04-consumer-rewiring.md b/docs/dialect-migration/ru/minimal-split/04-consumer-rewiring.md new file mode 100644 index 0000000..efb46c7 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/04-consumer-rewiring.md @@ -0,0 +1,97 @@ + + +# 04 – Перепривязка потребителей и OSGi + +Назад к [плану минимального среза](README.md). + +Затронуты три группы потребителей: (1) собственный код SQL, (2) модули `jdbc.db`, (3) предоставление/ +связывание новых компаньонов `MetadataProvider`. + +--- + +## 1. Потребители в SQL-репозитории (внутри реактора) + +Как в плане полного переноса ([../05-consumer-rewiring.md](../05-consumer-rewiring.md) §1), но +листовые типы теперь из `sql.api` вместо `jdbc.db.api`: + +| Модуль | Новое (внутри реактора) | +|---|---| +| `statement/api` | `sql.dialect.api.generator.*`, `sql.api.schema.{Schema,Table,Column}Reference`, `sql.api.type.*`, `sql.api.sql.*` | +| `statement/impl` | `sql.dialect.db.common`, `sql.dialect.db.mysql`, `sql.api.*` | +| `statement/demo` | `sql.dialect.db.common/mysql/mssqlserver`, `sql.api.*` | +| `guard/jsqltranspiler` | `sql.dialect.db.h2`, `sql.dialect.db.common` | +| `deparser/api`, `deparser/jsqlparser` | `sql.dialect.api` | + +Замена импортов: `org.eclipse.daanse.jdbc.db.dialect` → `org.eclipse.daanse.sql.dialect`; +`org.eclipse.daanse.jdbc.db.api.{schema,type,sql}` → `org.eclipse.daanse.sql.api.{…}`. Код SQL +**никогда** не затрагивает `record`/`api.meta` → других изменений нет. + +--- + +## 2. Потребители в `jdbc.db` (опубликованные снапшоты `sql.*`) + +### 2.1 `impl` (движок интроспекции) + +- Импорты `…jdbc.db.api.{schema,type,sql}` → `…sql.api.*` (волна S). +- Сохраняет `…jdbc.db.api` (meta + `MetadataProvider`/`DatabaseService`) **локально** и `…record` + **локально**. +- Pom: зависимость на `sql.api` (внешняя). В продакшн-коде **нет** зависимости от `dialect.*` (движок + знает только `MetadataProvider`). +- Тесты, ранее передававшие `Dialect` как провайдер, теперь используют нужный `MetadataProvider` + (из `jdbc.db.dialect.metadata`) или `MetadataProvider.EMPTY`. + +### 2.2 `jdbc.db.dialect.metadata.` (новое — компаньоны) + +- Pom: compile-зависимости на `jdbc.db.api` (MetadataProvider/meta), `jdbc.db.record` и `sql.api` + (интерфейсы схемы, которые реализуют записи). Runtime/DS по необходимости. +- Содержит по `MetadataProvider` (из волны R). + +### 2.3 `importer/csv` (ETL) + +- Импорты `…jdbc.db.api.{schema,type}` → `…sql.api.*`; `…dialect.api.Dialect`/`DialectFactory` → + `…sql.dialect.api.*`. `record`/`impl`/`fastcsv`/`io.fs.watcher.*` без изменений. +- **Примечание:** `CsvDataImporter` использует `Dialect` только для кавычек/DDL (роль B) — после + разделения по-прежнему корректно. Если он дополнительно *читает* метаданные, для этого использовать + `MetadataProvider`. + +### 2.4 Очистка реактора `jdbc.db/pom.xml` + +- Убрать из ``: `dialect` (активное подмножество `api`+`db`, теперь в SQL). +- Добавить: `dialect/metadata` (сборный модуль компаньонов). +- `api`, `record`, `impl`, `importer` остаются. + +--- + +## 3. Связывание провайдеров через OSGi + +**Текущее состояние:** каждая `DialectFactory` — `@Component(service = DialectFactory.class)`, с тегом +`@DialectName("MYSQL")`. `DatabaseServiceImpl` (`@Component(service = DatabaseService.class)`) принимает +`MetadataProvider` как **параметр метода** — то есть провайдер добывается в точке вызова, а не +инжектируется. + +**Целевое состояние:** компаньон `MetadataProvider` публикуется как отдельный сервис и делается +разрешимым: + +- Вариант A (рекомендуется): `MetadataProvider` как `@Component(service = MetadataProvider.class)` + с `@DialectName("MYSQL")`. Вызывающий, которому нужны метаданные продукта, фильтрует сервисы + `MetadataProvider` по `@DialectName`/имени продукта; запасной — `MetadataProvider.EMPTY`. +- Вариант B: `MetadataProviderFactory` по аналогии с `DialectFactory` + (`MetadataProvider create(DialectInitData)`), зарегистрированная параллельно. + +Важно: `Dialect` и `MetadataProvider` теперь **отдельные сервисы**. Кто ранее использовал `Dialect` +и для генерации SQL, и как провайдер метаданных, теперь держит **две** ссылки (диалект из `sql`, +провайдер из `jdbc.db`), обе разрешимы по одному `@DialectName`/имени продукта. + +--- + +## 4. Предусловие сборки + +`jdbc.db` (включая компаньоны) соберётся зелёным только когда доступны снапшоты `sql.api` + +`sql.dialect.*`: в SQL-репозитории сначала `mvn -q install`, затем `jdbc.db`. См. +[06](06-build-and-verification.md). diff --git a/docs/dialect-migration/ru/minimal-split/05-per-version-hardening.md b/docs/dialect-migration/ru/minimal-split/05-per-version-hardening.md new file mode 100644 index 0000000..03905cd --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/05-per-version-hardening.md @@ -0,0 +1,82 @@ + + +# 05 – Усиление по версиям (опционально, вариант O2) + +Назад к [плану минимального среза](README.md). + +**Опционально.** Эта фаза **не требуется** для минимального среза — после разделения ролей +([01](01-role-separation.md)) диалект уже свободен от `record`/`api.meta`. Она последовательно +воплощает исходную идею «**встроить диалекты статически по версиям, без JDBC-record для установки +свойств**» и дополнительно развязывает диалект от любого получения метаданных во время выполнения. + +--- + +## 1. Исходное состояние (обосновано) + +Генерация SQL сегодня читает только константы + один `int` `dialectVersion` (из `DialectInitData`). +Уже существуют два паттерна версий: + +- **Подкласс на вариант:** `Db2OldAs400Dialect extends Db2Dialect` (переопределяет + `allowsFromQuery()→false`), собственный `@DialectName("DB2_OLD_AS400")`. +- **Внутри метода:** `dialectVersion.isUnknownOrAtLeast(8,0)` (mysql/oracle/postgresql/mssql/mariadb/sqlite). + +`DialectInitData` кроме версии несёт ещё выводимые из метаданных поля (`readOnly`, +`supportedResultSetStyles`, `sqlKeywordsLower`, `maxColumnNameLength`), которые жёстко заданный +вариант мог бы предоставить как константы. + +--- + +## 2. Цель + +Вместо чтения `DialectInitData.fromConnection(...)` во время выполнения — фиксированные классы по +версиям с **вкомпилированными** свойствами; `DialectFactory` выбирает нужный класс только по имени +продукта + версии; доступ к `DatabaseMetaData` **больше не нужен**. + +``` +MySqlDialect (база, общая генерация SQL) + ├─ MySql57Dialect → dialectVersion=(5,7), возможности/ключевые слова как константы + └─ MySql8Dialect → dialectVersion=(8,0), рекурсивные CTE, Percentile, … +``` + +--- + +## 3. Шаги (на каждую БД, пример MySQL) + +1. Выявить внутриметодные ветвления (`isUnknownOrAtLeast(8,0)`) и перенести их в **фиксированные + переопределения** по классу версии (например, `cteGenerator()` в `MySql8Dialect` сразу возвращает + рекурсивный вариант). +2. Поля, выводимые из метаданных, внести константами в класс версии (символ кавычки, + `maxColumnNameLength`, при необходимости набор ключевых слов). +3. `MySqlDialectFactory.createDialect(DialectInitData init)` выбирает по `init.version()` / + `init.productVersion()` конкретный класс (запасной — новейшая известная версия при UNKNOWN). +4. `DialectInitData` остаётся носителем имени продукта/версии, но может создаваться полностью + **офлайн** (`ansiDefaults().withVersion(8,0).withQuoteIdentifierString("`")`). + +--- + +## 4. Компромисс + +| Плюс | Минус | +|---|---| +| Диалект полностью статичен, без `DatabaseMetaData` во время выполнения | Взрыв числа классов (несколько версий на БД) | +| Тестируем без живой БД (фиксированные варианты) | Логика выбора версии должна быть надёжной | +| Поведение по версии явно видно | Поддержка при новых версиях БД | + +**Рекомендация по порядку:** сначала завершить разделение ролей ([01](01-role-separation.md)) + +перенос (уже даёт минимальное ядро). Усиление по версиям — потом, **инкрементально по каждой БД**; оно +независимо и не является предпосылкой разделения. + +--- + +## 5. Приёмка (опционально) + +- [ ] По каждой усиленной БД: классы для конкретных версий с константными свойствами; нет ветвления + `isUnknownOrAtLeast` в горячем пути. +- [ ] `DialectFactory` выбирает корректно по имени продукта/версии; UNKNOWN → определённый запасной. +- [ ] Unit-тесты диалектов проходят **без** живой БД (только `ansiDefaults().withVersion(...)`). diff --git a/docs/dialect-migration/ru/minimal-split/06-build-and-verification.md b/docs/dialect-migration/ru/minimal-split/06-build-and-verification.md new file mode 100644 index 0000000..6d3df32 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/06-build-and-verification.md @@ -0,0 +1,109 @@ + + +# 06 – Сборка, Java 21 и проверка + +Назад к [плану минимального среза](README.md). + +--- + +## 1. Java 21 и pom-файлы + +Как в плане полного переноса ([../06-build-and-java21.md](../06-build-and-java21.md)): + +- Корневой pom SQL `java.version`/`java.release` на **21**. +- Новые агрегаторы `sql/dialect/pom.xml` (packaging=pom) + `sql/dialect/db/pom.xml` с ``: + `common test-support duckdb h2 mariadb mssqlserver mysql oracle postgresql sqlite`. +- Расширить корневой `` на `api`, `dialect` (**без** `record` — он остаётся в `jdbc.db`). +- Расширить `dependencyManagement` тест-драйверами/Testcontainers/JUnit из pom диалектов. +- Родитель `pom.parent:0.0.7`, `${revision}=0.0.1-SNAPSHOT`, снапшот-репозиторий без изменений. + +**Отличие от плана полного переноса:** модуля `sql/record` **нет** — `record` остаётся целиком в +`jdbc.db`. `sql/api` содержит только `schema`+`type`+`sql`. + +--- + +## 2. Порядок сборки + +```bash +# 1) собрать + установить SQL-репозиторий (предусловие для jdbc.db) +cd org.eclipse.daanse.sql && mvn -q clean install + +# 2) jdbc.db против свежих снапшотов sql.* (включая разделение api, компаньоны, impl, importer) +cd ../org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +`sql` **обязан** быть установлен до `jdbc.db` (одностороннее ребро `jdbc.db → sql`). + +--- + +## 3. Проверка + +### 3.1 Фаза R (разделение ролей, только `jdbc.db`, до любого переноса) + +```bash +cd org.eclipse.daanse.jdbc.db +# main диалектов свободен от record/api.meta: +grep -rE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main # → пусто +# Dialect больше не MetadataProvider: +grep -n "extends" dialect/api/src/main/java/org/eclipse/daanse/jdbc/db/dialect/api/Dialect.java # нет MetadataProvider +mvn -q verify # включая round-trip / MetaInfo-тесты против MetadataProvider +``` + +### 3.2 Структура после переноса + +```bash +# SQL-репозиторий без ссылок на jdbc.db: +grep -rl "org.eclipse.daanse.jdbc.db" org.eclipse.daanse.sql/ --include=*.java # → пусто +# сторона SQL без record/api.meta: +grep -rn "sql.record\|api\.meta" org.eclipse.daanse.sql/ --include=*.java # → пусто +# SQL-репозиторий без внешних зависимостей jdbc.db: +grep -rn "jdbc.db" org.eclipse.daanse.sql/ --include=pom.xml # → пусто +# jdbc.db сохраняет record/impl/importer/dialect.metadata: +grep -nE "(api|record|impl|importer|dialect)" org.eclipse.daanse.jdbc.db/pom.xml +``` + +### 3.3 Тесты + +- **SQL:** `statement/demo` (H2/MSSQL через Testcontainers), `guard/jsqltranspiler`, `deparser`, + unit-тесты диалектов (генератор/квотер) — зелёные. +- **jdbc.db:** тесты `impl` (интроспекция + MetaInfo против компаньонов), ETL-тесты `importer/csv` — + зелёные. + +--- + +## 4. Риски + +| Риск | Вероятность | Эффект | Мера | +|---|---|---|---| +| Слом SPI `Dialect` задевает внешних потребителей | средняя | ошибки компиляции у потребителей `dialect.getAll*` | роль A была внутренней (движок использует параметр `MetadataProvider`); предоставить компаньоны; мигрировать | +| Разрешение OSGi диалект ↔ провайдер | средняя | провайдер не найден во время выполнения | сохранить тег `@DialectName`; запасной `MetadataProvider.EMPTY` | +| Разделение `api` проводит границу неверно | низкая | цикл/ошибка компиляции | граница обоснована: `api.schema` не импортирует другие пакеты api | +| Round-trip-тесты держались на «диалект как провайдер» | средняя | ошибки тестов после разделения | перевести тесты на компаньоны ([04](04-consumer-rewiring.md) §2.1) | +| Порядок релизов | средняя | `jdbc.db` не собирается | сначала установить/опубликовать `sql` | + +--- + +## 5. Определение готовности + +**Рефакторинг (Фаза R):** +- [ ] `Dialect` без `extends MetadataProvider`; `jdbc.db` зелёный. +- [ ] 6 компаньонов `MetadataProvider` полны; main диалектов без `record`/`api.meta`. + +**Разделение и перенос:** +- [ ] `sql.api` = только schema/type/sql; **нет** `sql.record`. +- [ ] SQL-репозиторий собирается без зависимостей от `jdbc.db` (Java 21); сторона SQL без + `record`/`api.meta`. +- [ ] `jdbc.db` собирается против снапшотов `sql.*`; `record`, `api.meta`, `impl`, `importer`, + `dialect.metadata` остаются там. +- [ ] Односторонняя связь `jdbc.db → sql`, без цикла. +- [ ] Интеграционные тесты `statement/demo` (H2/MSSQL) + тесты `impl`/`importer` зелёные. +- [ ] CI лицензионных заголовков / Javadoc в обоих репозиториях зелёный. + +**Опционально (Фаза V):** см. [05](05-per-version-hardening.md). diff --git a/docs/dialect-migration/ru/minimal-split/IMPLEMENTATION-GUIDE.md b/docs/dialect-migration/ru/minimal-split/IMPLEMENTATION-GUIDE.md new file mode 100644 index 0000000..bd607fb --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/IMPLEMENTATION-GUIDE.md @@ -0,0 +1,712 @@ + + +# Руководство по внедрению (пошагово) — минимальный срез + версии диалектов + +Назад к [плану минимального среза](README.md). + +> 🌐 Язык: [Deutsch](../../minimal-split/IMPLEMENTATION-GUIDE.md) · **Русский** (этот файл) + +**Для кого?** Это руководство написано для **junior-разработчика**. Каждый шаг говорит **точно**: в +каком **репозитории**, каком **модуле**, каком **файле** и **что** ты делаешь, с **примерами кода** +(до/после) и **командой проверки**. Выполняй части **по порядку** и делай сборку **после каждой +контрольной точки**. Если контрольная точка красная — **не продолжай**, сначала почини. + +**Две цели этого руководства:** +1. **Минимальный срез** (части 1–4): минимально вынести стек диалектов из `jdbc.db` в `sql`. +2. **Версии диалектов** (часть 5): встроить диалекты статически по версиям (твоя идея — «без + JDBC-record для установки свойств»). + +В конце (часть 6) **всё собирается и работает** в обоих репозиториях. + +--- + +## Обозначения / соглашения + +- **Репозиторий `jdbc.db`** = `/home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db` +- **Репозиторий `sql`** = `/home/stbischof/git/daanse/org.eclipse.daanse.sql` +- Префикс пакета старый: `org.eclipse.daanse.jdbc.db.…` — новый (сторона SQL): `org.eclipse.daanse.sql.…` +- «Команда проверки» означает: выполнять в каталоге соответствующего репозитория. +- **Важно:** всегда используй `git mv` (не копирование+удаление) — это сохраняет историю Git. + +--- + +# Часть 0 — Подготовка + +### Шаг 0.1 — Оба репозитория, рабочие ветки + +**Репо:** оба. **Действие:** создать по ветке. + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && git checkout -b feat/minimal-split +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && git checkout -b feat/minimal-split +``` + +### Шаг 0.2 — Исходная сборка (должна быть зелёной до любых изменений) + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean install +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install +``` + +> Если здесь уже что-то красное — это **не** из-за тебя; сначала выясни с командой. + +### Шаг 0.3 — Запомни 3 группы диалектов + +- **6 «больших» диалектов** (имеют роль A метаданных): `h2, mariadb, mssqlserver, mysql, oracle, postgresql` +- **2 «маленьких» диалекта** (нет роли A): `duckdb, sqlite` +- **Инфраструктура:** `common, test-support` + +Компаньон в части 1 получают только **6 больших**. + +--- + +# Часть 1 — Фаза R: разделение ролей (целиком в репозитории `jdbc.db`) + +**Цель:** из каждого большого диалекта вынести методы «чтения метаданных» (роль A) в отдельный +класс-компаньон и отвязать `Dialect` от `MetadataProvider`. **Пока ничего не переезжает** — всё +остаётся в `jdbc.db`, но после этого должно собираться зелёным. + +### Шаг 1.1 — Создать новый модуль `dialect/metadata` + +**Репо:** `jdbc.db`. **Действие:** новая папка + агрегатор-pom. + +Новый файл: `dialect/metadata/pom.xml` +```xml + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect + ${revision} + ../pom.xml + + org.eclipse.daanse.jdbc.db.dialect.metadata + pom + Eclipse Daanse JDBC DB Dialect Metadata Providers + + mysql + mariadb + h2 + mssqlserver + oracle + postgresql + + +``` + +Изменить файл: `dialect/pom.xml` → добавить `metadata` в ``: +```xml + + api + db + metadata + +``` + +### Шаг 1.2 — Leaf-pom для первого компаньона (MySQL) + +**Репо:** `jdbc.db`. **Новый файл:** `dialect/metadata/mysql/pom.xml` +```xml + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.metadata + ${revision} + ../pom.xml + + org.eclipse.daanse.jdbc.db.dialect.metadata.mysql + Eclipse Daanse JDBC DB Metadata Provider MySQL + + org.slf4jslf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.api + ${revision} + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.record + ${revision} + + + + org.eclipse.daanse + org.eclipse.daanse.jdbc.db.dialect.api + ${revision} + + + +``` + +### Шаг 1.3 — Создать класс-компаньон `MySqlMetadataProvider` + +**Репо:** `jdbc.db`. **Новый файл:** +`dialect/metadata/mysql/src/main/java/org/eclipse/daanse/jdbc/db/dialect/metadata/mysql/MySqlMetadataProvider.java` + +Скопируй **заголовок EPL** из существующего файла в начало. Затем: + +```java +package org.eclipse.daanse.jdbc.db.dialect.metadata.mysql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.daanse.jdbc.db.api.MetadataProvider; +import org.eclipse.daanse.jdbc.db.api.meta.IndexInfo; +import org.eclipse.daanse.jdbc.db.api.meta.IndexInfoItem; +import org.eclipse.daanse.jdbc.db.api.schema.ColumnReference; +import org.eclipse.daanse.jdbc.db.api.schema.ImportedKey; +import org.eclipse.daanse.jdbc.db.api.schema.PrimaryKey; +import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference; +import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.jdbc.db.dialect.api.DialectName; +import org.eclipse.daanse.jdbc.db.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.jdbc.db.record.schema.IndexInfoRecord; +import org.eclipse.daanse.jdbc.db.record.schema.PrimaryKeyRecord; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** + * MySQL-native metadata provider. Extracted from MySqlDialect (role A) so that + * the SQL-generation dialect no longer depends on record/api.meta. + */ +@Component(service = MetadataProvider.class, scope = ServiceScope.SINGLETON) +@DialectName("MYSQL") +public class MySqlMetadataProvider implements MetadataProvider { + + // ---- ЗДЕСЬ: методы getAll*/get*, перенесённые из MySqlDialect ---- + // Пример: getAllPrimaryKeys (взято 1:1 из MySqlDialect) + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + // ... аналогично getAllImportedKeys, getAllExportedKeys, getAllIndexInfo, + // getUniqueConstraints, getAllTriggers, getAllSequences (все из MySqlDialect) ... + + // ---- ЗДЕСЬ: приватные помощники, нужные этим методам ---- + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) return schema; + String catalog = connection.getCatalog(); + if (catalog != null) return catalog; + return connection.getSchema() != null ? connection.getSchema() : ""; + } + + private static IndexInfoItem.IndexType mapMySqlIndexType(String indexType) { + if (indexType == null) return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + return switch (indexType.toUpperCase()) { + case "HASH" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + // Builder для составных первичных ключей — 1:1 из MySqlDialect + private static final class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; this.constraintName = constraintName; this.schemaName = schemaName; + } + void addColumn(String c) { columns.add(c); } + PrimaryKey build() { + TableReference table = new TableReference( + Optional.of(new SchemaReference(schemaName)), tableName); + List cols = new ArrayList<>(); + for (String c : columns) cols.add(new ColumnReference(Optional.of(table), c)); + return new PrimaryKeyRecord(table, cols, Optional.ofNullable(constraintName)); + } + } +} +``` + +> **Важно:** тела методов бери **дословно** из `MySqlDialect.java`. Перенеси **также** все приватные +> помощники, которые используются только этими методами (`readImportedKey`, `mapTriggerTiming`, +> `mapTriggerEvent`, `mapReferentialAction`, `mapMySqlIndexType`, `resolveSchema`, `PkBuilder` …). + +### Шаг 1.4 — Удалить перенесённые методы из `MySqlDialect` + +**Репо:** `jdbc.db`. **Файл:** +`dialect/db/mysql/src/main/java/.../mysql/MySqlDialect.java` + +1. Удалить все `@Override`-методы, скопированные в `MySqlMetadataProvider` (`getAllPrimaryKeys`, + `getAllIndexInfo`, `getAllImportedKeys`, …). +2. Удалить приватные помощники, используемые только там (`PkBuilder`, `resolveSchema`, + `mapMySqlIndexType`, …). +3. Удалить ставшие неиспользуемыми импорты — прежде всего все + `import org.eclipse.daanse.jdbc.db.record.*` и `import org.eclipse.daanse.jdbc.db.api.meta.*`. + +**Проверка (должно быть пусто):** +```bash +grep -nE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/mysql/src/main/java/org/eclipse/daanse/jdbc/db/dialect/db/mysql/MySqlDialect.java +``` + +> **Внимание, роль B:** импорты `…api.schema.*` (например `TableReference`, `ColumnDefinition`) +> **остаются** в классе диалекта, если используются в DDL-методах — они разрешены и позже переедут как +> остров в SQL. + +### Шаг 1.5 — Повторить для 5 остальных больших диалектов + +**Репо:** `jdbc.db`. Повтори шаги 1.2–1.4 для: `h2, mariadb, mssqlserver, oracle, postgresql`. Для +каждого — `dialect/metadata//…/MetadataProvider.java` с `@DialectName("")`. Значения +`@DialectName` смотри в соответствующей `DialectFactory` (например `H2`, `MARIADB`, `MSSQLSERVER`, +`ORACLE`, `POSTGRESQL`). + +> `duckdb`, `sqlite`, `common`: **ничего не делать** — у них нет методов роли A. + +### Шаг 1.6 — Отвязать `Dialect` от `MetadataProvider` + +**Репо:** `jdbc.db`. **Файл:** `dialect/api/src/main/java/.../dialect/api/Dialect.java` + +До: +```java +import org.eclipse.daanse.jdbc.db.api.MetadataProvider; +... +public interface Dialect + extends IdentifierQuoter, LiteralQuoter, DialectCapabilitiesProvider, TypeMapper, MetadataProvider { +``` +После: +```java +// (импорт удалён) +public interface Dialect + extends IdentifierQuoter, LiteralQuoter, DialectCapabilitiesProvider, TypeMapper { +``` + +`BestFitColumnType getType(ResultSetMetaData, int)` и импорты `api.type` **остаются**. + +### Шаг 1.7 — Перевести вызывающих и тесты на компаньон + +**Репо:** `jdbc.db`. + +- **`DatabaseServiceImpl`** (`impl/.../DatabaseServiceImpl.java`) **не** меняет сигнатуры — он + принимает `MetadataProvider` как параметр (`createMetaInfo(Connection, MetadataProvider)`, + `getCatalogs(Connection, MetadataProvider)`). Меняется только **точка вызова** — теперь передаётся + компаньон. +- **Тесты**, ранее передававшие диалект как провайдер, меняют одну строку: + +До: +```java +MetaInfo mi = service.createMetaInfo(conn, new MySqlDialect(init)); +``` +После: +```java +MetaInfo mi = service.createMetaInfo(conn, new MySqlMetadataProvider()); +``` + +> Для диалектов без нативных метаданных (duckdb/sqlite): используй `MetadataProvider.EMPTY`. + +### ✅ Контрольная точка 1 (репозиторий `jdbc.db` должен быть зелёным) + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db +# main диалектов свободен от record/api.meta: +grep -rE "import org.eclipse.daanse.jdbc.db.(record|api\.meta)\." \ + dialect/db/{h2,mariadb,mssqlserver,mysql,oracle,postgresql}/src/main # → пусто +# Dialect больше не MetadataProvider: +grep -n "extends" dialect/api/src/main/java/org/eclipse/daanse/jdbc/db/dialect/api/Dialect.java +mvn -q clean verify # должно быть зелёным +``` + +Если зелёно: **фаза R завершена.** Коммит: `git commit -am "Phase R: split MetadataProvider role out of dialects"`. + +--- + +# Часть 2 — Фаза S: разделение `api` (schema/type/sql → новый `sql.api`) + +**Цель:** только пакеты, нужные диалекту (`api.schema`, `api.type`, `api.sql`), становятся новым +модулем SQL. Остаток интроспекции (`api.meta`, `MetadataProvider`, `DatabaseService`, …) остаётся в +`jdbc.db`. + +### Шаг 2.1 — Создать модуль `sql/api` + +**Репо:** `sql`. **Новый файл:** `api/pom.xml` +```xml + + + org.eclipse.daanse + org.eclipse.daanse.sql + ${revision} + + org.eclipse.daanse.sql.api + Eclipse Daanse SQL API (schema/type/sql model) + + org.slf4jslf4j-api + + +``` +**Изменить файл:** `pom.xml` (корень SQL) → поставить `api` в начало ``: +```xml + + api + guard + deparser + statement + +``` + +### Шаг 2.2 — Перенести пакеты (`git mv`) + переименовать + +**Репо:** оба (источник `jdbc.db`, цель `sql`). Для каждого из 3 пакетов (`schema`, `type`, `sql`): + +```bash +# Пример для 'schema' — аналогично для 'type' и 'sql' +SRC=/home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db/api/src/main/java/org/eclipse/daanse/jdbc/db/api/schema +DST=/home/stbischof/git/daanse/org.eclipse.daanse.sql/api/src/main/java/org/eclipse/daanse/sql/api/schema +mkdir -p "$(dirname "$DST")" +``` + +> **Внимание:** `git mv` работает только внутри одного репозитория. Через границу репозиториев: +> перенеси папку через `mv`, затем в `jdbc.db` — `git rm -r` (старый путь), а в `sql` — `git add` +> (новый путь). История при этом через границу теряется — это нормально (см. план полного переноса +> [../07-anomalies-and-risks.md](../07-anomalies-and-risks.md) §5). + +Затем во **всех** перенесённых файлах заменить имя пакета: +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql/api +grep -rl "org.eclipse.daanse.jdbc.db.api.schema" . | xargs sed -i \ + 's/org\.eclipse\.daanse\.jdbc\.db\.api\.schema/org.eclipse.daanse.sql.api.schema/g' +# аналогично .type и .sql +``` +И поправить `package-info.java` в каждом из трёх пакетов. + +### Шаг 2.3 — Остаток `jdbc.db/api` направить на `sql.api` + +**Репо:** `jdbc.db`. Оставшийся модуль `api` (теперь только `api.meta` + верхний уровень интроспекции) +импортирует типы схемы. Перенаправить эти импорты: +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db/api +grep -rl "org.eclipse.daanse.jdbc.db.api.\(schema\|type\|sql\)" . | xargs sed -i \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.schema/org.eclipse.daanse.sql.api.schema/g' \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.type/org.eclipse.daanse.sql.api.type/g' \ + -e 's/org\.eclipse\.daanse\.jdbc\.db\.api\.sql/org.eclipse.daanse.sql.api.sql/g' +``` +**Файл:** `api/pom.xml` → добавить зависимость на новый модуль SQL: +```xml + + org.eclipse.daanse + org.eclipse.daanse.sql.api + 0.0.1-SNAPSHOT + +``` + +### Шаг 2.4 — Перевести `record`, `impl`, компаньоны на `sql.api` + +**Репо:** `jdbc.db`, модули `record`, `impl`, `dialect/metadata/*`. Та же замена `sed` импортов +`api.{schema,type,sql}`, что и в 2.3; в каждом `pom.xml` добавить зависимость `sql.api`. + +### ✅ Контрольная точка 2 + +```bash +# sql.api собирается изолированно: +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q -pl org.eclipse.daanse.sql.api -am install +# jdbc.db собирается против свежего снапшота sql.api: +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` +Коммит, если зелёно. + +--- + +# Часть 3 — Фаза D: перенос диалектов в `sql` + +**Цель:** `dialect.api` (теперь без MetadataProvider) + `common` + `test-support` + 8 активных +диалектов в `sql`. Им теперь **не** нужны `record` и `api.meta`. + +### Шаг 3.1 — Агрегаторы в SQL-репозитории + +**Репо:** `sql`. **Новые файлы:** `dialect/pom.xml` и `dialect/db/pom.xml` (packaging=pom). Шаблон см. +[06-build-and-verification.md](06-build-and-verification.md) §1. Добавить `dialect` в корневой +``. + +### Шаг 3.2 — Перенести + переименовать `dialect.api` + +**Репо:** оба. Папка `jdbc.db/dialect/api` → `sql/dialect/api`. Переименование пакета +`org.eclipse.daanse.jdbc.db.dialect.api` → `org.eclipse.daanse.sql.dialect.api`. **Pom:** родитель на +`org.eclipse.daanse.sql.dialect`, зависимость `jdbc.db.api` → `org.eclipse.daanse.sql.api` (внутри +реактора, `${project.version}`). + +### Шаг 3.3 — Перенести `common` и `test-support` + +**Репо:** оба. Как 3.2; зависимости → `sql.dialect.api` / `sql.api`. + +### Шаг 3.4 — Перенести 8 активных диалектов (mysql ПЕРЕД mariadb) + +**Репо:** оба. Порядок: `mysql`, затем `mariadb` (т. к. `mariadb → mysql`), затем `duckdb, h2, +mssqlserver, oracle, postgresql, sqlite` в любом порядке. + +По каждому диалекту: +1. Папка `jdbc.db/dialect/db/` → `sql/dialect/db/`. +2. Переименование пакета `…dialect.db.` → `…sql.dialect.db.`; импорты схемы + `…jdbc.db.api.schema` → `…sql.api.schema` (и т. д.). +3. **Переписать pom** — пример `mysql` (до/после ключевых зависимостей): + +До (`dialect/db/mysql/pom.xml`): +```xml +org.eclipse.daanse.jdbc.db.dialect.api${revision} +org.eclipse.daanse.jdbc.db.dialect.db.common${revision} +org.eclipse.daanse.jdbc.db.record${revision} +org.eclipse.daanse.jdbc.db.impl${revision}test +``` +После (`sql/dialect/db/mysql/pom.xml`): +```xml +org.eclipse.daanse.sql.dialect.api${project.version} +org.eclipse.daanse.sql.dialect.db.common${project.version} +org.eclipse.daanse.sql.api${project.version} + + +``` + +> Зависимости `record` и `impl`(test) **исчезают**, потому что роль метаданных (которая их требовала) +> в части 1 переехала в `jdbc.db/dialect/metadata`. Если тесту диалекта всё ещё нужен `impl` +> (round-trip), перенеси этот тест в `jdbc.db/dialect/metadata/` или временно пометь `@Disabled` +> (см. [01-role-separation.md](01-role-separation.md)). + +4. Внести `` в `sql/dialect/db/pom.xml`. + +### ✅ Контрольная точка 3 + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql +# В пакетах диалектов больше нет jdbc.db: +grep -rl "org.eclipse.daanse.jdbc.db" dialect/ --include=*.java # → пусто +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am install # собирается вместе с зависимостями +``` + +--- + +# Часть 4 — Фаза C: перепривязка потребителей + демонтаж `jdbc.db` + +### Шаг 4.1 — Потребители SQL внутри реактора + +**Репо:** `sql`, модули `guard`, `deparser`, `statement`. В их `pom.xml` заменить внешние +`jdbc.db.dialect.*` на внутренние `sql.dialect.*`; импорты Java `org.eclipse.daanse.jdbc.db.dialect` +→ `org.eclipse.daanse.sql.dialect`, `…jdbc.db.api.{schema,type,sql}` → `…sql.api.*`. Подробнее: +[04-consumer-rewiring.md](04-consumer-rewiring.md) §1. + +### Шаг 4.2 — Java 21 в SQL-репозитории + +**Репо:** `sql`. **Файл:** `pom.xml` (корень) → `java.version`/`java.release` на `21` (см. +[06-build-and-verification.md](06-build-and-verification.md) §1). + +### Шаг 4.3 — `jdbc.db` на опубликованные снапшоты `sql.*` + +**Репо:** `jdbc.db`, модули `impl`, `importer/csv`, `dialect/metadata/*`. В `pom.xml`: +зависимости диалектов/схемы на `sql.*` (внешние, `0.0.1-SNAPSHOT`). **Файл:** `jdbc.db/pom.xml` → +убрать из `` (переехавший) агрегат `dialect` или свести к `dialect/metadata`. + +### ✅ Контрольная точка 4 (соблюдай порядок!) + +```bash +# 1) сначала установить SQL: +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install +# 2) jdbc.db против снапшотов sql.*: +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +**На этом минимальный срез завершён.** Часть 5 — опциональное расширение с версиями диалектов. + +--- + +# Часть 5 — Фаза V: встроить версии диалектов статически (твоя идея, O2) + +**Цель:** вместо вычисления версии из `DialectInitData` во время выполнения +(`isUnknownOrAtLeast(8,0)`) — **фиксированные классы на версию** с вкомпилированными свойствами. +Пример MySQL. **Репо:** `sql`, модуль `sql/dialect/db/mysql`. + +### Шаг 5.1 — Определить базовый класс + +`MySqlDialect` остаётся **общей базой** (генерация SQL, кавычки). Сегодня внутри ветвится: +```java +// MySqlDialect.java (сегодня) +public CteGenerator cteGenerator() { + boolean supports = DialectVersion.UNKNOWN.equals(dialectVersion) || dialectVersion.atLeast(8, 0); + ... +} +public boolean requiresOrderByAlias() { return dialectVersion.isUnknownOrAtLeast(5, 7); } +``` + +### Шаг 5.2 — Создать два класса версий + +**Новый файл:** `…/mysql/MySql8Dialect.java` +```java +package org.eclipse.daanse.sql.dialect.db.mysql; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; + +/** MySQL 8.x — recursive CTE, ORDER BY alias required, percentile functions. */ +public class MySql8Dialect extends MySqlDialect { + public MySql8Dialect(DialectInitData init) { + super(init); + } + @Override public boolean requiresOrderByAlias() { return true; } // фиксировано, вместо проверки версии + // cteGenerator(): рекурсивный вариант — включить фиксировано + // supportsPercentileCont()/Disc(): true +} +``` +**Новый файл:** `…/mysql/MySql57Dialect.java` +```java +package org.eclipse.daanse.sql.dialect.db.mysql; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; + +/** MySQL 5.7 — no recursive CTE, ORDER BY alias required, no percentile. */ +public class MySql57Dialect extends MySqlDialect { + public MySql57Dialect(DialectInitData init) { + super(init); + } + @Override public boolean requiresOrderByAlias() { return true; } + // cteGenerator(): НЕ-рекурсивный вариант + // supportsPercentileCont()/Disc(): false +} +``` + +> Вынеси сегодняшние ветвления `dialectVersion.isUnknownOrAtLeast(...)` из `MySqlDialect` и сделай их +> **фиксированными** `@Override`-возвратами в классах версий. То, что не зависит от версии, остаётся в +> `MySqlDialect`. + +### Шаг 5.3 — Фабрика выбирает по версии + +**Файл:** `…/mysql/MySqlDialectFactory.java` + +До: +```java +@Override +public Function getConstructorFunction() { + return MySqlDialect::new; +} +``` +После (выбор вместо фиксированного конструктора): +```java +@Override +public Dialect createDialect(DialectInitData init) { + if (MySqlDialect.looksLikeInfobright(init)) { + throw new IllegalStateException("Snapshot looks like Infobright; use InfobrightDialectFactory"); + } + // Версия определяет конкретный класс; UNKNOWN → новейший известный (8.x) + var v = init.version(); + if (v.atLeast(8, 0) || v.equals(org.eclipse.daanse.sql.dialect.api.DialectVersion.UNKNOWN)) { + return new MySql8Dialect(init); + } + return new MySql57Dialect(init); +} +``` +`getConstructorFunction()` тогда больше не нужен (или возвращает базу для совместимости). + +### Шаг 5.4 — Unit-тест без живой БД + +**Новый файл:** `…/mysql/src/test/java/.../MySql8DialectTest.java` +```java +@Test +void mysql8_requiresOrderByAlias_true_offline() { + var init = DialectInitData.ansiDefaults() + .withQuoteIdentifierString("`") + .withVersion(8, 0); // чисто статически, БЕЗ Connection + var d = new MySql8Dialect(init); + assertThat(d.requiresOrderByAlias()).isTrue(); +} +``` + +### ✅ Контрольная точка 5 + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql +mvn -q -pl org.eclipse.daanse.sql.dialect.db.mysql -am verify # классы версий + офлайн-тесты зелёные +``` + +> Повторяй 5.1–5.4 для других БД только по необходимости — каждая БД независима. + +--- + +# Часть 6 — Финальная приёмка: всё собирается и работает + +### Шаг 6.1 — Полная сборка в правильном порядке + +```bash +cd /home/stbischof/git/daanse/org.eclipse.daanse.sql && mvn -q clean install # SQL сначала! +cd /home/stbischof/git/daanse/org.eclipse.daanse.jdbc.db && mvn -q clean verify +``` + +### Шаг 6.2 — Структурные проверки (все должны выдать «пусто») + +```bash +# В SQL-репозитории больше нет ссылок на jdbc.db: +grep -rl "org.eclipse.daanse.jdbc.db" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=*.java +# Сторона SQL без record/api.meta: +grep -rn "sql\.record\|api\.meta" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=*.java +# Pom-файлы SQL без зависимостей jdbc.db: +grep -rn "jdbc.db" /home/stbischof/git/daanse/org.eclipse.daanse.sql/ --include=pom.xml +``` + +### Шаг 6.3 — Важные тесты + +- **SQL:** `statement/demo` (H2 + MSSQL через Testcontainers), `guard`, `deparser`, тесты версий MySQL. +- **jdbc.db:** `impl` (интроспекция + MetaInfo против новых компаньонов), `importer/csv` (ETL). + +### Шаг 6.4 — Итоговый чек-лист + +- [ ] Оба репозитория собираются зелёными (SQL установлен первым). +- [ ] SQL-репозиторий **не** зависит от `jdbc.db`; код диалектов SQL без `record`/`api.meta`. +- [ ] `Dialect` больше не наследует `MetadataProvider`; 6 компаньонов в `jdbc.db/dialect/metadata`. +- [ ] Классы версий MySQL (`MySql8Dialect`/`MySql57Dialect`) + офлайн-тесты зелёные. +- [ ] Интеграционные тесты `statement/demo` проходят; тесты `impl`/`importer` зелёные. +- [ ] CI лицензионных заголовков / Javadoc в обоих репозиториях зелёный. + +**Когда все галочки стоят — готово.** Срез + версии диалектов внедрены, и всё работает. + +--- + +## Приложение — Если сборка стала красной (помощь junior'у) + +| Симптом | Вероятная причина | Исправление | +|---|---|---| +| `cannot find symbol: class PkBuilder` в `MySqlDialect` | помощник удалён, но ещё используется | проверь ссылки; помощник теперь в `MySqlMetadataProvider` | +| `package org.eclipse.daanse.jdbc.db.api.schema does not exist` (сторона SQL) | импорт не переименован | снова прогони замену `sed` на `sql.api.schema` | +| `record`/`api.meta` всё ещё в импортах диалекта | пропущен метод роли A | `grep` из контрольной точки 1; перенеси оставшийся метод | +| `jdbc.db` не находит `sql.api` | SQL не установлен | в SQL-репозитории `mvn -q clean install`, **потом** `jdbc.db` | +| OSGi: провайдер во время выполнения `EMPTY` | нет/неверный `@DialectName` у компаньона | проверь `@DialectName("MYSQL")` (то же значение, что у фабрики) | +| Ошибка цикла/реактора | модуль в неверном порядке/нет в `` | соблюдай порядок R→S→D; проверь `` агрегатора | diff --git a/docs/dialect-migration/ru/minimal-split/README.md b/docs/dialect-migration/ru/minimal-split/README.md new file mode 100644 index 0000000..1e7e687 --- /dev/null +++ b/docs/dialect-migration/ru/minimal-split/README.md @@ -0,0 +1,143 @@ + + +# Полный план: минимальный срез (разделение ролей) + +**Статус:** план / спецификация (перенос кода ещё не выполнен) +**Отношение:** реализация **варианта O1 (+ опционально O2)** из 2-го анализа +([../09-minimal-split-analysis.md](../09-minimal-split-analysis.md)). Альтернатива полному переносу +([../README.md](../README.md)). + +> 🌐 Язык: [Deutsch](../../minimal-split/README.md) · **Русский** (этот файл) + +Этот каталог — **исполняемый мастер-план** для *минимального* среза: вместо полного переноса +`api`+`record` (План 1) из диалектов **выносится роль выдачи метаданных**, так что ядро SQL +становится минимальным, а `record` + `api.meta` + движок интроспекции остаются в `jdbc.db`. + +--- + +## 1. Основная идея + +Конкретный диалект сегодня несёт **две роли** (обоснование: +[../09](../09-minimal-split-analysis.md)): + +- **Роль A — нативный провайдер метаданных:** `Dialect extends MetadataProvider`; 6 больших диалектов + переопределяют по 14–21 метода (`getAllIndexInfo`, `getAllPrimaryKeys`, …), которые читают + `information_schema` и строят объекты `record.schema`. → **интроспекция, принадлежит `jdbc.db`.** +- **Роль B — генерация SQL/DDL:** кавычки, генераторы, возможности, DDL/DML из словаря `api.schema`. + → **принадлежит `sql`.** + +**Ключевой рычаг:** `DatabaseServiceImpl` (движок интроспекции) уже потребляет `MetadataProvider` +как **параметр** (`createMetaInfo(Connection, MetadataProvider)`, `getCatalogs(Connection, +MetadataProvider)` → вызывает `provider.getAllIndexInfo(...)`). Он **не** знает `Dialect`. Роли +фактически уже разделены — их сваривает лишь наследование `Dialect extends MetadataProvider`. + +**План:** убрать наследование; методы роли A каждого диалекта вынести в отдельный класс-компаньон +`MetadataProvider implements MetadataProvider`, остающийся в `jdbc.db`; лёгкий диалект генерации +SQL — в `sql`. + +--- + +## 2. Результат: что переезжает / что остаётся + +**В `org.eclipse.daanse.sql` (минимальное ядро):** + +| В SQL | Содержание | +|---|---| +| `sql.api` (schema/type/sql) | `api.schema.*` (замкнутый `sealed`-остров), `api.type.*`, `api.sql.*` | +| `sql.dialect.api` | API диалектов **без** `extends MetadataProvider`; генераторы, возможности, квотеры, TypeMapper | +| `sql.dialect.db.*` | 8 активных диалектов **без** методов роли A | + +**Остаётся в `org.eclipse.daanse.jdbc.db`:** + +| Остаётся | Содержание | +|---|---| +| `jdbc.db.api` (остаток) | `api.meta.*`, `MetadataProvider`, `DatabaseService`, `MetaDataQueries`, `SnapshotBuilder` | +| `jdbc.db.record` | **целиком** (`record.schema` + `record.meta`) | +| `jdbc.db.impl` | движок интроспекции | +| `jdbc.db.dialect.metadata` (**новое**) | компаньоны `MetadataProvider` (вынесенные методы роли A) | +| `jdbc.db.importer` | ETL | + +`api.type`/`api.sql` всегда уходят в SQL; `impl`/`importer` всегда остаются. Результат: +**одностороннее `jdbc.db → sql`**, без цикла. + +> Сравнение объёмов: по сравнению с полным переносом (План 1) здесь остаются в `jdbc.db` **`record` +> целиком**, **`api.meta`** и интерфейсы интроспекции. Переезжают только `api.schema`+`type`+`sql` + +> диалекты. + +--- + +## 3. Целевая структура в SQL-репозитории + +``` +sql/ + api/ (новое — leaf: только schema/type/sql) + dialect/ (новое — агрегатор) + api/ (leaf — без наследования MetadataProvider) + db/ (агрегатор) + common/ test-support/ duckdb/ h2/ mariadb/ mssqlserver/ mysql/ oracle/ postgresql/ sqlite/ + deparser/ guard/ statement/ (существует — перевести на внутренние реактора) +``` + +Новое в `jdbc.db`: +``` +jdbc.db/ + dialect/metadata/ (новое — компаньоны MetadataProvider, OSGi-компоненты) + api/ record/ impl/ importer/ (остаются; api разделён → см. 02) +``` + +--- + +## 4. Фазы + +1. **Фаза R — разделение ролей** ([01](01-role-separation.md)): убрать `Dialect extends + MetadataProvider`; по каждому диалекту вынести методы роли A в `MetadataProvider`; перевести + вызывающих. *Внутри `jdbc.db`, ещё без переноса репозитория.* +2. **Фаза S — разделение API** ([02](02-target-structure.md)): разделить `api` на SQL-модель + (schema/type/sql) и остаток интроспекции (meta/верхний уровень); переименование пакетов стороны SQL. +3. **Фаза D — перенос диалектов** ([03](03-migration-procedure.md)): `dialect.api` + активные + `dialect.db.*` в SQL (теперь лёгкие, без роли A). +4. **Фаза C — потребители** ([04](04-consumer-rewiring.md)): потребители SQL и jdbc.db + OSGi заново. +5. **Фаза V — усиление по версиям** (*опционально*, [05](05-per-version-hardening.md)): O2. +6. **Фаза B — сборка/проверка** ([06](06-build-and-verification.md)): Java 21, pom, тесты. + +Порядок важен: **R перед S перед D** — сначала развязать (внутри `jdbc.db`, собирается зелёным), +потом разделить, потом переносить. + +--- + +## 5. Детальные документы + +| # | Документ | Содержание | +|---|---|---| +| 01 | [Разделение ролей](01-role-separation.md) | Ключевой рефакторинг: разделить `Dialect` ↔ `MetadataProvider`, классы-компаньоны, вызывающие | +| 02 | [Целевая структура и разделение API](02-target-structure.md) | Разделение `api`, раскладка пакетов/модулей, сопоставление переименований | +| 03 | [Процедура миграции](03-migration-procedure.md) | Пошагово, волны, порядок | +| 04 | [Потребители и OSGi](04-consumer-rewiring.md) | Потребители SQL, `impl`/`importer`, связывание OSGi | +| 05 | [Усиление по версиям (опция)](05-per-version-hardening.md) | O2: статические диалекты по версиям | +| 06 | [Сборка и проверка](06-build-and-verification.md) | Java 21, pom, объём тестов, риски, DoD | +| ⭐ | [**Руководство по внедрению (пошагово)**](IMPLEMENTATION-GUIDE.md) | Линейный путь для junior'а с примерами кода: срез **и** версии диалектов, в конце всё работает | + +--- + +## 6. Определение готовности + +**Рефакторинг (Фаза R, проверяется в одном `jdbc.db`):** +- [ ] `Dialect` больше не наследует `MetadataProvider`; `mvn -q verify` в `jdbc.db` зелёный. +- [ ] Для каждого из 6 больших диалектов есть `MetadataProvider` с вынесенными методами. +- [ ] Вызывающие `DatabaseServiceImpl` получают компаньон-провайдер (или `MetadataProvider.EMPTY`). +- [ ] Round-trip / MetaInfo-тесты зелёные против компаньонов. + +**Разделение и перенос (Фазы S–B):** +- [ ] SQL-репозиторий собирается с `sql.api` (только schema/type/sql) + `sql.dialect.*`, **без** + `record`/`api.meta`. +- [ ] `jdbc.db` собирается против опубликованных снапшотов `sql.*`; `record`, `api.meta`, `impl`, + `importer`, `dialect.metadata` остаются там. +- [ ] Односторонняя зависимость `jdbc.db → sql` (нет зависимости от `jdbc.db` в SQL-репозитории). +- [ ] Main-код диалектов в SQL-репозитории больше не импортирует `record` и `api.meta`. +- [ ] Интеграционные тесты `statement/demo` (H2/MSSQL) зелёные. diff --git a/guard/api/src/main/java/org/eclipse/daanse/sql/guard/api/SqlGuardFactory.java b/guard/api/src/main/java/org/eclipse/daanse/sql/guard/api/SqlGuardFactory.java index 605b8fa..46b5acb 100644 --- a/guard/api/src/main/java/org/eclipse/daanse/sql/guard/api/SqlGuardFactory.java +++ b/guard/api/src/main/java/org/eclipse/daanse/sql/guard/api/SqlGuardFactory.java @@ -15,7 +15,7 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.eclipse.daanse.sql.guard.api.elements.DatabaseCatalog; public interface SqlGuardFactory { diff --git a/guard/jsqltranspiler/pom.xml b/guard/jsqltranspiler/pom.xml index 2fe318a..7cf0c49 100644 --- a/guard/jsqltranspiler/pom.xml +++ b/guard/jsqltranspiler/pom.xml @@ -54,13 +54,13 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.h2 + org.eclipse.daanse.sql.dialect.db.h2 0.0.1-SNAPSHOT test org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.common + org.eclipse.daanse.sql.dialect.db.common 0.0.1-SNAPSHOT test diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java index c160b1a..5a842f7 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/DeparserColumResolver.java @@ -13,7 +13,7 @@ */ package org.eclipse.daanse.sql.guard.jsqltranspiler; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import ai.starlake.transpiler.JSQLColumResolver; diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java index 0956313..f916f54 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuard.java @@ -19,7 +19,7 @@ import java.util.Set; import java.util.regex.Pattern; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import org.eclipse.daanse.sql.guard.api.SqlGuard; import org.eclipse.daanse.sql.guard.api.elements.DatabaseCatalog; diff --git a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java index ccb0da3..fc38c41 100644 --- a/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java +++ b/guard/jsqltranspiler/src/main/java/org/eclipse/daanse/sql/guard/jsqltranspiler/TranspilerSqlGuardFactory.java @@ -15,7 +15,7 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; import org.eclipse.daanse.sql.deparser.api.DialectDeparser; import org.eclipse.daanse.sql.guard.api.SqlGuard; import org.eclipse.daanse.sql.guard.api.SqlGuardFactory; diff --git a/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java b/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java index 1b194d0..aa9f51b 100644 --- a/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java +++ b/guard/jsqltranspiler/src/test/java/org/eclipse/daanse/sql/guard/jsqltranspiler/integration/SqlGuardTest.java @@ -21,8 +21,8 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.dialect.db.h2.H2Dialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; import org.eclipse.daanse.sql.guard.api.SqlGuard; import org.eclipse.daanse.sql.guard.api.SqlGuardFactory; import org.eclipse.daanse.sql.guard.api.elements.DatabaseCatalog; diff --git a/guard/jsqltranspiler/test.bndrun b/guard/jsqltranspiler/test.bndrun index a9fefab..ed6794c 100644 --- a/guard/jsqltranspiler/test.bndrun +++ b/guard/jsqltranspiler/test.bndrun @@ -64,16 +64,15 @@ net.bytebuddy.byte-buddy-agent;version='[1.17.5,1.17.6)',\ net.sf.jsqlparser;version='[5.3.167,5.3.168)',\ org.apache.felix.scr;version='[2.2.10,2.2.11)',\ - org.eclipse.daanse.jdbc.db.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.jdbc.db.dialect.api;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.jdbc.db.dialect.db.common;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.jdbc.db.dialect.db.h2;version='[0.0.1,0.0.2)',\ - org.eclipse.daanse.jdbc.db.record;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.deparser.api;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.deparser.jsqlparser;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.h2;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.api;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.jsqltranspiler;version='[0.0.1,0.0.2)',\ org.eclipse.daanse.sql.guard.jsqltranspiler-tests;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ org.mockito.mockito-core;version='[4.9.0,4.9.1)',\ org.objenesis;version='[3.3.0,3.3.1)',\ org.opentest4j;version='[1.3.0,1.3.1)',\ diff --git a/guard/pom.xml b/guard/pom.xml index 4187630..eb59d43 100644 --- a/guard/pom.xml +++ b/guard/pom.xml @@ -44,7 +44,7 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.api + org.eclipse.daanse.sql.dialect.api 0.0.1-SNAPSHOT compile diff --git a/jdbc/api/pom.xml b/jdbc/api/pom.xml new file mode 100644 index 0000000..9a181b6 --- /dev/null +++ b/jdbc/api/pom.xml @@ -0,0 +1,36 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc + ${revision} + + org.eclipse.daanse.sql.jdbc.api + Eclipse Daanse SQL JDBC API + Core API definitions for JDBC database operations in Daanse. + Provides interfaces and contracts for database connectivity, SQL execution, + and database metadata access across different database systems and dialects. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + ${project.version} + + + + diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/DatabaseService.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/DatabaseService.java new file mode 100644 index 0000000..2f95173 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/DatabaseService.java @@ -0,0 +1,22 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api; + +/** + * Combined entry point for snapshot capture ({@link SnapshotBuilder}) and + * catalog queries ({@link MetaDataQueries}). Consumers that only need one facet + * should depend on the narrower interface. + */ +public interface DatabaseService extends SnapshotBuilder, MetaDataQueries { +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetaDataQueries.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetaDataQueries.java new file mode 100644 index 0000000..2abc8c0 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetaDataQueries.java @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.api; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; +import org.eclipse.daanse.sql.jdbc.api.schema.BestRowIdentifier; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.VersionColumn; + +/** + * JDBC catalog queries keyed on {@link Connection} + {@link MetadataProvider}. + * The provider serves cached/precomputed answers when available; the impl falls + * back to {@link DatabaseMetaData} reads when the provider has no answer. + */ +public interface MetaDataQueries { + + // --- Catalog / schema / type-info --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider; serves cached answers when + * available + * @return all catalogs visible to {@code connection} + * @throws SQLException on database access error + */ + List getCatalogs(Connection connection, MetadataProvider provider) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} for all + * @return schemas in {@code catalog} + * @throws SQLException on database access error + */ + List getSchemas(Connection connection, MetadataProvider provider, String catalog) + throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @return supported table-type names (e.g. {@code TABLE}, {@code VIEW}) + * @throws SQLException on database access error + */ + List getTableTypes(Connection connection, MetadataProvider provider) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @return type info entries describing the engine's SQL types + * @throws SQLException on database access error + */ + List getTypeInfo(Connection connection, MetadataProvider provider) throws SQLException; + + // --- Tables --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @param types table types to include or {@code null} for all + * @return matching tables + * @throws SQLException on database access error + */ + List getTableDefinitions(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String[] types) throws SQLException; + + /** @return all tables visible to {@code connection} */ + default List getTableDefinitions(Connection connection, MetadataProvider provider) + throws SQLException { + return getTableDefinitions(connection, provider, null, null, null, null); + } + + /** @return tables matching the given types */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + List types) throws SQLException { + String[] typesArr = types == null ? null : types.toArray(new String[0]); + return getTableDefinitions(connection, provider, null, null, null, typesArr); + } + + /** @return tables in {@code catalog} */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + CatalogReference catalog) throws SQLException { + return getTableDefinitions(connection, provider, catalog.name(), null, null, null); + } + + /** @return tables in {@code catalog} matching {@code types} */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + CatalogReference catalog, List types) throws SQLException { + String[] typesArr = types == null ? null : types.toArray(new String[0]); + return getTableDefinitions(connection, provider, catalog.name(), null, null, typesArr); + } + + /** @return tables in {@code schema} */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + SchemaReference schema) throws SQLException { + String catalog = schema.catalog().map(CatalogReference::name).orElse(null); + return getTableDefinitions(connection, provider, catalog, schema.name(), null, null); + } + + /** @return tables in {@code schema} matching {@code types} */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + SchemaReference schema, List types) throws SQLException { + String catalog = schema.catalog().map(CatalogReference::name).orElse(null); + String[] typesArr = types == null ? null : types.toArray(new String[0]); + return getTableDefinitions(connection, provider, catalog, schema.name(), null, typesArr); + } + + /** + * @return tables matching {@code table} (catalog/schema derived from + * {@code table.schema()}) + */ + default List getTableDefinitions(Connection connection, MetadataProvider provider, + TableReference table) throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return getTableDefinitions(connection, provider, catalog, schema, table.name(), new String[] { table.type() }); + } + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param table the table to test + * @return {@code true} when {@code table} resolves to an existing table + * @throws SQLException on database access error + */ + boolean tableExists(Connection connection, MetadataProvider provider, TableReference table) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @param types table types to include or {@code null} for all + * @return {@code true} when at least one table matches + * @throws SQLException on database access error + */ + boolean tableExists(Connection connection, MetadataProvider provider, String catalog, String schemaPattern, + String tableNamePattern, String[] types) throws SQLException; + + // --- Columns --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @param columnNamePattern column name pattern or {@code null} + * @return matching columns + * @throws SQLException on database access error + */ + List getColumnDefinitions(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException; + + /** @return all column definitions visible to {@code connection} */ + default List getColumnDefinitions(Connection connection, MetadataProvider provider) + throws SQLException { + return getColumnDefinitions(connection, provider, null, null, null, null); + } + + /** @return columns of {@code table} */ + default List getColumnDefinitions(Connection connection, MetadataProvider provider, + TableReference table) throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return getColumnDefinitions(connection, provider, catalog, schema, table.name(), null); + } + + /** @return column definitions matching {@code column} */ + default List getColumnDefinitions(Connection connection, MetadataProvider provider, + ColumnReference column) throws SQLException { + var tableOpt = column.table(); + String catalog = tableOpt.flatMap(TableReference::schema).flatMap(SchemaReference::catalog) + .map(CatalogReference::name).orElse(null); + String schema = tableOpt.flatMap(TableReference::schema).map(SchemaReference::name).orElse(null); + String tableName = tableOpt.map(TableReference::name).orElse(null); + return getColumnDefinitions(connection, provider, catalog, schema, tableName, column.name()); + } + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @param columnNamePattern column name pattern or {@code null} + * @return {@code true} when at least one column matches + * @throws SQLException on database access error + */ + boolean columnExists(Connection connection, MetadataProvider provider, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param column the column to test + * @return {@code true} when {@code column} resolves to an existing column + * @throws SQLException on database access error + */ + boolean columnExists(Connection connection, MetadataProvider provider, ColumnReference column) throws SQLException; + + // --- Foreign keys --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schema schema filter or {@code null} + * @param tableName table name (must be set) + * @return foreign keys this table imports + * @throws SQLException on database access error + */ + List getImportedKeys(Connection connection, MetadataProvider provider, String catalog, String schema, + String tableName) throws SQLException; + + /** @return foreign keys imported by {@code table} */ + default List getImportedKeys(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return getImportedKeys(connection, provider, catalog, schema, table.name()); + } + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schema schema filter or {@code null} + * @param tableName table name (must be set) + * @return foreign keys other tables export referencing this one + * @throws SQLException on database access error + */ + List getExportedKeys(Connection connection, MetadataProvider provider, String catalog, String schema, + String tableName) throws SQLException; + + /** @return foreign keys exported referencing {@code table} */ + default List getExportedKeys(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return getExportedKeys(connection, provider, catalog, schema, table.name()); + } + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param parentCatalog catalog of the parent table + * @param parentSchema schema of the parent table + * @param parentTable parent table name + * @param foreignCatalog catalog of the foreign table + * @param foreignSchema schema of the foreign table + * @param foreignTable foreign table name + * @return foreign-key constraints between the two tables + * @throws SQLException on database access error + */ + List getCrossReference(Connection connection, MetadataProvider provider, String parentCatalog, + String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) + throws SQLException; + + /** + * @return foreign-key constraints between {@code parentTable} and + * {@code foreignTable} + */ + default List getCrossReference(Connection connection, MetadataProvider provider, + TableReference parentTable, TableReference foreignTable) throws SQLException { + String parentCatalog = parentTable.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name) + .orElse(null); + String parentSchema = parentTable.schema().map(SchemaReference::name).orElse(null); + String foreignCatalog = foreignTable.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name) + .orElse(null); + String foreignSchema = foreignTable.schema().map(SchemaReference::name).orElse(null); + return getCrossReference(connection, provider, parentCatalog, parentSchema, parentTable.name(), foreignCatalog, + foreignSchema, foreignTable.name()); + } + + // --- Procedures / functions / UDTs --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @return stored procedures visible to {@code connection} + * @throws SQLException on database access error + */ + List getProcedures(Connection connection, MetadataProvider provider) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param procedureNamePattern procedure name pattern or {@code null} + * @return matching stored procedures + * @throws SQLException on database access error + */ + List getProcedures(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String procedureNamePattern) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @return user-defined functions visible to {@code connection} + * @throws SQLException on database access error + */ + List getFunctions(Connection connection, MetadataProvider provider) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param functionNamePattern function name pattern or {@code null} + * @return matching user-defined functions + * @throws SQLException on database access error + */ + List getFunctions(Connection connection, MetadataProvider provider, String catalog, String schemaPattern, + String functionNamePattern) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param typeNamePattern type name pattern or {@code null} + * @param types UDT type codes or {@code null} + * @return user-defined types + * @throws SQLException on database access error + */ + List getUDTs(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String typeNamePattern, int[] types) throws SQLException; + + // --- Row identification --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param table target table + * @param scope JDBC scope constant (e.g. + * {@link DatabaseMetaData#bestRowSession}) + * @param nullable whether nullable columns are eligible + * @return best row identifier columns + * @throws SQLException on database access error + */ + List getBestRowIdentifier(Connection connection, MetadataProvider provider, TableReference table, + int scope, boolean nullable) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param table target table + * @return version columns automatically updated when any row is updated + * @throws SQLException on database access error + */ + List getVersionColumns(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @param columnNamePattern column name pattern or {@code null} + * @return pseudo columns (system columns hidden from {@code SELECT *}) + * @throws SQLException on database access error + */ + List getPseudoColumns(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException; + + // --- Privileges --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @return table-level privileges + * @throws SQLException on database access error + */ + List getTablePrivileges(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param table target table + * @param columnNamePattern column name pattern or {@code null} + * @return column-level privileges + * @throws SQLException on database access error + */ + List getColumnPrivileges(Connection connection, MetadataProvider provider, TableReference table, + String columnNamePattern) throws SQLException; + + // --- Type hierarchy --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param typeNamePattern type name pattern or {@code null} + * @return UDT supertype links + * @throws SQLException on database access error + */ + List getSuperTypes(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String typeNamePattern) throws SQLException; + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schemaPattern schema name pattern or {@code null} + * @param tableNamePattern table name pattern or {@code null} + * @return table-supertable links + * @throws SQLException on database access error + */ + List getSuperTables(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException; + + // --- Partitions --- + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param table partitioned table + * @return partitions of {@code table}; empty when not partitioned or + * unsupported + * @throws SQLException on database access error + */ + default List getPartitions(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return provider.getPartitions(connection, catalog, schema, table.name()); + } + + /** + * @param connection the connection (not closed by this method) + * @param provider dialect-specific provider + * @param catalog catalog filter or {@code null} + * @param schema schema filter or {@code null} + * @return all partitions in the given scope; empty when nothing is partitioned + * @throws SQLException on database access error + */ + default List getAllPartitions(Connection connection, MetadataProvider provider, String catalog, + String schema) throws SQLException { + return provider.getAllPartitions(connection, catalog, schema); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java new file mode 100644 index 0000000..f14d4c4 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProvider.java @@ -0,0 +1,394 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; +import org.eclipse.daanse.sql.jdbc.api.schema.BestRowIdentifier; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.VersionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; + +public interface MetadataProvider { + + /** + * No-op provider that returns {@link Optional#empty()} for every query, forcing + * callers of {@link MetaDataQueries} modern methods to fall back to the JDBC + * path. Use when the dialect has no precomputed metadata to serve. + */ + MetadataProvider EMPTY = new MetadataProvider() { + }; + + /** + * @param catalog the catalog name, or null + * @return the index info list, or Optional.empty() to fall back to standard + * JDBC + * @throws SQLException on database access error + */ + default Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + return Optional.empty(); + } + + /** + * @param catalog the catalog name, or null + * @return the index info list, or Optional.empty() to fall back to standard + * JDBC + * @throws SQLException on database access error + */ + default Optional> getIndexInfo(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + return Optional.empty(); + } + + /** + * @param catalog the catalog name, or null + * @return the primary key list, or Optional.empty() to fall back to standard + * JDBC + * @throws SQLException on database access error + */ + default Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + return Optional.empty(); + } + + /** + * @param catalog the catalog name, or null + * @return the imported key list, or Optional.empty() to fall back to standard + * JDBC + * @throws SQLException on database access error + */ + default Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + return Optional.empty(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @return the partition list — empty when the engine has no partitions or the + * loader is not implemented for this dialect + * @throws SQLException on database access error + */ + default List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @param table the table name (must not be null) + * @return partitions of the table — empty when the table is not partitioned + * @throws SQLException on database access error + */ + default List getPartitions(Connection connection, String catalog, String schema, String table) + throws SQLException { + List all = getAllPartitions(connection, catalog, schema); + if (all.isEmpty()) { + return List.of(); + } + java.util.List result = new java.util.ArrayList<>(); + for (Partition p : all) { + if (table.equals(p.table().name())) { + result.add(p); + } + } + return List.copyOf(result); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllUserDefinedTypes(Connection connection, String catalog, String schema) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllMaterializedViews(Connection connection, String catalog, String schema) + throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + + /** + * @param catalog the catalog name, or null + * @throws SQLException on database access error + */ + default List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + return List.of(); + } + + /** Bulk alternative to {@link java.sql.DatabaseMetaData#getCatalogs()}. */ + default Optional> getAllCatalogs(Connection connection) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getSchemas(String, String)}. + */ + default Optional> getAllSchemas(Connection connection, String catalog) throws SQLException { + return Optional.empty(); + } + + /** Bulk alternative to {@link java.sql.DatabaseMetaData#getTableTypes()}. */ + default Optional> getAllTableTypes(Connection connection) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getTables(String, String, String, String[])}. + */ + default Optional> getAllTableDefinitions(Connection connection, String catalog, + String schemaPattern, String tableNamePattern, String[] types) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getColumns(String, String, String, String)}. + */ + default Optional> getAllColumnDefinitions(Connection connection, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return Optional.empty(); + } + + /** Bulk alternative to {@link java.sql.DatabaseMetaData#getTypeInfo()}. */ + default Optional> getAllTypeInfo(Connection connection) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getExportedKeys(String, String, String)} + * over a schema. + */ + default Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + return Optional.empty(); + } + + /** + * Per-table alternative to + * {@link java.sql.DatabaseMetaData#getExportedKeys(String, String, String)}. + */ + default Optional> getExportedKeys(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + return Optional.empty(); + } + + /** + * Alternative to + * {@link java.sql.DatabaseMetaData#getCrossReference(String, String, String, String, String, String)}. + */ + default Optional> getCrossReference(Connection connection, String parentCatalog, + String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) + throws SQLException { + return Optional.empty(); + } + + /** + * Per-procedure alternative to + * {@link java.sql.DatabaseMetaData#getProcedureColumns(String, String, String, String)}. + */ + default Optional> getProcedureColumns(Connection connection, String catalog, String schema, + String procedureName) throws SQLException { + return Optional.empty(); + } + + /** + * Per-function alternative to + * {@link java.sql.DatabaseMetaData#getFunctionColumns(String, String, String, String)}. + */ + default Optional> getFunctionColumns(Connection connection, String catalog, String schema, + String functionName) throws SQLException { + return Optional.empty(); + } + + /** + * Per-table alternative to + * {@link java.sql.DatabaseMetaData#getBestRowIdentifier(String, String, String, int, boolean)}. + */ + default Optional> getBestRowIdentifier(Connection connection, String catalog, String schema, + String tableName, int scope, boolean nullable) throws SQLException { + return Optional.empty(); + } + + /** + * Per-table alternative to + * {@link java.sql.DatabaseMetaData#getVersionColumns(String, String, String)}. + */ + default Optional> getVersionColumns(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getPseudoColumns(String, String, String, String)}. + */ + default Optional> getAllPseudoColumns(Connection connection, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getTablePrivileges(String, String, String)}. + */ + default Optional> getAllTablePrivileges(Connection connection, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + return Optional.empty(); + } + + /** + * Per-table alternative to + * {@link java.sql.DatabaseMetaData#getColumnPrivileges(String, String, String, String)}. + */ + default Optional> getColumnPrivileges(Connection connection, String catalog, String schema, + String tableName, String columnNamePattern) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getSuperTypes(String, String, String)}. + */ + default Optional> getAllSuperTypes(Connection connection, String catalog, String schemaPattern, + String typeNamePattern) throws SQLException { + return Optional.empty(); + } + + /** + * Bulk alternative to + * {@link java.sql.DatabaseMetaData#getSuperTables(String, String, String)}. + */ + default Optional> getAllSuperTables(Connection connection, String catalog, String schemaPattern, + String tableNamePattern) throws SQLException { + return Optional.empty(); + } + + default Optional> getAllUDTs(Connection connection, String catalog, String schemaPattern, + String typeNamePattern, int[] types) throws SQLException { + List list = getAllUserDefinedTypes(connection, catalog, schemaPattern); + return list.isEmpty() ? Optional.empty() : Optional.of(list); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProviderFactory.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProviderFactory.java new file mode 100644 index 0000000..a9178f4 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/MetadataProviderFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.api; + +/** + * Creates the engine-specific {@link MetadataProvider} for a database product — the reading-side + * counterpart of the SQL dialect factories. Implementations are published as OSGi services, one + * per engine; a consumer selects by {@link #supports(String)} against + * {@code DatabaseMetaData.getDatabaseProductName()}. + */ +public interface MetadataProviderFactory { + + /** + * Whether this factory serves the given database product + * ({@code DatabaseMetaData.getDatabaseProductName()}); implementations match + * case-insensitively and must disambiguate lookalike products (e.g. MariaDB reporting as + * "MySQL"). + */ + boolean supports(String databaseProductName); + + MetadataProvider createProvider(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/SnapshotBuilder.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/SnapshotBuilder.java new file mode 100644 index 0000000..0e53cf1 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/SnapshotBuilder.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.api; + +import java.sql.Connection; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; + +/** + * Captures a {@link MetaInfo} snapshot from a JDBC source. Narrow facet of + * {@link DatabaseService} for callers that only need the snapshot path. + */ +public interface SnapshotBuilder { + + /** + * @param dataSource pooled or unpooled source from which a {@link Connection} + * is borrowed and immediately released + * @return MetaInfo snapshot + * @throws SQLException on database access error + */ + MetaInfo createMetaInfo(DataSource dataSource) throws SQLException; + + /** + * @param connection caller-managed connection (not closed by this method) + * @return MetaInfo snapshot + * @throws SQLException on database access error + */ + MetaInfo createMetaInfo(Connection connection) throws SQLException; + + /** + * @param dataSource pooled or unpooled source + * @param metadataProvider dialect-specific override; default implementation + * ignores it and delegates to + * {@link #createMetaInfo(DataSource)} + * @return MetaInfo snapshot + * @throws SQLException on database access error + */ + default MetaInfo createMetaInfo(DataSource dataSource, MetadataProvider metadataProvider) throws SQLException { + return createMetaInfo(dataSource); + } + + /** + * @param connection caller-managed connection (not closed by this method) + * @param metadataProvider dialect-specific override; default implementation + * ignores it and delegates to + * {@link #createMetaInfo(Connection)} + * @return MetaInfo snapshot + * @throws SQLException on database access error + */ + default MetaInfo createMetaInfo(Connection connection, MetadataProvider metadataProvider) throws SQLException { + return createMetaInfo(connection); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/DatabaseInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/DatabaseInfo.java new file mode 100644 index 0000000..4cec2f2 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/DatabaseInfo.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +public interface DatabaseInfo { + + /** @return the underlying database's major version */ + int databaseMajorVersion(); + + /** @return underlying database's minor version */ + int databaseMinorVersion(); + + String databaseProductName(); + + String databaseProductVersion(); + +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IdentifierInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IdentifierInfo.java new file mode 100644 index 0000000..e004190 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IdentifierInfo.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.util.List; +import java.util.Set; + +public interface IdentifierInfo { + + /** @return the quoting string or a space if quoting is not supported */ + String quoteString(); + + int maxColumnNameLength(); + + boolean readOnly(); + + Set> supportedResultSetStyles(); + +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfo.java new file mode 100644 index 0000000..17a4ec4 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfo.java @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.util.List; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +public interface IndexInfo { + + TableReference tableReference(); + + List indexInfoItems(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfoItem.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfoItem.java new file mode 100644 index 0000000..860fdf5 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/IndexInfoItem.java @@ -0,0 +1,66 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.sql.DatabaseMetaData; +import java.util.Optional; +import java.util.stream.Stream; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +public interface IndexInfoItem { + + Optional indexName(); + + IndexType type(); + + Optional column(); + + int ordinalPosition(); + + Optional ascending(); + + long cardinality(); + + long pages(); + + Optional filterCondition(); + + boolean unique(); + + enum IndexType { + TABLE_INDEX_STATISTIC(DatabaseMetaData.tableIndexStatistic), + TABLE_INDEX_CLUSTERED(DatabaseMetaData.tableIndexClustered), + TABLE_INDEX_HASHED(DatabaseMetaData.tableIndexHashed), TABLE_INDEX_OTHER(DatabaseMetaData.tableIndexOther); + + private final int value; + + IndexType(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static IndexType of(int value) { + return Stream.of(IndexType.values()).filter(t -> t.value == value).findFirst().orElse(TABLE_INDEX_OTHER); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/MetaInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/MetaInfo.java new file mode 100644 index 0000000..dd14eeb --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/MetaInfo.java @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.util.List; + +public interface MetaInfo { + + DatabaseInfo databaseInfo(); + + IdentifierInfo identifierInfo(); + + List typeInfos(); + + StructureInfo structureInfo(); + + List indexInfos(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java new file mode 100644 index 0000000..b824ae2 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/StructureInfo.java @@ -0,0 +1,98 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.util.List; + +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; + +public interface StructureInfo { + List catalogs(); + + List schemas(); + + List tables(); + + List columns(); + + /** @return the imported keys (foreign key constraints) */ + List importedKeys(); + + List primaryKeys(); + + /** @return the triggers, empty list if not available */ + default List triggers() { + return List.of(); + } + + /** @return the sequences, empty list if not available */ + default List sequences() { + return List.of(); + } + + /** @return the check constraints, empty list if not available */ + default List checkConstraints() { + return List.of(); + } + + /** @return the unique constraints, empty list if not available */ + default List uniqueConstraints() { + return List.of(); + } + + /** @return the user-defined types, empty list if not available */ + default List userDefinedTypes() { + return List.of(); + } + + /** @return the view definitions, empty list if not available */ + default List viewDefinitions() { + return List.of(); + } + + /** @return the procedures, empty list if not available */ + default List procedures() { + return List.of(); + } + + /** @return the functions, empty list if not available */ + default List functions() { + return List.of(); + } + + /** @return the materialized views, empty list if not available */ + default List materializedViews() { + return List.of(); + } + + /** @return the partitions, empty list if not available */ + default List partitions() { + return List.of(); + } + +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/TypeInfo.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/TypeInfo.java new file mode 100644 index 0000000..a22e456 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/TypeInfo.java @@ -0,0 +1,86 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.meta; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.Optional; +import java.util.stream.Stream; + +public interface TypeInfo { + + String typeName(); + + JDBCType dataType(); + + int precision(); + + Optional literalPrefix(); + + Optional literalSuffix(); + + Optional createParams(); + + Nullable nullable(); + + boolean caseSensitive(); + + Searchable searchable(); + + boolean unsignedAttribute(); + + boolean fixedPrecScale(); + + boolean autoIncrement(); + + Optional localTypeName(); + + short minimumScale(); + + short maximumScale(); + + int numPrecRadix(); + + enum Nullable { + NO_NULLS(DatabaseMetaData.typeNoNulls), NULLABLE(DatabaseMetaData.typeNullable), + UNKNOWN(DatabaseMetaData.typeNullableUnknown); + + int value; + + Nullable(int value) { + this.value = value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Nullable of(int value) { + return Stream.of(Nullable.values()).filter(n -> n.value == value).findAny().orElse(null); + } + } + + enum Searchable { + PRED_NONE(DatabaseMetaData.typePredNone), PRED_CHAR(DatabaseMetaData.typePredChar), + PRED_BASIC(DatabaseMetaData.typePredBasic), SEARCHABLE(DatabaseMetaData.typeSearchable); + + int value; + + Searchable(int value) { + this.value = value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Searchable of(int value) { + return Stream.of(Searchable.values()).filter(n -> n.value == value).findAny().orElse(null); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/package-info.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/package-info.java new file mode 100644 index 0000000..e453bf6 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/meta/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.api.meta; diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/package-info.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/package-info.java new file mode 100644 index 0000000..fb44f99 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.api; diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/BestRowIdentifier.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/BestRowIdentifier.java new file mode 100644 index 0000000..5177cba --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/BestRowIdentifier.java @@ -0,0 +1,95 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.OptionalInt; +import java.util.stream.Stream; + +public interface BestRowIdentifier { + + /** The column. */ + ColumnReference column(); + + /** Actual scope of the identifier's validity. */ + Scope scope(); + + /** SQL/JDBC type of the column. */ + JDBCType dataType(); + + /** Database-specific type name. */ + String typeName(); + + /** Precision / size of the column. */ + OptionalInt columnSize(); + + /** The number of fractional digits, if applicable. */ + OptionalInt decimalDigits(); + + /** Whether the column is a pseudo column (e.g. Oracle ROWID). */ + PseudoColumnKind pseudoColumn(); + + /** Scope of the identifier (how long it remains valid). */ + enum Scope { + TEMPORARY(DatabaseMetaData.bestRowTemporary), TRANSACTION(DatabaseMetaData.bestRowTransaction), + SESSION(DatabaseMetaData.bestRowSession); + + private final int value; + + Scope(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Scope of(int value) { + return Stream.of(values()).filter(s -> s.value == value).findFirst().orElse(SESSION); + } + } + + /** Pseudo-column categorization. */ + enum PseudoColumnKind { + UNKNOWN(DatabaseMetaData.bestRowUnknown), NOT_PSEUDO(DatabaseMetaData.bestRowNotPseudo), + PSEUDO(DatabaseMetaData.bestRowPseudo); + + private final int value; + + PseudoColumnKind(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static PseudoColumnKind of(int value) { + return Stream.of(values()).filter(k -> k.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/CheckConstraint.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/CheckConstraint.java new file mode 100644 index 0000000..01fd744 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/CheckConstraint.java @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +public non-sealed interface CheckConstraint extends Constraint { + + String name(); + + TableReference table(); + + String checkClause(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ColumnPrivilege.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ColumnPrivilege.java new file mode 100644 index 0000000..ba6e888 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ColumnPrivilege.java @@ -0,0 +1,36 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.util.Optional; + +public interface ColumnPrivilege { + + /** The column. */ + ColumnReference column(); + + /** Principal that granted the privilege (or empty if unknown). */ + Optional grantor(); + + /** Principal that received the privilege. */ + String grantee(); + + /** Privilege kind: SELECT, INSERT, UPDATE, REFERENCES, ... */ + String privilege(); + + /** "YES", "NO", or empty if unknown. */ + Optional isGrantable(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Constraint.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Constraint.java new file mode 100644 index 0000000..a6eea78 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Constraint.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +public sealed interface Constraint permits ImportedKey, UniqueConstraint, CheckConstraint { +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/DropImportedKey.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/DropImportedKey.java new file mode 100644 index 0000000..c1b7659 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/DropImportedKey.java @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public interface DropImportedKey extends Named { + + TableReference table(); + +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Function.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Function.java new file mode 100644 index 0000000..16840b4 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Function.java @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import java.sql.DatabaseMetaData; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public non-sealed interface Function extends SchemaObject { + + FunctionReference reference(); + + FunctionType functionType(); + + Optional remarks(); + + List columns(); + + /** @return the function body, or empty if not available */ + default Optional body() { + return Optional.empty(); + } + + /** @return the full CREATE FUNCTION definition, or empty if not available */ + default Optional fullDefinition() { + return Optional.empty(); + } + + /** @return the last-modified time, or empty if not available */ + default Optional lastModified() { + return Optional.empty(); + } + + enum FunctionType { + UNKNOWN(DatabaseMetaData.functionResultUnknown), NO_TABLE(DatabaseMetaData.functionNoTable), + RETURNS_TABLE(DatabaseMetaData.functionReturnsTable); + + private final int value; + + FunctionType(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static FunctionType of(int value) { + return Stream.of(FunctionType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionColumn.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionColumn.java new file mode 100644 index 0000000..ea36619 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionColumn.java @@ -0,0 +1,95 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Stream; + +public interface FunctionColumn extends Named { + + ColumnType columnType(); + + JDBCType dataType(); + + String typeName(); + + OptionalInt precision(); + + OptionalInt scale(); + + OptionalInt radix(); + + /** @return the nullability */ + Nullability nullable(); + + Optional remarks(); + + OptionalInt charOctetLength(); + + int ordinalPosition(); + + enum ColumnType { + UNKNOWN(DatabaseMetaData.functionColumnUnknown), IN(DatabaseMetaData.functionColumnIn), + INOUT(DatabaseMetaData.functionColumnInOut), OUT(DatabaseMetaData.functionColumnOut), + RETURN(DatabaseMetaData.functionReturn), RESULT(DatabaseMetaData.functionColumnResult); + + private final int value; + + ColumnType(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static ColumnType of(int value) { + return Stream.of(ColumnType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } + + enum Nullability { + NO_NULLS(DatabaseMetaData.functionNoNulls), NULLABLE(DatabaseMetaData.functionNullable), + UNKNOWN(DatabaseMetaData.functionNullableUnknown); + + private final int value; + + Nullability(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Nullability of(int value) { + return Stream.of(Nullability.values()).filter(n -> n.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionReference.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionReference.java new file mode 100644 index 0000000..83c22be --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/FunctionReference.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public record FunctionReference(Optional schema, String name, String specificName) implements Named { + + public FunctionReference(Optional schema, String name) { + this(schema, name, name); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ImportedKey.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ImportedKey.java new file mode 100644 index 0000000..317c7a5 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ImportedKey.java @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.sql.DatabaseMetaData; +import java.util.Optional; +import java.util.stream.Stream; + +public non-sealed interface ImportedKey extends Named, Constraint { + + /** @return the primary key column reference */ + ColumnReference primaryKeyColumn(); + + /** @return the foreign key column reference */ + ColumnReference foreignKeyColumn(); + + /** @return sequence number within the foreign key */ + int keySequence(); + + ReferentialAction updateRule(); + + ReferentialAction deleteRule(); + + Optional primaryKeyName(); + + Deferrability deferrability(); + + enum ReferentialAction { + NO_ACTION(DatabaseMetaData.importedKeyNoAction), CASCADE(DatabaseMetaData.importedKeyCascade), + SET_NULL(DatabaseMetaData.importedKeySetNull), SET_DEFAULT(DatabaseMetaData.importedKeySetDefault), + RESTRICT(DatabaseMetaData.importedKeyRestrict); + + private final int value; + + ReferentialAction(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static ReferentialAction of(int value) { + return Stream.of(ReferentialAction.values()).filter(r -> r.value == value).findFirst().orElse(NO_ACTION); + } + } + + enum Deferrability { + INITIALLY_DEFERRED(DatabaseMetaData.importedKeyInitiallyDeferred), + INITIALLY_IMMEDIATE(DatabaseMetaData.importedKeyInitiallyImmediate), + NOT_DEFERRABLE(DatabaseMetaData.importedKeyNotDeferrable); + + private final int value; + + Deferrability(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Deferrability of(int value) { + return Stream.of(Deferrability.values()).filter(d -> d.value == value).findFirst().orElse(NOT_DEFERRABLE); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/MaterializedView.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/MaterializedView.java new file mode 100644 index 0000000..133aa58 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/MaterializedView.java @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +import java.time.Instant; +import java.util.Optional; + +public non-sealed interface MaterializedView extends SchemaObject { + + /** @return the table reference for the view */ + TableReference view(); + + /** @return the table reference for the view */ + default TableReference reference() { + return view(); + } + + /** @return the defining SELECT, or empty */ + Optional viewBody(); + + /** @return the full CREATE MATERIALIZED VIEW definition, or empty */ + Optional fullDefinition(); + + /** @return the refresh mode, or empty if not applicable */ + Optional refreshMode(); + + /** @return the last refresh time, or empty */ + Optional lastRefresh(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Partition.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Partition.java new file mode 100644 index 0000000..4b0048b --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Partition.java @@ -0,0 +1,46 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +import java.util.Optional; + +public interface Partition { + + String name(); + + /** Table this partition belongs to. */ + TableReference table(); + + Optional ordinalPosition(); + + /** The partitioning strategy. */ + PartitionMethod method(); + + Optional expression(); + + Optional description(); + + Optional rowCount(); + + Optional parentPartitionName(); + + Optional subPartitionMethod(); + + /** + * Sub-partitioning key expression, paired with {@link #subPartitionMethod()}. + */ + Optional subPartitionExpression(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PartitionMethod.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PartitionMethod.java new file mode 100644 index 0000000..7a9a408 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PartitionMethod.java @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +/** Strategy used to assign rows to a table partition. */ +public enum PartitionMethod { + + /** Partitions defined by contiguous value ranges of the partition key. */ + RANGE, + + /** + * Partitions defined by an explicit set of values (each value to one + * partition). + */ + LIST, + + /** Partitions chosen by a hash of the partition key (engine-defined hash). */ + HASH, + + /** + * MySQL {@code PARTITION BY KEY} — like {@link #HASH} but using the engine's + * internal hash. + */ + KEY, + + /** + * MySQL {@code PARTITION BY LINEAR HASH} — power-of-two-friendly variant of + * {@link #HASH}. + */ + LINEAR_HASH, + + /** + * MySQL {@code PARTITION BY LINEAR KEY} — power-of-two-friendly variant of + * {@link #KEY}. + */ + LINEAR_KEY, + + /** Engine-specific or unrecognized partition method. */ + OTHER +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Procedure.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Procedure.java new file mode 100644 index 0000000..2a88533 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Procedure.java @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import java.sql.DatabaseMetaData; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +public non-sealed interface Procedure extends SchemaObject { + + ProcedureReference reference(); + + ProcedureType procedureType(); + + Optional remarks(); + + List columns(); + + /** @return the procedure body, or empty if not available */ + default Optional body() { + return Optional.empty(); + } + + /** @return the full CREATE PROCEDURE definition, or empty if not available */ + default Optional fullDefinition() { + return Optional.empty(); + } + + /** @return the last-modified time, or empty if not available */ + default Optional lastModified() { + return Optional.empty(); + } + + enum ProcedureType { + UNKNOWN(DatabaseMetaData.procedureResultUnknown), NO_RESULT(DatabaseMetaData.procedureNoResult), + RETURNS_RESULT(DatabaseMetaData.procedureReturnsResult); + + private final int value; + + ProcedureType(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static ProcedureType of(int value) { + return Stream.of(ProcedureType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureColumn.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureColumn.java new file mode 100644 index 0000000..149080b --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureColumn.java @@ -0,0 +1,95 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Stream; + +public interface ProcedureColumn extends Named { + + ColumnType columnType(); + + JDBCType dataType(); + + String typeName(); + + OptionalInt precision(); + + OptionalInt scale(); + + OptionalInt radix(); + + /** @return the nullability */ + Nullability nullable(); + + Optional remarks(); + + Optional columnDefault(); + + int ordinalPosition(); + + enum ColumnType { + UNKNOWN(DatabaseMetaData.procedureColumnUnknown), IN(DatabaseMetaData.procedureColumnIn), + INOUT(DatabaseMetaData.procedureColumnInOut), OUT(DatabaseMetaData.procedureColumnOut), + RETURN(DatabaseMetaData.procedureColumnReturn), RESULT(DatabaseMetaData.procedureColumnResult); + + private final int value; + + ColumnType(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static ColumnType of(int value) { + return Stream.of(ColumnType.values()).filter(t -> t.value == value).findFirst().orElse(UNKNOWN); + } + } + + enum Nullability { + NO_NULLS(DatabaseMetaData.procedureNoNulls), NULLABLE(DatabaseMetaData.procedureNullable), + UNKNOWN(DatabaseMetaData.procedureNullableUnknown); + + private final int value; + + Nullability(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Nullability of(int value) { + return Stream.of(Nullability.values()).filter(n -> n.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureReference.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureReference.java new file mode 100644 index 0000000..8513f92 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ProcedureReference.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public record ProcedureReference(Optional schema, String name, String specificName) implements Named { + + public ProcedureReference(Optional schema, String name) { + this(schema, name, name); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PseudoColumn.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PseudoColumn.java new file mode 100644 index 0000000..0d2bafb --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/PseudoColumn.java @@ -0,0 +1,50 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +public interface PseudoColumn { + + /** The pseudo column. */ + ColumnReference column(); + + /** SQL/JDBC type of the column. */ + JDBCType dataType(); + + /** Precision / size of the column. */ + OptionalInt columnSize(); + + /** Number of fractional digits, if applicable. */ + OptionalInt decimalDigits(); + + /** Radix used to express precision. */ + OptionalInt numPrecRadix(); + + /** Comment on the column. */ + Optional remarks(); + + /** Max bytes (for character/binary). */ + OptionalInt charOctetLength(); + + /** Whether the column accepts NULL values: "YES", "NO" or empty for unknown. */ + Optional isNullable(); + + /** Classification of the pseudo column usage. */ + String columnUsage(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java new file mode 100644 index 0000000..54641bd --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SchemaObject.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +public sealed interface SchemaObject permits TableDefinition, ViewDefinition, MaterializedView, Sequence, Function, + Procedure, UserDefinedType { +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Sequence.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Sequence.java new file mode 100644 index 0000000..2833878 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/Sequence.java @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public non-sealed interface Sequence extends SchemaObject, Named { + + SequenceReference reference(); + + @Override + default String name() { + return reference().name(); + } + + /** Convenience: the schema of this sequence. */ + default Optional schema() { + return reference().schema(); + } + + long startValue(); + + long incrementBy(); + + Optional minValue(); + + Optional maxValue(); + + boolean cycle(); + + Optional cacheSize(); + + Optional dataType(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SequenceReference.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SequenceReference.java new file mode 100644 index 0000000..0da0155 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SequenceReference.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public record SequenceReference(Optional schema, String name) implements Named { + + /** Convenience: an unqualified sequence (no schema). */ + public SequenceReference(String name) { + this(Optional.empty(), name); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperTable.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperTable.java new file mode 100644 index 0000000..ed37180 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperTable.java @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +public interface SuperTable { + + /** The sub-table. */ + TableReference table(); + + /** The direct super-table name (same catalog/schema as the sub-table). */ + String superTableName(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperType.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperType.java new file mode 100644 index 0000000..cf9d512 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/SuperType.java @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public interface SuperType { + + /** The sub-type. */ + String typeName(); + + /** Schema of the sub-type. */ + Optional typeSchema(); + + /** The direct super-type name. */ + String superTypeName(); + + /** Schema of the super-type. */ + Optional superTypeSchema(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableDefinition.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableDefinition.java new file mode 100644 index 0000000..67fe410 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableDefinition.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +public non-sealed interface TableDefinition extends SchemaObject { + + TableReference table(); + + TableMetaData tableMetaData(); + + default TableReference reference() { + return table(); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableMetaData.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableMetaData.java new file mode 100644 index 0000000..accc281 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TableMetaData.java @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import java.util.Optional; + +public interface TableMetaData { + + Optional remarks(); + + Optional typeCatalog(); + + Optional typeSchema(); + + Optional typeName(); + + Optional selfReferencingColumnName(); + + Optional refGeneration(); + +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TablePrivilege.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TablePrivilege.java new file mode 100644 index 0000000..c50d289 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/TablePrivilege.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +import java.util.Optional; + +public interface TablePrivilege { + + /** The table. */ + TableReference table(); + + /** Principal that granted the privilege (or empty if unknown). */ + Optional grantor(); + + /** Principal that received the privilege. */ + String grantee(); + + String privilege(); + + /** "YES", "NO", or empty if unknown. */ + Optional isGrantable(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UniqueConstraint.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UniqueConstraint.java new file mode 100644 index 0000000..71e53ca --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UniqueConstraint.java @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.util.List; + +public non-sealed interface UniqueConstraint extends Constraint { + + String name(); + + TableReference table(); + + /** @return ordered list of column references */ + List columns(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedType.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedType.java new file mode 100644 index 0000000..46ac48e --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedType.java @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.sql.JDBCType; +import java.util.Optional; + +public non-sealed interface UserDefinedType extends SchemaObject, Named { + + UserDefinedTypeReference reference(); + + @Override + default String name() { + return reference().name(); + } + + /** Convenience: the schema of this user-defined type. */ + default Optional schema() { + return reference().schema(); + } + + String className(); + + JDBCType baseType(); + + /** @return the remarks, or empty */ + Optional remarks(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedTypeReference.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedTypeReference.java new file mode 100644 index 0000000..d5eba54 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/UserDefinedTypeReference.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.Named; +import org.eclipse.daanse.sql.model.schema.SchemaReference; + +import java.util.Optional; + +public record UserDefinedTypeReference(Optional schema, String name) implements Named { + + /** Convenience: an unqualified UDT (no schema). */ + public UserDefinedTypeReference(String name) { + this(Optional.empty(), name); + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/VersionColumn.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/VersionColumn.java new file mode 100644 index 0000000..d490c98 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/VersionColumn.java @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.OptionalInt; +import java.util.stream.Stream; + +public interface VersionColumn { + + /** The column. */ + ColumnReference column(); + + /** SQL/JDBC type of the column. */ + JDBCType dataType(); + + /** Database-specific type name. */ + String typeName(); + + /** Precision / size of the column. */ + OptionalInt columnSize(); + + /** Number of fractional digits, if applicable. */ + OptionalInt decimalDigits(); + + /** Whether the column is a pseudo column. */ + PseudoColumnKind pseudoColumn(); + + /** Pseudo-column categorization. */ + enum PseudoColumnKind { + UNKNOWN(DatabaseMetaData.versionColumnUnknown), NOT_PSEUDO(DatabaseMetaData.versionColumnNotPseudo), + PSEUDO(DatabaseMetaData.versionColumnPseudo); + + private final int value; + + PseudoColumnKind(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static PseudoColumnKind of(int value) { + return Stream.of(values()).filter(k -> k.value == value).findFirst().orElse(UNKNOWN); + } + } +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ViewDefinition.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ViewDefinition.java new file mode 100644 index 0000000..7de992a --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/ViewDefinition.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.api.schema; + +import org.eclipse.daanse.sql.model.schema.TableReference; + +import java.util.Optional; + +public non-sealed interface ViewDefinition extends SchemaObject { + + /** @return the table reference representing the view */ + TableReference view(); + + /** @return the view body/query, or empty if not available */ + Optional viewBody(); + + /** @return the full CREATE VIEW definition, or empty if not available */ + Optional fullDefinition(); +} diff --git a/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/package-info.java b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/package-info.java new file mode 100644 index 0000000..5c74335 --- /dev/null +++ b/jdbc/api/src/main/java/org/eclipse/daanse/sql/jdbc/api/schema/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.api.schema; diff --git a/jdbc/impl/pom.xml b/jdbc/impl/pom.xml new file mode 100644 index 0000000..b02de8b --- /dev/null +++ b/jdbc/impl/pom.xml @@ -0,0 +1,99 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc + ${revision} + + org.eclipse.daanse.sql.jdbc.impl + Eclipse Daanse SQL JDBC Impl + Core implementation of JDBC database operations for Daanse. + Provides concrete implementations of database connectivity, SQL execution, + and database metadata services with support for multiple database dialects. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + ${project.version} + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${project.version} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.metadata + ${project.version} + test + + + + com.h2database + h2 + 2.2.224 + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.h2 + ${project.version} + test + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.db.common + ${project.version} + test + + + + org.junit.jupiter + junit-jupiter-api + 5.12.2 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.12.2 + test + + + org.assertj + assertj-core + 3.26.0 + test + + + diff --git a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseService.java b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseService.java new file mode 100644 index 0000000..2c815d8 --- /dev/null +++ b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseService.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import java.sql.SQLException; +import java.time.Duration; +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; + +public final class CachingDatabaseService extends DatabaseServiceImpl { + + private final Duration ttl; + private final Map cache = java.util.Collections.synchronizedMap(new WeakHashMap<>()); + + private record Entry(MetaInfo info, Instant expiresAt) {} + + /** + * @param ttl how long each snapshot stays valid; entries past their + * expiry are recomputed on next access + */ + public CachingDatabaseService(Duration ttl) { + this.ttl = Objects.requireNonNull(ttl, "ttl"); + if (ttl.isNegative() || ttl.isZero()) { + throw new IllegalArgumentException("ttl must be > 0: " + ttl); + } + } + + @Override + public MetaInfo createMetaInfo(DataSource dataSource) throws SQLException { + Entry e = cache.get(dataSource); + Instant now = Instant.now(); + if (e != null && now.isBefore(e.expiresAt)) { + return e.info; + } + MetaInfo info = super.createMetaInfo(dataSource); + cache.put(dataSource, new Entry(info, now.plus(ttl))); + return info; + } + + // createMetaInfo(Connection): inherited unchanged — connections from a + // pool aren't stable cache keys. + + // createMetaInfo(DataSource, MetadataProvider) and + // createMetaInfo(Connection, MetadataProvider): inherited unchanged — + // provider-customized snapshots bypass the cache because the same + // DataSource can yield different MetaInfo depending on the provider. + + /** Force-evict all cached snapshots. */ + public void invalidateAll() { + cache.clear(); + } + + /** Force-evict the snapshot for {@code dataSource} if cached. */ + public void invalidate(DataSource dataSource) { + cache.remove(dataSource); + } + + /** Best-effort entry count for diagnostics. May undercount under concurrent eviction. */ + public int approximateSize() { + return cache.size(); + } +} diff --git a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java new file mode 100644 index 0000000..31c64d4 --- /dev/null +++ b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImpl.java @@ -0,0 +1,1625 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.impl; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.schema.BestRowIdentifier; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedTypeReference; +import org.eclipse.daanse.sql.jdbc.api.schema.VersionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.DatabaseInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IdentifierInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo.Nullable; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo.Searchable; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TableMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.record.meta.DatabaseInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.meta.IdentifierInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.meta.MetaInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.meta.StructureInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.meta.TypeInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.BestRowIdentifierRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnPrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PseudoColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SuperTableRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SuperTypeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TableDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TableMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UserDefinedTypeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.VersionColumnRecord; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component(service = DatabaseService.class, scope = ServiceScope.SINGLETON) +public class DatabaseServiceImpl implements DatabaseService { + + private static final Logger LOGGER = LoggerFactory.getLogger(DatabaseServiceImpl.class); + + // Index info column positions (from JDBC spec) + private static final int INDEX_NON_UNIQUE = 4; + private static final int INDEX_NAME = 6; + private static final int INDEX_TYPE = 7; + private static final int INDEX_ORDINAL_POSITION = 8; + private static final int INDEX_COLUMN_NAME = 9; + private static final int INDEX_ASC_OR_DESC = 10; + private static final int INDEX_CARDINALITY = 11; + private static final int INDEX_PAGES = 12; + private static final int INDEX_FILTER_CONDITION = 13; + + private static final int[] RESULT_SET_TYPE_VALUES = { ResultSet.TYPE_FORWARD_ONLY, + ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE }; + + private static final int[] CONCURRENCY_VALUES = { ResultSet.CONCUR_READ_ONLY, ResultSet.CONCUR_UPDATABLE }; + + @Override + public MetaInfo createMetaInfo(DataSource dataSource) throws SQLException { + try (Connection connection = dataSource.getConnection()) { + return createMetaInfo(connection); + } + } + + @Override + public MetaInfo createMetaInfo(Connection connection) throws SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + return readMetaInfo(databaseMetaData); + } + + /** + * @param dataSource the data source + * @param metadataProvider the dialect-specific metadata provider + * @return MetaInfo snapshot with extended metadata + * @throws SQLException on database access error + */ + @Override + public MetaInfo createMetaInfo(DataSource dataSource, MetadataProvider metadataProvider) throws SQLException { + try (Connection connection = dataSource.getConnection()) { + return createMetaInfo(connection, metadataProvider); + } + } + + /** + * @param connection the connection (not closed by this method) + * @param metadataProvider the dialect-specific metadata provider + * @return MetaInfo snapshot with extended metadata + * @throws SQLException on database access error + */ + @Override + public MetaInfo createMetaInfo(Connection connection, MetadataProvider metadataProvider) throws SQLException { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + return readMetaInfoWithProvider(connection, databaseMetaData, metadataProvider); + } + + protected MetaInfo readMetaInfoWithProvider(Connection connection, DatabaseMetaData databaseMetaData, + MetadataProvider provider) throws SQLException { + + // Standard metadata (always via JDBC) + DatabaseInfo databaseInfo = readDatabaseInfo(databaseMetaData); + IdentifierInfo identifierInfo = readIdentifierInfo(databaseMetaData); + List typeInfos = getTypeInfo(databaseMetaData); + + // Tables, columns, catalogs, schemas (always via JDBC as the base) + List catalogs = getCatalogs(databaseMetaData); + List schemas = getSchemas(databaseMetaData); + List tables = getTableDefinitions(databaseMetaData); + // BULK: Columns — dialect-optimized when supported (Oracle's ALL_TAB_COLS + // avoids the COLUMN_DEF LONG quirk that breaks DatabaseMetaData.getColumns). + List columns; + Optional> providerColumns = + provider.getAllColumnDefinitions(connection, null, null, null, null); + if (providerColumns.isPresent()) { + columns = providerColumns.get(); + } else { + columns = getColumnDefinitions(databaseMetaData); + } + + // BULK: Indexes — dialect-optimized or fallback to per-table JDBC + List indexInfos; + Optional> providerIndexes = provider.getAllIndexInfo(connection, null, null); + if (providerIndexes.isPresent()) { + indexInfos = providerIndexes.get(); + } else { + indexInfos = getIndexInfo(databaseMetaData); + } + + // BULK: PrimaryKeys — dialect-optimized or fallback to per-table JDBC + List primaryKeys; + Optional> providerPKs = provider.getAllPrimaryKeys(connection, null, null); + if (providerPKs.isPresent()) { + primaryKeys = providerPKs.get(); + } else { + primaryKeys = new ArrayList<>(); + for (TableDefinition tableDefinition : tables) { + PrimaryKey pk = getPrimaryKey(databaseMetaData, tableDefinition.table()); + if (pk != null) { + primaryKeys.add(pk); + } + } + } + + // BULK: ImportedKeys — dialect-optimized or fallback to per-table JDBC + List importedKeys; + Optional> providerFKs = provider.getAllImportedKeys(connection, null, null); + if (providerFKs.isPresent()) { + importedKeys = providerFKs.get(); + } else { + importedKeys = new ArrayList<>(); + for (TableDefinition tableDefinition : tables) { + importedKeys.addAll(getImportedKeys(databaseMetaData, tableDefinition.table())); + } + } + + // NEW metadata — only via dialect, no JDBC fallback needed + List triggers = provider.getAllTriggers(connection, null, null); + List sequences = provider.getAllSequences(connection, null, null); + List checkConstraints = provider.getAllCheckConstraints(connection, null, null); + List uniqueConstraints = provider.getAllUniqueConstraints(connection, null, null); + List userDefinedTypes = provider.getAllUserDefinedTypes(connection, null, null); + List viewDefinitions = provider.getAllViewDefinitions(connection, null, null); + List procedures = provider.getAllProcedures(connection, null, null); + List functions = provider.getAllFunctions(connection, null, null); + List materializedViews = provider.getAllMaterializedViews(connection, null, null); + List partitions = + provider.getAllPartitions(connection, null, null); + + // Deduplicate materialized views out of tables() and viewDefinitions(): Oracle's + // JDBC driver reports MVs as TABLE_TYPE='TABLE', PostgreSQL's as + // TABLE_TYPE='MATERIALIZED VIEW'. Either way, when a provider returns them in + // getAllMaterializedViews we keep them only there. + if (!materializedViews.isEmpty()) { + Set mvKeys = new HashSet<>(); + for (MaterializedView mv : materializedViews) { + mvKeys.add(tableKey(mv.view())); + } + List filteredTables = new ArrayList<>(tables.size()); + for (TableDefinition td : tables) { + if (!mvKeys.contains(tableKey(td.table()))) { + filteredTables.add(td); + } + } + tables = filteredTables; + List filteredViews = new ArrayList<>(viewDefinitions.size()); + for (ViewDefinition vd : viewDefinitions) { + if (!mvKeys.contains(tableKey(vd.view()))) { + filteredViews.add(vd); + } + } + viewDefinitions = filteredViews; + } + + StructureInfo structureInfo = new StructureInfoRecord(catalogs, schemas, tables, columns, + importedKeys, primaryKeys, triggers, sequences, checkConstraints, uniqueConstraints, + userDefinedTypes, viewDefinitions, procedures, functions, materializedViews, partitions); + return new MetaInfoRecord(databaseInfo, structureInfo, identifierInfo, typeInfos, indexInfos); + } + + private static String tableKey(TableReference table) { + String schema = table.schema().map(SchemaReference::name).orElse(""); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(""); + return catalog + "\u0001" + schema + "\u0001" + table.name(); + } + + protected MetaInfo readMetaInfo(DatabaseMetaData databaseMetaData) throws SQLException { + DatabaseInfo databaseInfo = readDatabaseInfo(databaseMetaData); + IdentifierInfo identifierInfo = readIdentifierInfo(databaseMetaData); + List typeInfos = getTypeInfo(databaseMetaData); + StructureInfo structureInfo = getStructureInfo(databaseMetaData); + List indexInfos = getIndexInfo(databaseMetaData); + return new MetaInfoRecord(databaseInfo, structureInfo, identifierInfo, typeInfos, indexInfos); + } + + public List getIndexInfo(DatabaseMetaData databaseMetaData) throws SQLException { + List tables = getTableDefinitions(databaseMetaData); + List indexInfos = new ArrayList<>(); + for (TableDefinition tableDefinition : tables) { + String catalog = null; + String schema = null; + TableReference table = tableDefinition.table(); + List indexInfoItems = new ArrayList<>(); + Optional oSchema = table.schema(); + if (oSchema.isPresent()) { + SchemaReference sr = oSchema.get(); + schema = oSchema.get().name(); + if (sr.catalog().isPresent()) { + catalog = sr.catalog().get().name(); + } + } + LOGGER.debug("Reading index info for table: {}.{}.{}", catalog, schema, table.name()); + try (ResultSet resultSet = databaseMetaData.getIndexInfo(catalog, schema, table.name(), false, true)) { + while (resultSet.next()) { + boolean nonUnique = resultSet.getBoolean(INDEX_NON_UNIQUE); + Optional indexName = Optional.ofNullable(resultSet.getString(INDEX_NAME)); + int type = resultSet.getInt(INDEX_TYPE); + int ordinalPosition = resultSet.getInt(INDEX_ORDINAL_POSITION); + String columnNameStr = resultSet.getString(INDEX_COLUMN_NAME); + Optional colRef = Optional.ofNullable(columnNameStr) + .map(cn -> new ColumnReference(Optional.of(table), cn)); + String ascOrDesc = resultSet.getString(INDEX_ASC_OR_DESC); + Optional ascending = ascOrDesc == null ? Optional.empty() : + Optional.of("A".equalsIgnoreCase(ascOrDesc)); + long cardinality = resultSet.getLong(INDEX_CARDINALITY); + long pages = resultSet.getLong(INDEX_PAGES); + Optional filterCondition = Optional.ofNullable(resultSet.getString(INDEX_FILTER_CONDITION)); + + IndexInfoItem.IndexType indexType = IndexInfoItem.IndexType.of(type); + indexInfoItems.add(new IndexInfoItemRecord(indexName, indexType, colRef, ordinalPosition, + ascending, cardinality, pages, filterCondition, !nonUnique)); + } + } catch (SQLException e) { + LOGGER.warn("Error reading index info for table: {}.{}.{} - {}", catalog, schema, table.name(), + e.getMessage()); + + continue; + } + indexInfos.add(new IndexInfoRecord(table, indexInfoItems)); + } + return List.copyOf(indexInfos); + } + + protected StructureInfo getStructureInfo(DatabaseMetaData databaseMetaData) throws SQLException { + List catalogs = getCatalogs(databaseMetaData); + List schemas = getSchemas(databaseMetaData); + List tables = getTableDefinitions(databaseMetaData); + List columns = getColumnDefinitions(databaseMetaData); + + List importedKeys = new ArrayList(); + List primaryKeys = new ArrayList(); + for (TableDefinition tableDefinition : tables) { + List iks = getImportedKeys(databaseMetaData, tableDefinition.table()); + importedKeys.addAll(iks); + + PrimaryKey pk = getPrimaryKey(databaseMetaData, tableDefinition.table()); + if (pk != null) { + primaryKeys.add(pk); + } + } + + StructureInfo structureInfo = new StructureInfoRecord(catalogs, schemas, tables, columns, importedKeys, primaryKeys, + List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), List.of(), + List.of()); + return structureInfo; + } + + private List getCatalogs(DatabaseMetaData databaseMetaData) throws SQLException { + + List catalogs = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getCatalogs()) { + while (rs.next()) { + final String catalogName = rs.getString("TABLE_CAT"); + catalogs.add(new CatalogReference(catalogName)); + } + } + return List.copyOf(catalogs); + } + + private List getSchemas(DatabaseMetaData databaseMetaData) throws SQLException { + return getSchemas(databaseMetaData, null); + } + + private List getSchemas(DatabaseMetaData databaseMetaData, CatalogReference catalog) + throws SQLException { + + String catalogName = null; + + if (catalog != null) { + catalogName = catalog.name(); + } + + List schemas = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getSchemas(catalogName, null)) { + while (rs.next()) { + final String schemaName = rs.getString("TABLE_SCHEM"); + final Optional c = Optional.ofNullable(rs.getString("TABLE_CATALOG")) + .map(cat -> new CatalogReference(cat)); + schemas.add(new SchemaReference(c, schemaName)); + } + } + return List.copyOf(schemas); + } + + private List getTableTypes(DatabaseMetaData databaseMetaData) throws SQLException { + + List typeInfos = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getTableTypes()) { + while (rs.next()) { + final String tableTypeName = rs.getString("TABLE_TYPE"); + typeInfos.add(tableTypeName); + } + } + + return List.copyOf(typeInfos); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData) throws SQLException { + return getTableDefinitions(databaseMetaData, List.of()); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, List types) + throws SQLException { + + String[] typesArr = typesArrayForFilterNull(types); + + return getTableDefinitions(databaseMetaData, null, null, null, typesArr); + } + + private static String[] typesArrayForFilterNull(List types) { + String[] typesArr = null; + if (types != null && !types.isEmpty()) { + typesArr = types.toArray(String[]::new); + } + return typesArr; + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, CatalogReference catalog) + throws SQLException { + return getTableDefinitions(databaseMetaData, List.of()); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, CatalogReference catalog, + List types) throws SQLException { + String[] typesArr = typesArrayForFilterNull(types); + return getTableDefinitions(databaseMetaData, catalog.name(), null, null, typesArr); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, SchemaReference schema) + throws SQLException { + return getTableDefinitions(databaseMetaData, schema, List.of()); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, SchemaReference schema, + List types) throws SQLException { + String[] typesArr = typesArrayForFilterNull(types); + + String catalog = schema.catalog().map(CatalogReference::name).orElse(null); + return getTableDefinitions(databaseMetaData, catalog, schema.name(), null, typesArr); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, TableReference table) + throws SQLException { + + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + return getTableDefinitions(databaseMetaData, catalog, schema, table.name(), null); + } + + private List getTableDefinitions(DatabaseMetaData databaseMetaData, String catalog, + String schemaPattern, String tableNamePattern, String types[]) throws SQLException { + + List tabeDefinitions = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getTables(catalog, schemaPattern, tableNamePattern, types)) { + int columnCount = rs.getMetaData().getColumnCount(); + Set columnNames = new HashSet<>(); + for (int i = 1; i <= columnCount; i++) { + + String colName = rs.getMetaData().getColumnName(i); + columnNames.add(colName); + } + while (rs.next()) { + final Optional oCatalogName = Optional.ofNullable(rs.getString("TABLE_CAT")); + final Optional oSchemaName = Optional.ofNullable(rs.getString("TABLE_SCHEM")); + final String tableName = rs.getString("TABLE_NAME"); + final String tableType = rs.getString("TABLE_TYPE"); + final Optional oRemarks = Optional.ofNullable(rs.getString("REMARKS")); + + final Optional oTypeCat = getColumnValue(rs, columnNames, "TYPE_CAT"); + final Optional oTypeSchema = getColumnValue(rs, columnNames, "TYPE_SCHEM"); + final Optional oTypeName = getColumnValue(rs, columnNames, "TYPE_NAME"); + final Optional oSelfRefColName = getColumnValue(rs, columnNames, "SELF_REFERENCING_COL_NAME"); + final Optional oRefGen = getColumnValue(rs, columnNames, "REF_GENERATION"); + + Optional oCatRef = oCatalogName.map(cn -> new CatalogReference(cn)); + Optional oSchemaRef = oSchemaName.map(sn -> new SchemaReference(oCatRef, sn)); + + TableReference tableReference = new TableReference(oSchemaRef, tableName, tableType); + TableMetaData tableMetaData = new TableMetaDataRecord(oRemarks, oTypeCat, oTypeSchema, oTypeName, + oSelfRefColName, oRefGen); + + TableDefinition tableDefinition = new TableDefinitionRecord(tableReference, tableMetaData); + tabeDefinitions.add(tableDefinition); + } + } + + return List.copyOf(tabeDefinitions); + } + + private Optional getColumnValue(ResultSet rs, Set columnNames, String columnName) + throws SQLException { + if (!columnNames.contains(columnName)) { + return Optional.empty(); + } + String value = rs.getString(columnName); + if (rs.wasNull()) { + return Optional.empty(); + } else { + return Optional.of(value); + } + } + + private boolean tableExists(DatabaseMetaData databaseMetaData, TableReference table) throws SQLException { + + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + return tableExists(databaseMetaData, catalog, schema, table.name(), null); + } + + private boolean tableExists(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String tableNamePattern, String types[]) throws SQLException { + + try (ResultSet rs = databaseMetaData.getTables(catalog, schemaPattern, tableNamePattern, types)) { + return rs.next(); + } + } + + private List getTypeInfo(DatabaseMetaData databaseMetaData) throws SQLException { + + List typeInfos = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getTypeInfo()) { + while (rs.next()) { + final String typeName = rs.getString("TYPE_NAME"); + final int dataType = rs.getInt("DATA_TYPE"); + final int precision = rs.getInt("PRECISION"); + final Optional literalPrefix = Optional.ofNullable(rs.getString("LITERAL_PREFIX")); + final Optional literalSuffix = Optional.ofNullable(rs.getString("LITERAL_SUFFIX")); + final Optional createParams = Optional.ofNullable(rs.getString("CREATE_PARAMS")); + final Nullable nullable = TypeInfo.Nullable.of(rs.getShort("NULLABLE")); + final boolean caseSensitive = rs.getBoolean("CASE_SENSITIVE"); + final Searchable searchable = TypeInfo.Searchable.of(rs.getShort("SEARCHABLE")); + final boolean unsignedAttribute = rs.getBoolean("UNSIGNED_ATTRIBUTE"); + final boolean fixedPrecScale = rs.getBoolean("FIXED_PREC_SCALE"); + final boolean autoIncrement = rs.getBoolean("AUTO_INCREMENT"); + final Optional localTypeName = Optional.ofNullable(rs.getString("LOCAL_TYPE_NAME")); + final short minimumScale = rs.getShort("MINIMUM_SCALE"); + final short maximumScale = rs.getShort("MAXIMUM_SCALE"); + final int numPrecRadix = rs.getInt("NUM_PREC_RADIX"); + + JDBCType jdbcType; + try { + jdbcType = JDBCType.valueOf(dataType); + } catch (IllegalArgumentException ex) { + jdbcType = JDBCType.OTHER; + LOGGER.info("Unknown JDBC-Typcode: " + dataType + " (" + typeName + ")"); + } + TypeInfoRecord typeInfo = new TypeInfoRecord(typeName, jdbcType, precision, literalPrefix, literalSuffix, + createParams, nullable, caseSensitive, searchable, unsignedAttribute, fixedPrecScale, + autoIncrement, localTypeName, minimumScale, maximumScale, numPrecRadix); + typeInfos.add(typeInfo); + } + } + + return List.copyOf(typeInfos); + } + + protected static DatabaseInfo readDatabaseInfo(DatabaseMetaData databaseMetaData) { + + String productName = ""; + try { + productName = databaseMetaData.getDatabaseProductName(); + } catch (SQLException e) { + LOGGER.error("Exception while reading productName", e); + } + + String productVersion = ""; + try { + productVersion = databaseMetaData.getDatabaseProductVersion(); + } catch (SQLException e) { + LOGGER.error("Exception while reading productVersion", e); + } + + int majorVersion = 0; + try { + majorVersion = databaseMetaData.getDatabaseMajorVersion(); + } catch (SQLException e) { + LOGGER.error("Exception while reading majorVersion", e); + } + + int minorVersion = 0; + try { + minorVersion = databaseMetaData.getDatabaseMinorVersion(); + } catch (SQLException e) { + LOGGER.error("Exception while reading minorVersion", e); + } + + return new DatabaseInfoRecord(productName, productVersion, majorVersion, minorVersion); + } + + protected static IdentifierInfo readIdentifierInfo(DatabaseMetaData databaseMetaData) { + + String quoteString = " "; + boolean readOnly = true; + int maxColumnNameLength = 0; + Set> supportedResultSetStyles = Set.of(); + try { + quoteString = databaseMetaData.getIdentifierQuoteString(); + maxColumnNameLength = databaseMetaData.getMaxColumnNameLength(); + readOnly = databaseMetaData.isReadOnly(); + supportedResultSetStyles = supportedResultSetStyles(databaseMetaData); + } catch (SQLException e) { + LOGGER.error("Exception while reading quoteString", e); + } + return new IdentifierInfoRecord(quoteString, maxColumnNameLength, readOnly, supportedResultSetStyles); + } + + private List getColumnDefinitions(DatabaseMetaData databaseMetaData) throws SQLException { + return getColumnDefinitions(databaseMetaData, null, null, null, null); + + } + + private List getColumnDefinitions(DatabaseMetaData databaseMetaData, TableReference table) + throws SQLException { + + String sTable = table.name(); + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + return getColumnDefinitions(databaseMetaData, catalog, schema, sTable, null); + } + + private List getColumnDefinitions(DatabaseMetaData databaseMetaData, ColumnReference column) + throws SQLException { + + Optional oTable = column.table(); + String table = oTable.map(TableReference::name).orElse(null); + Optional oSchema = oTable.flatMap(TableReference::schema); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + return getColumnDefinitions(databaseMetaData, catalog, schema, table, column.name()); + } + + private List getColumnDefinitions(DatabaseMetaData databaseMetaData, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + List columnDefinitions = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern);) { + while (rs.next()) { + + final Optional oCatalogName = Optional.ofNullable(rs.getString("TABLE_CAT")); + final Optional oSchemaName = Optional.ofNullable(rs.getString("TABLE_SCHEM")); + final String tableName = rs.getString("TABLE_NAME"); + final String columName = rs.getString("COLUMN_NAME"); + + final String typeName = rs.getString("TYPE_NAME"); + OptionalInt oColumnSize = OptionalInt.of(rs.getInt("COLUMN_SIZE")); + + if (rs.wasNull()) { + oColumnSize = OptionalInt.empty(); + } + + OptionalInt oDecimalDigits = OptionalInt.of(rs.getInt("DECIMAL_DIGITS")); + if (rs.wasNull()) { + oDecimalDigits = OptionalInt.empty(); + } + + OptionalInt oNumPrecRadix = OptionalInt.of(rs.getInt("NUM_PREC_RADIX")); + if (rs.wasNull()) { + oNumPrecRadix = OptionalInt.empty(); + } + + OptionalInt oNullable = OptionalInt.of(rs.getInt("NULLABLE")); + if (rs.wasNull()) { + oNullable = OptionalInt.empty(); + } + + OptionalInt oCharOctetLength = OptionalInt.of(rs.getInt("CHAR_OCTET_LENGTH")); + if (rs.wasNull()) { + oCharOctetLength = OptionalInt.empty(); + } + + final int dataType = rs.getInt("DATA_TYPE"); + + final Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + + // Additional fields from JDBC spec + final Optional columnDefault = Optional.ofNullable(rs.getString("COLUMN_DEF")); + final ColumnMetaData.Nullability nullability = oNullable.isPresent() + ? ColumnMetaData.Nullability.of(oNullable.getAsInt()) + : ColumnMetaData.Nullability.UNKNOWN; + + // IS_AUTOINCREMENT and IS_GENERATEDCOLUMN may not be available in all drivers + ColumnMetaData.AutoIncrement autoIncrement = ColumnMetaData.AutoIncrement.UNKNOWN; + ColumnMetaData.GeneratedColumn generatedColumn = ColumnMetaData.GeneratedColumn.UNKNOWN; + try { + String isAutoIncrement = rs.getString("IS_AUTOINCREMENT"); + autoIncrement = ColumnMetaData.AutoIncrement.ofString(isAutoIncrement); + } catch (SQLException e) { + LOGGER.debug("IS_AUTOINCREMENT not available for column: {}.{}", tableName, columName); + } + try { + String isGeneratedColumn = rs.getString("IS_GENERATEDCOLUMN"); + generatedColumn = ColumnMetaData.GeneratedColumn.ofString(isGeneratedColumn); + } catch (SQLException e) { + LOGGER.debug("IS_GENERATEDCOLUMN not available for column: {}.{}", tableName, columName); + } + + Optional oCatRef = oCatalogName.map(cn -> new CatalogReference(cn)); + Optional oSchemaRef = oSchemaName.map(sn -> new SchemaReference(oCatRef, sn)); + + JDBCType jdbcType; + try { + jdbcType = JDBCType.valueOf(dataType); + } catch (IllegalArgumentException ex) { + jdbcType = JDBCType.OTHER; + LOGGER.info("Unknown JDBC-Typcode: " + dataType + " (" + typeName + ") in table: " + tableName + + " column: " + columName); + } + + TableReference tableReference = new TableReference(oSchemaRef, tableName); + + ColumnReference columnReference = new ColumnReference(Optional.of(tableReference), columName); + ColumnDefinition columnDefinition = new ColumnDefinitionRecord(columnReference, new ColumnMetaDataRecord( + jdbcType, typeName, oColumnSize, oDecimalDigits, oNumPrecRadix, nullability, + oCharOctetLength, remarks, columnDefault, autoIncrement, generatedColumn)); + + columnDefinitions.add(columnDefinition); + } + } + return List.copyOf(columnDefinitions); + } + + private boolean columnExists(DatabaseMetaData databaseMetaData, ColumnReference column) throws SQLException { + + Optional oTable = column.table(); + String table = oTable.map(TableReference::name).orElse(null); + Optional oSchema = oTable.flatMap(TableReference::schema); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + return columnExists(databaseMetaData, catalog, schema, table, column.name()); + } + + private boolean columnExists(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { + + try (ResultSet rs = databaseMetaData.getColumns(catalog, schemaPattern, tableNamePattern, columnNamePattern)) { + return rs.next(); + } + } + + private List getImportedKeys(DatabaseMetaData databaseMetaData, TableReference table) + throws SQLException { + + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + return getImportedKeys(databaseMetaData, catalog, schema, table.name()); + } + + private List getImportedKeys(DatabaseMetaData databaseMetaData, String catalog, String schema, + String tableName) throws SQLException { + List importedKeys = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getImportedKeys(catalog, schema, tableName);) { + while (rs.next()) { + + final Optional oCatalogNamePK = Optional.ofNullable(rs.getString("PKTABLE_CAT")); + final Optional oSchemaNamePk = Optional.ofNullable(rs.getString("PKTABLE_SCHEM")); + final String tableNamePk = rs.getString("PKTABLE_NAME"); + final String columNamePk = rs.getString("PKCOLUMN_NAME"); + + final Optional oCatalogNameFK = Optional.ofNullable(rs.getString("FKTABLE_CAT")); + final Optional oSchemaNameFk = Optional.ofNullable(rs.getString("FKTABLE_SCHEM")); + final String tableNameFk = rs.getString("FKTABLE_NAME"); + final String columNameFk = rs.getString("FKCOLUMN_NAME"); + + // Additional fields from JDBC spec + final int keySeq = rs.getInt("KEY_SEQ"); + final int updateRule = rs.getInt("UPDATE_RULE"); + final int deleteRule = rs.getInt("DELETE_RULE"); + final String fkName = rs.getString("FK_NAME"); + final Optional pkName = Optional.ofNullable(rs.getString("PK_NAME")); + final int deferrability = rs.getInt("DEFERRABILITY"); + + // PK + Optional oCatRefPk = oCatalogNamePK.map(cn -> new CatalogReference(cn)); + Optional oSchemaRefPk = oSchemaNamePk.map(sn -> new SchemaReference(oCatRefPk, sn)); + TableReference tableReferencePk = new TableReference(oSchemaRefPk, tableNamePk); + ColumnReference primaryKeyColumn = new ColumnReference(Optional.of(tableReferencePk), columNamePk); + + // FK + Optional oCatRefFk = oCatalogNameFK.map(cn -> new CatalogReference(cn)); + Optional oSchemaRefFk = oSchemaNameFk.map(sn -> new SchemaReference(oCatRefFk, sn)); + TableReference tableReferenceFk = new TableReference(oSchemaRefFk, tableNameFk); + ColumnReference foreignKeyColumn = new ColumnReference(Optional.of(tableReferenceFk), columNameFk); + + // Use FK_NAME from database if available, otherwise generate one + String constraintName; + if (fkName != null && !fkName.isBlank()) { + constraintName = fkName; + } else { + StringBuilder sb = new StringBuilder(); + sb.append("fk_").append(tableReferenceFk.name()).append("_").append(foreignKeyColumn.name()) + .append("_").append(tableReferencePk.name()).append("_").append(primaryKeyColumn.name()); + constraintName = sb.toString(); + } + + ImportedKey importedKey = new ImportedKeyRecord( + primaryKeyColumn, + foreignKeyColumn, + constraintName, + keySeq, + ImportedKey.ReferentialAction.of(updateRule), + ImportedKey.ReferentialAction.of(deleteRule), + pkName, + ImportedKey.Deferrability.of(deferrability)); + importedKeys.add(importedKey); + } + } + return List.copyOf(importedKeys); + } + + /** + * @param databaseMetaData the database metadata + * @param table the table reference + * @return the primary key, or null if the table has no primary key + * @throws SQLException if a database access error occurs + */ + public PrimaryKey getPrimaryKey(DatabaseMetaData databaseMetaData, TableReference table) throws SQLException { + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + + List columns = new ArrayList<>(); + String pkName = null; + + try (ResultSet rs = databaseMetaData.getPrimaryKeys(catalog, schema, table.name())) { + // Results are ordered by COLUMN_NAME, but we need to order by KEY_SEQ + // So we collect all columns first + java.util.TreeMap orderedColumns = new java.util.TreeMap<>(); + + while (rs.next()) { + final String columnName = rs.getString("COLUMN_NAME"); + final int keySeq = rs.getInt("KEY_SEQ"); + pkName = rs.getString("PK_NAME"); // Same for all rows + + ColumnReference colRef = new ColumnReference(Optional.of(table), columnName); + orderedColumns.put(keySeq, colRef); + } + + // Add columns in KEY_SEQ order + columns.addAll(orderedColumns.values()); + } + + if (columns.isEmpty()) { + return null; // No primary key + } + + return new PrimaryKeyRecord(table, List.copyOf(columns), Optional.ofNullable(pkName)); + } + + private static Set> supportedResultSetStyles(DatabaseMetaData databaseMetaData) throws SQLException { + Set> supports = new HashSet<>(); + for (int type : RESULT_SET_TYPE_VALUES) { + for (int concurrency : CONCURRENCY_VALUES) { + if (databaseMetaData.supportsResultSetConcurrency(type, concurrency)) { + String driverName = databaseMetaData.getDriverName(); + if (type != ResultSet.TYPE_FORWARD_ONLY && driverName.equals("JDBC-ODBC Bridge (odbcjt32.dll)")) { + // In JDK 1.6, the Jdbc-Odbc bridge announces + // that it can handle TYPE_SCROLL_INSENSITIVE + // but it does so by generating a 'COUNT(*)' + // query, and this query is invalid if the query + // contains a single-quote. So, override the + // driver. + continue; + } + supports.add(new ArrayList<>(Arrays.asList(type, concurrency))); + } + } + } + return supports; + } + + private List getProcedures(DatabaseMetaData databaseMetaData) throws SQLException { + return getProcedures(databaseMetaData, null, null, null); + } + + private List getProcedures(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String procedureNamePattern) throws SQLException { + List procedures = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getProcedures(catalog, schemaPattern, procedureNamePattern)) { + while (rs.next()) { + final Optional oCatalogName = Optional.ofNullable(rs.getString("PROCEDURE_CAT")); + final Optional oSchemaName = Optional.ofNullable(rs.getString("PROCEDURE_SCHEM")); + final String procedureName = rs.getString("PROCEDURE_NAME"); + final Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + final int procedureType = rs.getInt("PROCEDURE_TYPE"); + final String specificName = rs.getString("SPECIFIC_NAME"); + + Optional oCatRef = oCatalogName.map(CatalogReference::new); + Optional oSchemaRef = oSchemaName.map(sn -> new SchemaReference(oCatRef, sn)); + + ProcedureReference reference = new ProcedureReference(oSchemaRef, procedureName, specificName); + + // Get procedure columns + List columns = getProcedureColumns(databaseMetaData, + oCatalogName.orElse(null), oSchemaName.orElse(null), procedureName); + + Procedure procedure = new ProcedureRecord(reference, Procedure.ProcedureType.of(procedureType), remarks, + columns, Optional.empty(), Optional.empty(), Optional.empty()); + procedures.add(procedure); + } + } + + return List.copyOf(procedures); + } + + private List getProcedureColumns(DatabaseMetaData databaseMetaData, String catalog, + String schema, String procedureName) throws SQLException { + List columns = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getProcedureColumns(catalog, schema, procedureName, null)) { + while (rs.next()) { + final String columnName = rs.getString("COLUMN_NAME"); + final int columnType = rs.getInt("COLUMN_TYPE"); + final int dataType = rs.getInt("DATA_TYPE"); + final String typeName = rs.getString("TYPE_NAME"); + + OptionalInt precision = OptionalInt.of(rs.getInt("PRECISION")); + if (rs.wasNull()) { + precision = OptionalInt.empty(); + } + + OptionalInt scale = OptionalInt.of(rs.getInt("SCALE")); + if (rs.wasNull()) { + scale = OptionalInt.empty(); + } + + OptionalInt radix = OptionalInt.of(rs.getInt("RADIX")); + if (rs.wasNull()) { + radix = OptionalInt.empty(); + } + + final int nullable = rs.getInt("NULLABLE"); + final Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + final Optional columnDefault = Optional.ofNullable(rs.getString("COLUMN_DEF")); + final int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + + JDBCType jdbcType; + try { + jdbcType = JDBCType.valueOf(dataType); + } catch (IllegalArgumentException ex) { + jdbcType = JDBCType.OTHER; + LOGGER.debug("Unknown JDBC type code: {} ({}) for procedure column: {}.{}", + dataType, typeName, procedureName, columnName); + } + + ProcedureColumn column = new ProcedureColumnRecord(columnName, ProcedureColumn.ColumnType.of(columnType), + jdbcType, typeName, precision, scale, radix, ProcedureColumn.Nullability.of(nullable), + remarks, columnDefault, ordinalPosition); + columns.add(column); + } + } + + return List.copyOf(columns); + } + + private List getFunctions(DatabaseMetaData databaseMetaData) throws SQLException { + return getFunctions(databaseMetaData, null, null, null); + } + + private List getFunctions(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String functionNamePattern) throws SQLException { + List functions = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getFunctions(catalog, schemaPattern, functionNamePattern)) { + while (rs.next()) { + final Optional oCatalogName = Optional.ofNullable(rs.getString("FUNCTION_CAT")); + final Optional oSchemaName = Optional.ofNullable(rs.getString("FUNCTION_SCHEM")); + final String functionName = rs.getString("FUNCTION_NAME"); + final Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + final int functionType = rs.getInt("FUNCTION_TYPE"); + final String specificName = rs.getString("SPECIFIC_NAME"); + + Optional oCatRef = oCatalogName.map(CatalogReference::new); + Optional oSchemaRef = oSchemaName.map(sn -> new SchemaReference(oCatRef, sn)); + + FunctionReference reference = new FunctionReference(oSchemaRef, functionName, specificName); + + // Get function columns + List columns = getFunctionColumns(databaseMetaData, + oCatalogName.orElse(null), oSchemaName.orElse(null), functionName); + + Function function = new FunctionRecord(reference, Function.FunctionType.of(functionType), remarks, + columns, Optional.empty(), Optional.empty(), Optional.empty()); + functions.add(function); + } + } + + return List.copyOf(functions); + } + + private List getFunctionColumns(DatabaseMetaData databaseMetaData, String catalog, + String schema, String functionName) throws SQLException { + List columns = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getFunctionColumns(catalog, schema, functionName, null)) { + while (rs.next()) { + final String columnName = rs.getString("COLUMN_NAME"); + final int columnType = rs.getInt("COLUMN_TYPE"); + final int dataType = rs.getInt("DATA_TYPE"); + final String typeName = rs.getString("TYPE_NAME"); + + OptionalInt precision = OptionalInt.of(rs.getInt("PRECISION")); + if (rs.wasNull()) { + precision = OptionalInt.empty(); + } + + OptionalInt scale = OptionalInt.of(rs.getInt("SCALE")); + if (rs.wasNull()) { + scale = OptionalInt.empty(); + } + + OptionalInt radix = OptionalInt.of(rs.getInt("RADIX")); + if (rs.wasNull()) { + radix = OptionalInt.empty(); + } + + final int nullable = rs.getInt("NULLABLE"); + final Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + + OptionalInt charOctetLength = OptionalInt.of(rs.getInt("CHAR_OCTET_LENGTH")); + if (rs.wasNull()) { + charOctetLength = OptionalInt.empty(); + } + + final int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + + JDBCType jdbcType; + try { + jdbcType = JDBCType.valueOf(dataType); + } catch (IllegalArgumentException ex) { + jdbcType = JDBCType.OTHER; + LOGGER.debug("Unknown JDBC type code: {} ({}) for function column: {}.{}", + dataType, typeName, functionName, columnName); + } + + FunctionColumn column = new FunctionColumnRecord(columnName, FunctionColumn.ColumnType.of(columnType), + jdbcType, typeName, precision, scale, radix, FunctionColumn.Nullability.of(nullable), + remarks, charOctetLength, ordinalPosition); + columns.add(column); + } + } + + return List.copyOf(columns); + } + + private List getExportedKeys(DatabaseMetaData databaseMetaData, TableReference table) + throws SQLException { + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + Optional oCatalog = oSchema.flatMap(SchemaReference::catalog); + String catalog = oCatalog.map(CatalogReference::name).orElse(null); + return getExportedKeys(databaseMetaData, catalog, schema, table.name()); + } + + private List getExportedKeys(DatabaseMetaData databaseMetaData, String catalog, String schema, + String tableName) throws SQLException { + List exportedKeys = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getExportedKeys(catalog, schema, tableName)) { + while (rs.next()) { + ImportedKey key = readForeignKeyFromResultSet(rs); + exportedKeys.add(key); + } + } + return List.copyOf(exportedKeys); + } + + private List getCrossReference(DatabaseMetaData databaseMetaData, TableReference parentTable, + TableReference foreignTable) throws SQLException { + Optional parentSchema = parentTable.schema(); + String pSchema = parentSchema.map(SchemaReference::name).orElse(null); + Optional parentCatalog = parentSchema.flatMap(SchemaReference::catalog); + String pCatalog = parentCatalog.map(CatalogReference::name).orElse(null); + + Optional foreignSchema = foreignTable.schema(); + String fSchema = foreignSchema.map(SchemaReference::name).orElse(null); + Optional foreignCatalog = foreignSchema.flatMap(SchemaReference::catalog); + String fCatalog = foreignCatalog.map(CatalogReference::name).orElse(null); + + return getCrossReference(databaseMetaData, pCatalog, pSchema, parentTable.name(), + fCatalog, fSchema, foreignTable.name()); + } + + private List getCrossReference(DatabaseMetaData databaseMetaData, String parentCatalog, + String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, + String foreignTable) throws SQLException { + List crossRefs = new ArrayList<>(); + + try (ResultSet rs = databaseMetaData.getCrossReference(parentCatalog, parentSchema, parentTable, + foreignCatalog, foreignSchema, foreignTable)) { + while (rs.next()) { + ImportedKey key = readForeignKeyFromResultSet(rs); + crossRefs.add(key); + } + } + return List.copyOf(crossRefs); + } + + private ImportedKey readForeignKeyFromResultSet(ResultSet rs) throws SQLException { + final Optional oCatalogNamePK = Optional.ofNullable(rs.getString("PKTABLE_CAT")); + final Optional oSchemaNamePk = Optional.ofNullable(rs.getString("PKTABLE_SCHEM")); + final String tableNamePk = rs.getString("PKTABLE_NAME"); + final String columNamePk = rs.getString("PKCOLUMN_NAME"); + + final Optional oCatalogNameFK = Optional.ofNullable(rs.getString("FKTABLE_CAT")); + final Optional oSchemaNameFk = Optional.ofNullable(rs.getString("FKTABLE_SCHEM")); + final String tableNameFk = rs.getString("FKTABLE_NAME"); + final String columNameFk = rs.getString("FKCOLUMN_NAME"); + + final int keySeq = rs.getInt("KEY_SEQ"); + final int updateRule = rs.getInt("UPDATE_RULE"); + final int deleteRule = rs.getInt("DELETE_RULE"); + final String fkName = rs.getString("FK_NAME"); + final Optional pkName = Optional.ofNullable(rs.getString("PK_NAME")); + final int deferrability = rs.getInt("DEFERRABILITY"); + + // PK + Optional oCatRefPk = oCatalogNamePK.map(CatalogReference::new); + Optional oSchemaRefPk = oSchemaNamePk.map(sn -> new SchemaReference(oCatRefPk, sn)); + TableReference tableReferencePk = new TableReference(oSchemaRefPk, tableNamePk); + ColumnReference primaryKeyColumn = new ColumnReference(Optional.of(tableReferencePk), columNamePk); + + // FK + Optional oCatRefFk = oCatalogNameFK.map(CatalogReference::new); + Optional oSchemaRefFk = oSchemaNameFk.map(sn -> new SchemaReference(oCatRefFk, sn)); + TableReference tableReferenceFk = new TableReference(oSchemaRefFk, tableNameFk); + ColumnReference foreignKeyColumn = new ColumnReference(Optional.of(tableReferenceFk), columNameFk); + + // Use FK_NAME from database if available, otherwise generate one + String constraintName; + if (fkName != null && !fkName.isBlank()) { + constraintName = fkName; + } else { + StringBuilder sb = new StringBuilder(); + sb.append("fk_").append(tableReferenceFk.name()).append("_").append(foreignKeyColumn.name()) + .append("_").append(tableReferencePk.name()).append("_").append(primaryKeyColumn.name()); + constraintName = sb.toString(); + } + + return new ImportedKeyRecord( + primaryKeyColumn, + foreignKeyColumn, + constraintName, + keySeq, + ImportedKey.ReferentialAction.of(updateRule), + ImportedKey.ReferentialAction.of(deleteRule), + pkName, + ImportedKey.Deferrability.of(deferrability)); + } + + private List getUDTs(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String typeNamePattern, int[] types) throws SQLException { + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getUDTs(catalog, schemaPattern, typeNamePattern, types)) { + while (rs.next()) { + Optional oCat = Optional.ofNullable(rs.getString("TYPE_CAT")); + Optional oSchema = Optional.ofNullable(rs.getString("TYPE_SCHEM")); + String typeName = rs.getString("TYPE_NAME"); + String className = rs.getString("CLASS_NAME"); + int dataType = rs.getInt("DATA_TYPE"); + Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + + JDBCType jdbcType; + try { + jdbcType = JDBCType.valueOf(dataType); + } catch (IllegalArgumentException e) { + jdbcType = JDBCType.OTHER; + } + Optional catRef = oCat.map(CatalogReference::new); + Optional schemaRef = oSchema.map(sn -> new SchemaReference(catRef, sn)); + result.add(new UserDefinedTypeRecord( + new UserDefinedTypeReference(schemaRef, typeName), + className, jdbcType, remarks)); + } + } + return List.copyOf(result); + } + + private List getBestRowIdentifier(DatabaseMetaData databaseMetaData, TableReference table, + int scope, boolean nullable) throws SQLException { + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + String catalog = oSchema.flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getBestRowIdentifier(catalog, schema, table.name(), scope, nullable)) { + while (rs.next()) { + int scopeVal = rs.getInt("SCOPE"); + String columnName = rs.getString("COLUMN_NAME"); + int dataType = rs.getInt("DATA_TYPE"); + String typeName = rs.getString("TYPE_NAME"); + + OptionalInt columnSize = optInt(rs, "COLUMN_SIZE"); + OptionalInt decimalDigits = optShort(rs, "DECIMAL_DIGITS"); + int pseudoCol = rs.getInt("PSEUDO_COLUMN"); + + ColumnReference colRef = new ColumnReference(Optional.of(table), columnName); + result.add(new BestRowIdentifierRecord(colRef, + BestRowIdentifier.Scope.of(scopeVal), + jdbcTypeOrOther(dataType), + typeName, + columnSize, + decimalDigits, + BestRowIdentifier.PseudoColumnKind.of(pseudoCol))); + } + } + return List.copyOf(result); + } + + private List getVersionColumns(DatabaseMetaData databaseMetaData, TableReference table) + throws SQLException { + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + String catalog = oSchema.flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getVersionColumns(catalog, schema, table.name())) { + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + int dataType = rs.getInt("DATA_TYPE"); + String typeName = rs.getString("TYPE_NAME"); + OptionalInt columnSize = optInt(rs, "COLUMN_SIZE"); + OptionalInt decimalDigits = optShort(rs, "DECIMAL_DIGITS"); + int pseudoCol = rs.getInt("PSEUDO_COLUMN"); + + ColumnReference colRef = new ColumnReference(Optional.of(table), columnName); + result.add(new VersionColumnRecord(colRef, + jdbcTypeOrOther(dataType), + typeName, + columnSize, + decimalDigits, + VersionColumn.PseudoColumnKind.of(pseudoCol))); + } + } + return List.copyOf(result); + } + + private List getPseudoColumns(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getPseudoColumns(catalog, schemaPattern, tableNamePattern, + columnNamePattern)) { + while (rs.next()) { + Optional oCat = Optional.ofNullable(rs.getString("TABLE_CAT")); + Optional oSchema = Optional.ofNullable(rs.getString("TABLE_SCHEM")); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + int dataType = rs.getInt("DATA_TYPE"); + + OptionalInt columnSize = optInt(rs, "COLUMN_SIZE"); + OptionalInt decimalDigits = optInt(rs, "DECIMAL_DIGITS"); + OptionalInt numPrecRadix = optInt(rs, "NUM_PREC_RADIX"); + Optional remarks = Optional.ofNullable(rs.getString("REMARKS")); + OptionalInt charOctetLength = optInt(rs, "CHAR_OCTET_LENGTH"); + Optional isNullable = Optional.ofNullable(rs.getString("IS_NULLABLE")); + String columnUsage = rs.getString("COLUMN_USAGE"); + + Optional catRef = oCat.map(CatalogReference::new); + Optional schemaRef = oSchema.map(sn -> new SchemaReference(catRef, sn)); + TableReference tableRef = new TableReference(schemaRef, tableName); + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + + result.add(new PseudoColumnRecord(colRef, jdbcTypeOrOther(dataType), + columnSize, decimalDigits, numPrecRadix, remarks, charOctetLength, isNullable, columnUsage)); + } + } + return List.copyOf(result); + } + + private List getTablePrivileges(DatabaseMetaData databaseMetaData, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getTablePrivileges(catalog, schemaPattern, tableNamePattern)) { + while (rs.next()) { + Optional oCat = Optional.ofNullable(rs.getString("TABLE_CAT")); + Optional oSchema = Optional.ofNullable(rs.getString("TABLE_SCHEM")); + String tableName = rs.getString("TABLE_NAME"); + Optional grantor = Optional.ofNullable(rs.getString("GRANTOR")); + String grantee = rs.getString("GRANTEE"); + String privilege = rs.getString("PRIVILEGE"); + Optional isGrantable = Optional.ofNullable(rs.getString("IS_GRANTABLE")); + + Optional catRef = oCat.map(CatalogReference::new); + Optional schemaRef = oSchema.map(sn -> new SchemaReference(catRef, sn)); + TableReference tableRef = new TableReference(schemaRef, tableName); + + result.add(new TablePrivilegeRecord(tableRef, grantor, grantee, privilege, isGrantable)); + } + } + return List.copyOf(result); + } + + private List getColumnPrivileges(DatabaseMetaData databaseMetaData, TableReference table, + String columnNamePattern) throws SQLException { + Optional oSchema = table.schema(); + String schema = oSchema.map(SchemaReference::name).orElse(null); + String catalog = oSchema.flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getColumnPrivileges(catalog, schema, table.name(), columnNamePattern)) { + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + Optional grantor = Optional.ofNullable(rs.getString("GRANTOR")); + String grantee = rs.getString("GRANTEE"); + String privilege = rs.getString("PRIVILEGE"); + Optional isGrantable = Optional.ofNullable(rs.getString("IS_GRANTABLE")); + + ColumnReference colRef = new ColumnReference(Optional.of(table), columnName); + result.add(new ColumnPrivilegeRecord(colRef, grantor, grantee, privilege, isGrantable)); + } + } + return List.copyOf(result); + } + + private List getSuperTypes(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String typeNamePattern) throws SQLException { + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getSuperTypes(catalog, schemaPattern, typeNamePattern)) { + while (rs.next()) { + Optional oTypeCat = Optional.ofNullable(rs.getString("TYPE_CAT")); + Optional oTypeSchema = Optional.ofNullable(rs.getString("TYPE_SCHEM")); + String typeName = rs.getString("TYPE_NAME"); + Optional oSuperCat = Optional.ofNullable(rs.getString("SUPERTYPE_CAT")); + Optional oSuperSchema = Optional.ofNullable(rs.getString("SUPERTYPE_SCHEM")); + String superTypeName = rs.getString("SUPERTYPE_NAME"); + + Optional typeCatRef = oTypeCat.map(CatalogReference::new); + Optional typeSchemaRef = oTypeSchema.map(sn -> new SchemaReference(typeCatRef, sn)); + Optional superCatRef = oSuperCat.map(CatalogReference::new); + Optional superSchemaRef = oSuperSchema + .map(sn -> new SchemaReference(superCatRef, sn)); + + result.add(new SuperTypeRecord(typeName, typeSchemaRef, superTypeName, superSchemaRef)); + } + } + return List.copyOf(result); + } + + private List getSuperTables(DatabaseMetaData databaseMetaData, String catalog, String schemaPattern, + String tableNamePattern) throws SQLException { + List result = new ArrayList<>(); + try (ResultSet rs = databaseMetaData.getSuperTables(catalog, schemaPattern, tableNamePattern)) { + while (rs.next()) { + Optional oCat = Optional.ofNullable(rs.getString("TABLE_CAT")); + Optional oSchema = Optional.ofNullable(rs.getString("TABLE_SCHEM")); + String tableName = rs.getString("TABLE_NAME"); + String superTableName = rs.getString("SUPERTABLE_NAME"); + + Optional catRef = oCat.map(CatalogReference::new); + Optional schemaRef = oSchema.map(sn -> new SchemaReference(catRef, sn)); + TableReference tableRef = new TableReference(schemaRef, tableName); + + result.add(new SuperTableRecord(tableRef, superTableName)); + } + } + return List.copyOf(result); + } + + private static OptionalInt optInt(ResultSet rs, String column) throws SQLException { + int v = rs.getInt(column); + return rs.wasNull() ? OptionalInt.empty() : OptionalInt.of(v); + } + + private static OptionalInt optShort(ResultSet rs, String column) throws SQLException { + short v = rs.getShort(column); + return rs.wasNull() ? OptionalInt.empty() : OptionalInt.of(v); + } + + private static JDBCType jdbcTypeOrOther(int dataType) { + try { + return JDBCType.valueOf(dataType); + } catch (IllegalArgumentException e) { + return JDBCType.OTHER; + } + } + + // ================================================================ + // Modern (Connection, MetadataProvider, …) surface + // Each method consults the provider first; on Optional.empty the + // private (DatabaseMetaData, …) helper above handles the JDBC fallback. + // ================================================================ + + @Override + public List getCatalogs(Connection connection, MetadataProvider provider) throws SQLException { + return provider.getAllCatalogs(connection) + .orElseGet(() -> invokeJdbc(() -> getCatalogs(connection.getMetaData()))); + } + + @Override + public List getSchemas(Connection connection, MetadataProvider provider, String catalog) + throws SQLException { + return provider.getAllSchemas(connection, catalog) + .orElseGet(() -> invokeJdbc(() -> { + CatalogReference catRef = catalog == null ? null : new CatalogReference(catalog); + return getSchemas(connection.getMetaData(), catRef); + })); + } + + @Override + public List getTableTypes(Connection connection, MetadataProvider provider) throws SQLException { + return provider.getAllTableTypes(connection) + .orElseGet(() -> invokeJdbc(() -> getTableTypes(connection.getMetaData()))); + } + + @Override + public List getTypeInfo(Connection connection, MetadataProvider provider) throws SQLException { + return provider.getAllTypeInfo(connection) + .orElseGet(() -> invokeJdbc(() -> getTypeInfo(connection.getMetaData()))); + } + + @Override + public List getTableDefinitions(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String[] types) throws SQLException { + return provider.getAllTableDefinitions(connection, catalog, schemaPattern, tableNamePattern, types) + .orElseGet(() -> invokeJdbc(() -> getTableDefinitions(connection.getMetaData(), + catalog, schemaPattern, tableNamePattern, types))); + } + + @Override + public boolean tableExists(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException { + return tableExists(connection.getMetaData(), table); + } + + @Override + public boolean tableExists(Connection connection, MetadataProvider provider, String catalog, String schemaPattern, + String tableNamePattern, String[] types) throws SQLException { + return tableExists(connection.getMetaData(), catalog, schemaPattern, tableNamePattern, types); + } + + @Override + public List getColumnDefinitions(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return provider.getAllColumnDefinitions(connection, catalog, schemaPattern, tableNamePattern, columnNamePattern) + .orElseGet(() -> invokeJdbc(() -> getColumnDefinitions(connection.getMetaData(), + catalog, schemaPattern, tableNamePattern, columnNamePattern))); + } + + @Override + public boolean columnExists(Connection connection, MetadataProvider provider, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { + return columnExists(connection.getMetaData(), catalog, schemaPattern, tableNamePattern, columnNamePattern); + } + + @Override + public boolean columnExists(Connection connection, MetadataProvider provider, ColumnReference column) + throws SQLException { + return columnExists(connection.getMetaData(), column); + } + + @Override + public List getImportedKeys(Connection connection, MetadataProvider provider, String catalog, + String schema, String tableName) throws SQLException { + Optional> allOpt = provider.getAllImportedKeys(connection, catalog, schema); + if (allOpt.isPresent()) { + List all = allOpt.get(); + List result = new ArrayList<>(); + for (ImportedKey k : all) { + if (k.foreignKeyColumn().table().isPresent() + && tableName.equals(k.foreignKeyColumn().table().get().name())) { + result.add(k); + } + } + return List.copyOf(result); + } + return getImportedKeys(connection.getMetaData(), catalog, schema, tableName); + } + + @Override + public List getExportedKeys(Connection connection, MetadataProvider provider, String catalog, + String schema, String tableName) throws SQLException { + Optional> perTable = provider.getExportedKeys(connection, catalog, schema, tableName); + if (perTable.isPresent()) { + return perTable.get(); + } + Optional> allOpt = provider.getAllExportedKeys(connection, catalog, schema); + if (allOpt.isPresent()) { + List all = allOpt.get(); + List result = new ArrayList<>(); + for (ImportedKey k : all) { + if (k.primaryKeyColumn().table().isPresent() + && tableName.equals(k.primaryKeyColumn().table().get().name())) { + result.add(k); + } + } + return List.copyOf(result); + } + return getExportedKeys(connection.getMetaData(), catalog, schema, tableName); + } + + @Override + public List getCrossReference(Connection connection, MetadataProvider provider, String parentCatalog, + String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) + throws SQLException { + return provider.getCrossReference(connection, parentCatalog, parentSchema, parentTable, + foreignCatalog, foreignSchema, foreignTable) + .orElseGet(() -> invokeJdbc(() -> getCrossReference(connection.getMetaData(), + parentCatalog, parentSchema, parentTable, + foreignCatalog, foreignSchema, foreignTable))); + } + + @Override + public List getProcedures(Connection connection, MetadataProvider provider) throws SQLException { + return getProcedures(connection.getMetaData()); + } + + @Override + public List getProcedures(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String procedureNamePattern) throws SQLException { + return getProcedures(connection.getMetaData(), catalog, schemaPattern, procedureNamePattern); + } + + @Override + public List getFunctions(Connection connection, MetadataProvider provider) throws SQLException { + return getFunctions(connection.getMetaData()); + } + + @Override + public List getFunctions(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String functionNamePattern) throws SQLException { + return getFunctions(connection.getMetaData(), catalog, schemaPattern, functionNamePattern); + } + + @Override + public List getUDTs(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return provider.getAllUDTs(connection, catalog, schemaPattern, typeNamePattern, types) + .orElseGet(() -> invokeJdbc(() -> getUDTs(connection.getMetaData(), + catalog, schemaPattern, typeNamePattern, types))); + } + + @Override + public List getBestRowIdentifier(Connection connection, MetadataProvider provider, + TableReference table, int scope, boolean nullable) throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return provider.getBestRowIdentifier(connection, catalog, schema, table.name(), scope, nullable) + .orElseGet(() -> invokeJdbc(() -> getBestRowIdentifier(connection.getMetaData(), table, scope, nullable))); + } + + @Override + public List getVersionColumns(Connection connection, MetadataProvider provider, TableReference table) + throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return provider.getVersionColumns(connection, catalog, schema, table.name()) + .orElseGet(() -> invokeJdbc(() -> getVersionColumns(connection.getMetaData(), table))); + } + + @Override + public List getPseudoColumns(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return provider.getAllPseudoColumns(connection, catalog, schemaPattern, tableNamePattern, columnNamePattern) + .orElseGet(() -> invokeJdbc(() -> getPseudoColumns(connection.getMetaData(), + catalog, schemaPattern, tableNamePattern, columnNamePattern))); + } + + @Override + public List getTablePrivileges(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + return provider.getAllTablePrivileges(connection, catalog, schemaPattern, tableNamePattern) + .orElseGet(() -> invokeJdbc(() -> getTablePrivileges(connection.getMetaData(), + catalog, schemaPattern, tableNamePattern))); + } + + @Override + public List getColumnPrivileges(Connection connection, MetadataProvider provider, + TableReference table, String columnNamePattern) throws SQLException { + String schema = table.schema().map(SchemaReference::name).orElse(null); + String catalog = table.schema().flatMap(SchemaReference::catalog).map(CatalogReference::name).orElse(null); + return provider.getColumnPrivileges(connection, catalog, schema, table.name(), columnNamePattern) + .orElseGet(() -> invokeJdbc(() -> getColumnPrivileges(connection.getMetaData(), table, columnNamePattern))); + } + + @Override + public List getSuperTypes(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String typeNamePattern) throws SQLException { + return provider.getAllSuperTypes(connection, catalog, schemaPattern, typeNamePattern) + .orElseGet(() -> invokeJdbc(() -> getSuperTypes(connection.getMetaData(), + catalog, schemaPattern, typeNamePattern))); + } + + @Override + public List getSuperTables(Connection connection, MetadataProvider provider, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + return provider.getAllSuperTables(connection, catalog, schemaPattern, tableNamePattern) + .orElseGet(() -> invokeJdbc(() -> getSuperTables(connection.getMetaData(), + catalog, schemaPattern, tableNamePattern))); + } + + private static T invokeJdbc(SqlSupplier supplier) { + try { + return supplier.get(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @FunctionalInterface + private interface SqlSupplier { + T get() throws SQLException; + } + +} diff --git a/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/package-info.java b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/package-info.java new file mode 100644 index 0000000..6310370 --- /dev/null +++ b/jdbc/impl/src/main/java/org/eclipse/daanse/sql/jdbc/impl/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.impl; diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseServiceTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseServiceTest.java new file mode 100644 index 0000000..191d32a --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CachingDatabaseServiceTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.time.Duration; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class CachingDatabaseServiceTest { + + private Connection h2KeepAlive; + private DataSource dataSource; + + private DataSource freshH2() throws Exception { + // Per-test isolated database. DB_CLOSE_DELAY=-1 + a held Connection + // keeps the in-memory schema alive across createMetaInfo calls. + String url = "jdbc:h2:mem:cache_" + UUID.randomUUID().toString().replace("-", "") + + ";DB_CLOSE_DELAY=-1"; + h2KeepAlive = DriverManager.getConnection(url, "sa", ""); + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL(url); + ds.setUser("sa"); + ds.setPassword(""); + return ds; + } + + @AfterEach + void tearDown() throws Exception { + if (h2KeepAlive != null && !h2KeepAlive.isClosed()) h2KeepAlive.close(); + } + + @Test + void cache_returns_same_instance_within_ttl() throws Exception { + dataSource = freshH2(); + CachingDatabaseService svc = new CachingDatabaseService(Duration.ofMinutes(1)); + + MetaInfo first = svc.createMetaInfo(dataSource); + MetaInfo second = svc.createMetaInfo(dataSource); + + assertThat(second).as("within TTL the cache returns the same instance").isSameAs(first); + assertThat(svc.approximateSize()).isEqualTo(1); + } + + @Test + void cache_recomputes_after_expiry() throws Exception { + dataSource = freshH2(); + // Tiny TTL to force re-computation on the second call. + CachingDatabaseService svc = new CachingDatabaseService(Duration.ofMillis(50)); + + MetaInfo first = svc.createMetaInfo(dataSource); + Thread.sleep(120); + MetaInfo second = svc.createMetaInfo(dataSource); + + assertThat(second).as("after TTL elapsed the cache returns a fresh snapshot").isNotSameAs(first); + } + + @Test + void invalidate_evicts_entry() throws Exception { + dataSource = freshH2(); + CachingDatabaseService svc = new CachingDatabaseService(Duration.ofMinutes(1)); + + MetaInfo first = svc.createMetaInfo(dataSource); + svc.invalidate(dataSource); + MetaInfo second = svc.createMetaInfo(dataSource); + + assertThat(second).as("post-invalidate the cache recomputes").isNotSameAs(first); + } + + @Test + void invalidateAll_clears_cache() throws Exception { + dataSource = freshH2(); + CachingDatabaseService svc = new CachingDatabaseService(Duration.ofMinutes(1)); + + svc.createMetaInfo(dataSource); + assertThat(svc.approximateSize()).isEqualTo(1); + svc.invalidateAll(); + assertThat(svc.approximateSize()).isZero(); + } + + @Test + void connection_path_does_not_cache() throws Exception { + dataSource = freshH2(); + CachingDatabaseService svc = new CachingDatabaseService(Duration.ofMinutes(1)); + + try (Connection c = dataSource.getConnection()) { + svc.createMetaInfo(c); + } + assertThat(svc.approximateSize()) + .as("connection-keyed snapshots bypass the cache") + .isZero(); + } + + @Test + void rejects_zero_or_negative_ttl() { + assertThatThrownBy(() -> new CachingDatabaseService(Duration.ZERO)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new CachingDatabaseService(Duration.ofMillis(-1))) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CoreTestAuditTrigger.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CoreTestAuditTrigger.java new file mode 100644 index 0000000..2214e05 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/CoreTestAuditTrigger.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.h2.api.Trigger; + +public class CoreTestAuditTrigger implements Trigger { + + @Override + public void init(Connection conn, String schemaName, String triggerName, + String tableName, boolean before, int type) throws SQLException { + // no-op + } + + @Override + public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { + // no-op + } + + @Override + public void close() throws SQLException { + // no-op + } + + @Override + public void remove() throws SQLException { + // no-op + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplH2Test.java new file mode 100644 index 0000000..ecb7fc5 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplH2Test.java @@ -0,0 +1,277 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TableMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +class DatabaseServiceImplH2Test { + private static final Logger LOGGER = org.slf4j.LoggerFactory.getLogger(DatabaseServiceImplH2Test.class); + private DatabaseService databaseService = new DatabaseServiceImpl(); + + private String catalogName = UUID.randomUUID().toString().toUpperCase(); + + private DataSource ds() { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:memFS:" + catalogName); + ds.setUser("sa"); + ds.setPassword("sa"); + return ds; + } + + private void setupData(Connection connection) throws SQLException { + String sql = "Create table test (ID int primary key, name varchar(50), val numeric(10,3) NOT NULL, birthday date NOT NULL, t time NOT NULL)"; + + Statement statement = connection.createStatement(); + + statement.execute(sql); + + LOGGER.debug("Created test table."); + + sql = "Insert into test (ID, name, val, birthday, t) values (1, 'name', 13.3, '1973-01-07', '18:20:59')"; + + int rows = statement.executeUpdate(sql); + + if (rows > 0) { + LOGGER.debug("Inserted a new row."); + } + connection.commit(); + } + + @Test + void createMetaDataTest() throws SQLException { + DataSource ds = ds(); + MetaInfo metaInfo = databaseService.createMetaInfo(ds); + assertThat(metaInfo).isNotNull(); + } + + @Test + void getTypeInfoTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + List types = databaseService.getTypeInfo(databaseMetaData.getConnection(), MetadataProvider.EMPTY); + assertThat(types).isNotNull().isNotEmpty(); + } + + @Test + void getCatalogsTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + List catalogs = databaseService.getCatalogs(databaseMetaData.getConnection(), MetadataProvider.EMPTY); + + assertThat(catalogs).isNotNull().isNotEmpty(); + } + + @Test + void getSchemasTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + List schemas = databaseService.getSchemas(databaseMetaData.getConnection(), MetadataProvider.EMPTY, null); + assertThat(schemas).isNotNull().isNotEmpty(); + } + + @Test + void getTablesTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + List tables = databaseService.getTableDefinitions(databaseMetaData.getConnection(), MetadataProvider.EMPTY); + assertThat(tables).isNotNull().isNotEmpty(); + } + + @Test + void getTableTypesTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + List tableTypes = databaseService.getTableTypes(databaseMetaData.getConnection(), MetadataProvider.EMPTY); + assertThat(tableTypes).isNotNull().isNotEmpty(); + } + + @Test + void tableExistsTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + + // + boolean constantsTableExists = databaseService.tableExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new TableReference("CONSTANTS")); + assertThat(constantsTableExists).isTrue(); + + // + constantsTableExists = databaseService.tableExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new TableReference("CONSTANTS", "TABLE")); + assertThat(constantsTableExists).isTrue(); + + // + constantsTableExists = databaseService.tableExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY, + new TableReference("CONSTANTS", "BASE TABLE")); + assertThat(constantsTableExists).isTrue(); + + // + Optional oSchema = Optional.of(new SchemaReference("INFORMATION_SCHEMA")); + constantsTableExists = databaseService.tableExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY, + new TableReference(oSchema, "CONSTANTS", "TABLE")); + assertThat(constantsTableExists).isTrue(); + + // + Optional oCatalog = Optional.of(new CatalogReference(catalogName)); + oSchema = Optional.of(new SchemaReference(oCatalog, "INFORMATION_SCHEMA")); + constantsTableExists = databaseService.tableExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY, + new TableReference(oSchema, "CONSTANTS", "TABLE")); + assertThat(constantsTableExists).isTrue(); + } + + @Test + void tableColumnTest() throws SQLException { + DataSource ds = ds(); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + + // + boolean exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference("USER_NAME")); + assertThat(exists).isTrue(); + + // + Optional oTable = Optional.of(new TableReference("USERS")); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "USER_NAME")); + assertThat(exists).isTrue(); + + // + oTable = Optional.of(new TableReference("USERS", "TABLE")); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "USER_NAME")); + assertThat(exists).isTrue(); + + // + oTable = Optional.of(new TableReference("USERS", "BASE TABLE")); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "USER_NAME")); + assertThat(exists).isTrue(); + + // + Optional oSchema = Optional.of(new SchemaReference("INFORMATION_SCHEMA")); + oTable = Optional.of(new TableReference(oSchema, "USERS", "TABLE")); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "USER_NAME")); + assertThat(exists).isTrue(); + + // + + Optional oCatalog = Optional.of(new CatalogReference(catalogName)); + oSchema = Optional.of(new SchemaReference(oCatalog, "INFORMATION_SCHEMA")); + oTable = Optional.of(new TableReference(oSchema, "USERS", "TABLE")); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "USER_NAME")); + assertThat(exists).isTrue(); + } + + @Test + void tableColumnTestWithCustomerTables() throws SQLException { + DataSource ds = ds(); + try (Connection connection = ds.getConnection()) { + setupData(connection); + DatabaseMetaData databaseMetaData = ds.getConnection().getMetaData(); + Optional oCatalog = Optional.of(new CatalogReference(catalogName)); + Optional oSchema = Optional.of(new SchemaReference(oCatalog, "PUBLIC")); + Optional oTable = Optional.of(new TableReference(oSchema, "TEST", "BASE TABLE")); + List tableDefinitions = databaseService.getTableDefinitions(databaseMetaData.getConnection(), MetadataProvider.EMPTY,oSchema.get()); + assertThat(tableDefinitions).isNotNull().hasSize(1); + boolean exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "ID")); + assertThat(exists).isTrue(); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "NAME")); + assertThat(exists).isTrue(); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "VAL")); + assertThat(exists).isTrue(); + exists = databaseService.columnExists(databaseMetaData.getConnection(), MetadataProvider.EMPTY,new ColumnReference(oTable, "T")); + assertThat(exists).isTrue(); + List columnDefinitions = databaseService.getColumnDefinitions(databaseMetaData.getConnection(), MetadataProvider.EMPTY,oTable.get()); + assertThat(columnDefinitions).isNotNull().hasSize(5); + } + } + + @Test + void testWithCustomerTables() throws SQLException { + DataSource ds = ds(); + try (Connection connection = ds.getConnection()) { + setupData(connection); + MetaInfo metaInfo = databaseService.createMetaInfo(connection); + + StructureInfo structureInfo = metaInfo.structureInfo(); + assertThat(structureInfo).isNotNull(); + + //check catalog + assertThat(structureInfo.catalogs()).isNotNull().hasSize(1); + assertThat(structureInfo.catalogs().get(0)).isNotNull(); + assertThat(structureInfo.catalogs().get(0).name()).isNotNull(); + + //check schemas + assertThat(structureInfo.schemas()).isNotNull().hasSize(2); + assertThat(structureInfo.schemas().stream().anyMatch(s -> "PUBLIC".equals(s.name()))).isTrue(); + assertThat(structureInfo.schemas().stream().anyMatch(s -> "INFORMATION_SCHEMA".equals(s.name()))).isTrue(); + Optional oSchemaReference = structureInfo.schemas().stream().filter(s -> "PUBLIC".equals(s.name())).findAny(); + assertThat(oSchemaReference).isNotNull().isPresent(); + assertThat(oSchemaReference.get()).isNotNull(); + assertThat(oSchemaReference.get().name()).isNotNull(); + assertThat(oSchemaReference.get().catalog()).isNotNull().isPresent(); + + //check tables + assertThat(structureInfo.tables()).isNotNull().hasSize(36); + assertThat(structureInfo.tables().stream().anyMatch(t -> "TEST".equals(t.table().name()))).isTrue(); + Optional oTableDefinition = structureInfo.tables().stream().filter(t -> "TEST".equals(t.table().name())).findAny(); + assertThat(oTableDefinition).isNotNull().isPresent(); + assertThat(oTableDefinition.get()).isNotNull(); + TableReference tableReference = oTableDefinition.get().table(); + assertThat(tableReference).isNotNull(); + assertThat(tableReference.name()).isNotNull(); + assertThat(tableReference.schema()).isNotNull().isPresent(); + assertThat(tableReference.schema().get()).isNotNull(); + assertThat(tableReference.schema().get().name()).isNotNull(); + TableMetaData tableMetaData = oTableDefinition.get().tableMetaData(); + assertThat(tableMetaData).isNotNull(); + + //check columns + assertThat(structureInfo.columns()).isNotNull().isNotEmpty(); + assertThat(structureInfo.columns().get(0)).isNotNull(); + ColumnDefinition columnDefinition = structureInfo.columns().get(0); + assertThat(columnDefinition.column()).isNotNull(); + assertThat(columnDefinition.column().name()).isNotNull(); + assertThat(columnDefinition.column().table()).isNotNull(); + Optional oTableReference = columnDefinition.column().table(); + assertThat(oTableReference).isNotNull().isPresent(); + assertThat(oTableReference.get()).isNotNull(); + assertThat(oTableReference.get().name()).isNotNull(); + assertThat(oTableReference.get().schema()).isNotNull().isPresent(); + assertThat(oTableReference.get().schema().get()).isNotNull(); + assertThat(oTableReference.get().schema().get().name()).isNotNull(); + } + } + +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplMocksTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplMocksTest.java new file mode 100644 index 0000000..a3f77ff --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplMocksTest.java @@ -0,0 +1,103 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.DatabaseInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IdentifierInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class DatabaseServiceImplMocksTest { + + private DatabaseService databaseService = new DatabaseServiceImpl(); + + @Mock + DataSource ds; + @Mock + Connection connection; + @Mock + DatabaseMetaData databaseMetaData; + @Mock + ResultSet resultSet; + + @Mock + ResultSetMetaData resultSetMetaData; + + @Test + void createMetaDataTest() throws SQLException { + + when(ds.getConnection()).thenReturn(connection); + when(connection.getMetaData()).thenReturn(databaseMetaData); + + when(databaseMetaData.getIdentifierQuoteString()).thenReturn("\""); + when(databaseMetaData.getDatabaseMajorVersion()).thenReturn(42); + when(databaseMetaData.getDatabaseMinorVersion()).thenReturn(21); + when(databaseMetaData.getDatabaseProductName()).thenReturn("MyDB"); + when(databaseMetaData.getDatabaseProductVersion()).thenReturn("a"); + when(databaseMetaData.getTypeInfo()).thenReturn(resultSet); + when(databaseMetaData.getCatalogs()).thenReturn(resultSet); + + when(resultSet.getMetaData()).thenReturn(resultSetMetaData); + when(resultSetMetaData.getColumnCount()).thenReturn(6); + + when(resultSetMetaData.getColumnName(1)).thenReturn("TABLE_CAT"); + when(resultSetMetaData.getColumnName(2)).thenReturn("TABLE_SCHEM"); + when(resultSetMetaData.getColumnName(3)).thenReturn("TABLE_NAME"); + when(resultSetMetaData.getColumnName(4)).thenReturn("TABLE_TYPE"); + when(resultSetMetaData.getColumnName(5)).thenReturn("REMARKS"); + when(resultSetMetaData.getColumnName(6)).thenReturn("TYPE_CAT"); +// when(databaseMetaData.getSchemas()).thenReturn(resultSet); + when(databaseMetaData.getSchemas(Mockito.any(), Mockito.any())).thenReturn(resultSet); + when(databaseMetaData.getTables(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(resultSet); + when(databaseMetaData.getColumns(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())) + .thenReturn(resultSet); +// when(databaseMetaData.getImportedKeys(Mockito.any(),Mockito.any(),Mockito.any())).thenReturn(resultSet); + + when(resultSet.next()).thenReturn(false); + + MetaInfo metaInfo = databaseService.createMetaInfo(ds); + + assertThat(metaInfo).isNotNull(); + + DatabaseInfo databaseInfo = metaInfo.databaseInfo(); + assertThat(databaseInfo).isNotNull(); + assertThat(databaseInfo.databaseProductName()).isEqualTo("MyDB"); + assertThat(databaseInfo.databaseProductVersion()).isEqualTo("a"); + assertThat(databaseInfo.databaseMajorVersion()).isEqualTo(42); + assertThat(databaseInfo.databaseMinorVersion()).isEqualTo(21); + + IdentifierInfo identifierInfo = metaInfo.identifierInfo(); + assertThat(identifierInfo).isNotNull(); + assertThat(identifierInfo.quoteString()).isEqualTo("\""); + } + +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplProviderH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplProviderH2Test.java new file mode 100644 index 0000000..3838138 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceImplProviderH2Test.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.Statement; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DatabaseServiceImplProviderH2Test { + + private static Connection connection; + private static H2Dialect dialect; + private static MetaInfo metaInfo; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection( + "jdbc:h2:mem:coreProviderTest;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + // Sequences + stmt.execute("CREATE SEQUENCE SEQ_ORDER_ID START WITH 1000 INCREMENT BY 1"); + stmt.execute(""" + CREATE SEQUENCE SEQ_AUDIT_ID START WITH 1 INCREMENT BY 5 + MINVALUE 1 MAXVALUE 999999 CYCLE CACHE 10 + """); + + // Tables + stmt.execute(""" + CREATE TABLE DEPARTMENTS ( + DEPT_ID INT NOT NULL, + DEPT_NAME VARCHAR(100) NOT NULL, + LOCATION VARCHAR(200), + CONSTRAINT PK_DEPARTMENTS PRIMARY KEY (DEPT_ID), + CONSTRAINT UQ_DEPT_NAME UNIQUE (DEPT_NAME), + CONSTRAINT CK_DEPT_NAME_LEN CHECK (LENGTH(DEPT_NAME) >= 2) + ) + """); + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL, + FIRST_NAME VARCHAR(50) NOT NULL, + LAST_NAME VARCHAR(50) NOT NULL, + EMAIL VARCHAR(100) NOT NULL, + SALARY DECIMAL(10,2), + DEPT_ID INT, + CONSTRAINT PK_EMPLOYEES PRIMARY KEY (EMP_ID), + CONSTRAINT UQ_EMP_EMAIL UNIQUE (EMAIL), + CONSTRAINT CK_SALARY_POSITIVE CHECK (SALARY > 0), + CONSTRAINT FK_EMP_DEPT FOREIGN KEY (DEPT_ID) + REFERENCES DEPARTMENTS(DEPT_ID) ON DELETE SET NULL ON UPDATE CASCADE + ) + """); + stmt.execute(""" + CREATE TABLE ORDERS ( + ORDER_ID INT NOT NULL, + EMP_ID INT NOT NULL, + ORDER_DATE DATE NOT NULL, + STATUS VARCHAR(20) DEFAULT 'PENDING', + CONSTRAINT PK_ORDERS PRIMARY KEY (ORDER_ID), + CONSTRAINT CK_STATUS CHECK (STATUS IN ('PENDING', 'PROCESSING', 'COMPLETED', 'CANCELLED')), + CONSTRAINT FK_ORD_EMP FOREIGN KEY (EMP_ID) REFERENCES EMPLOYEES(EMP_ID) + ) + """); + stmt.execute(""" + CREATE TABLE ORDER_ITEMS ( + ORDER_ID INT NOT NULL, + ITEM_SEQ INT NOT NULL, + PRODUCT_NAME VARCHAR(100) NOT NULL, + QUANTITY INT NOT NULL, + AMOUNT DECIMAL(10,2) NOT NULL, + CONSTRAINT PK_ORDER_ITEMS PRIMARY KEY (ORDER_ID, ITEM_SEQ), + CONSTRAINT CK_QUANTITY CHECK (QUANTITY > 0), + CONSTRAINT CK_AMOUNT_POSITIVE CHECK (AMOUNT > 0), + CONSTRAINT FK_OI_ORDER FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID) + ) + """); + stmt.execute(""" + CREATE TABLE AUDIT_LOG ( + LOG_ID INT NOT NULL, + TABLE_NAME VARCHAR(100) NOT NULL, + ACTION_TYPE VARCHAR(20) NOT NULL, + ACTION_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + USER_NAME VARCHAR(100), + CONSTRAINT PK_AUDIT_LOG PRIMARY KEY (LOG_ID), + CONSTRAINT UQ_AUDIT_UNIQUE UNIQUE (TABLE_NAME, ACTION_TYPE, ACTION_TIMESTAMP) + ) + """); + + // Views + stmt.execute(""" + CREATE VIEW V_EMP_DEPT AS + SELECT E.EMP_ID, E.FIRST_NAME, E.LAST_NAME, E.EMAIL, E.SALARY, + D.DEPT_NAME, D.LOCATION + FROM EMPLOYEES E LEFT JOIN DEPARTMENTS D ON E.DEPT_ID = D.DEPT_ID + """); + stmt.execute(""" + CREATE VIEW V_ORDER_SUMMARY AS + SELECT O.ORDER_ID, O.ORDER_DATE, O.STATUS, + E.FIRST_NAME || ' ' || E.LAST_NAME AS EMP_NAME + FROM ORDERS O JOIN EMPLOYEES E ON O.EMP_ID = E.EMP_ID + """); + + // Triggers (H2 uses CALL "class.name" -- TestAuditTrigger is in dialect module) + // Note: We use a simple Java class name that can be resolved from classpath + stmt.execute(""" + CREATE TRIGGER TRG_EMP_AUDIT AFTER INSERT ON EMPLOYEES + FOR EACH ROW CALL "org.eclipse.daanse.sql.jdbc.impl.CoreTestAuditTrigger" + """); + + // Functions (H2 uses CREATE ALIAS) + stmt.execute(""" + CREATE ALIAS CALC_BONUS AS $$ + double calcBonus(double salary, int years) { + return salary * years * 0.01; + } + $$ + """); + } + + dialect = new H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + DatabaseServiceImpl service = new DatabaseServiceImpl(); + metaInfo = service.createMetaInfo(connection, new org.eclipse.daanse.sql.jdbc.metadata.H2MetadataProvider()); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP ALL OBJECTS"); + } + connection.close(); + } + } + + @Test + void structureInfo_containsTriggers() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.triggers()).isNotEmpty(); + assertThat(si.triggers()).anyMatch(t -> "TRG_EMP_AUDIT".equals(t.name())); + } + + @Test + void structureInfo_containsSequences() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.sequences()).isNotEmpty(); + assertThat(si.sequences()).anyMatch(s -> "SEQ_ORDER_ID".equals(s.name())); + assertThat(si.sequences()).anyMatch(s -> "SEQ_AUDIT_ID".equals(s.name())); + } + + @Test + void structureInfo_containsCheckConstraints() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.checkConstraints()).isNotEmpty(); + assertThat(si.checkConstraints()).anyMatch(c -> "CK_SALARY_POSITIVE".equals(c.name())); + assertThat(si.checkConstraints()).anyMatch(c -> "CK_STATUS".equals(c.name())); + } + + @Test + void structureInfo_containsUniqueConstraints() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.uniqueConstraints()).isNotEmpty(); + assertThat(si.uniqueConstraints()).anyMatch(c -> "UQ_DEPT_NAME".equals(c.name())); + assertThat(si.uniqueConstraints()).anyMatch(c -> "UQ_EMP_EMAIL".equals(c.name())); + assertThat(si.uniqueConstraints()).anyMatch(c -> "UQ_AUDIT_UNIQUE".equals(c.name())); + } + + @Test + void structureInfo_containsViewDefinitions() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.viewDefinitions()).isNotEmpty(); + assertThat(si.viewDefinitions()).anyMatch(v -> "V_EMP_DEPT".equals(v.view().name())); + assertThat(si.viewDefinitions()).anyMatch(v -> "V_ORDER_SUMMARY".equals(v.view().name())); + } + + @Test + void primaryKeys_notEmpty() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.primaryKeys()).isNotEmpty(); + assertThat(si.primaryKeys()).hasSizeGreaterThanOrEqualTo(5); + } + + @Test + void importedKeys_notEmpty() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.importedKeys()).isNotEmpty(); + assertThat(si.importedKeys()).hasSizeGreaterThanOrEqualTo(3); + } + + @Test + void indexInfo_notEmpty() { + assertThat(metaInfo.indexInfos()).isNotEmpty(); + } + + @Test + void standardMetadata_populated() { + assertThat(metaInfo.databaseInfo()).isNotNull(); + assertThat(metaInfo.databaseInfo().databaseProductName()).isEqualTo("H2"); + assertThat(metaInfo.identifierInfo()).isNotNull(); + assertThat(metaInfo.typeInfos()).isNotEmpty(); + } + + @Test + void structureInfo_containsFunctions() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.functions()).isNotEmpty(); + assertThat(si.functions()).anyMatch(f -> "CALC_BONUS".equals(f.reference().name())); + } + + @Test + void structureInfo_proceduresNotNull() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.procedures()).isNotNull(); + } + + @Test + void userDefinedTypes_empty() { + StructureInfo si = metaInfo.structureInfo(); + assertThat(si.userDefinedTypes()).isEmpty(); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceJdbcWrappersH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceJdbcWrappersH2Test.java new file mode 100644 index 0000000..7256c60 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceJdbcWrappersH2Test.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.schema.BestRowIdentifier; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.VersionColumn; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DatabaseServiceJdbcWrappersH2Test { + + private static Connection connection; + private static DatabaseService service; + private static DatabaseMetaData dmd; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection( + "jdbc:h2:mem:jdbcWrappers;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL, + FIRST_NAME VARCHAR(50) NOT NULL, + LAST_NAME VARCHAR(50) NOT NULL, + CONSTRAINT PK_EMP PRIMARY KEY (EMP_ID) + ) + """); + } + service = new DatabaseServiceImpl(); + dmd = connection.getMetaData(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP ALL OBJECTS"); + } + connection.close(); + } + } + + private static TableReference employees() { + return new TableReference(Optional.of(new SchemaReference(Optional.empty(), "PUBLIC")), "EMPLOYEES"); + } + + @Test + void getUDTs_returnsEmptyListForH2() throws Exception { + List udts = service.getUDTs(connection, MetadataProvider.EMPTY, null, null, null, null); + assertThat(udts).isNotNull(); + } + + @Test + void getBestRowIdentifier_returnsPrimaryKeyColumnForEmployees() throws Exception { + List best = service.getBestRowIdentifier(connection, MetadataProvider.EMPTY, employees(), + DatabaseMetaData.bestRowSession, false); + assertThat(best).isNotEmpty(); + assertThat(best).anyMatch(b -> "EMP_ID".equalsIgnoreCase(b.column().name())); + } + + @Test + void getVersionColumns_returnsListForEmployees() throws Exception { + List vcs = service.getVersionColumns(connection, MetadataProvider.EMPTY, employees()); + // H2 has no auto-updated version columns; contract: return an empty list, not null. + assertThat(vcs).isNotNull(); + } + + @Test + void getPseudoColumns_returnsListForSchema() throws Exception { + List pcs = service.getPseudoColumns(connection, MetadataProvider.EMPTY, null, "PUBLIC", "%", "%"); + // H2 may or may not expose pseudo columns; just verify null-safety. + assertThat(pcs).isNotNull(); + } + + @Test + void getTablePrivileges_returnsList() throws Exception { + List tps = service.getTablePrivileges(connection, MetadataProvider.EMPTY, null, "PUBLIC", "%"); + assertThat(tps).isNotNull(); + } + + @Test + void getColumnPrivileges_returnsList() throws Exception { + List cps = service.getColumnPrivileges(connection, MetadataProvider.EMPTY, employees(), "%"); + assertThat(cps).isNotNull(); + } + + @Test + void getSuperTypes_returnsList() throws Exception { + // Drain the ResultSet via our wrapper; H2 returns an empty set. + List sts = service.getSuperTypes(connection, MetadataProvider.EMPTY, null, "PUBLIC", "%"); + assertThat(sts).isNotNull(); + } + + @Test + void getSuperTables_returnsList() throws Exception { + List sts = service.getSuperTables(connection, MetadataProvider.EMPTY, null, "PUBLIC", "%"); + assertThat(sts).isNotNull(); + } + + @Test + void getUDTs_matchesRawJdbcRowCount() throws Exception { + // Sanity: wrapper walks the ResultSet once and keeps the row count consistent. + int expected; + try (ResultSet rs = dmd.getUDTs(null, null, null, null)) { + int c = 0; + while (rs.next()) c++; + expected = c; + } + List udts = service.getUDTs(connection, MetadataProvider.EMPTY, null, null, null, null); + assertThat(udts).hasSize(expected); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceProviderFallbackH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceProviderFallbackH2Test.java new file mode 100644 index 0000000..e8d9375 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DatabaseServiceProviderFallbackH2Test.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.jdbc.record.schema.TableDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TableMetaDataRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DatabaseServiceProviderFallbackH2Test { + + private static Connection connection; + private static DatabaseService service; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection( + "jdbc:h2:mem:providerFallback;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + stmt.execute(""" + CREATE TABLE DEPARTMENTS ( + DEPT_ID INT NOT NULL PRIMARY KEY, + DEPT_NAME VARCHAR(100) NOT NULL + ) + """); + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL PRIMARY KEY, + DEPT_ID INT, + CONSTRAINT FK_EMP_DEPT FOREIGN KEY (DEPT_ID) REFERENCES DEPARTMENTS(DEPT_ID) + ) + """); + } + service = new DatabaseServiceImpl(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP ALL OBJECTS"); + } + connection.close(); + } + } + + /** A MetadataProvider that intentionally declines (always returns Optional.empty()). */ + private static MetadataProvider declining() { + return new MetadataProvider() { }; + } + + // ---------- Catalogs ---------- + + @Test + void getCatalogs_providerEmpty_fallsBackToJdbc() throws SQLException { + List viaProvider = service.getCatalogs(connection, declining()); + // Compare against the same modern path with a no-op provider — both paths must agree. + List viaJdbc = service.getCatalogs(connection, MetadataProvider.EMPTY); + assertThat(viaProvider).containsExactlyElementsOf(viaJdbc); + } + + @Test + void getCatalogs_providerPresent_usedVerbatim() throws SQLException { + List sentinel = List.of(new CatalogReference("FAKE_CAT")); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllCatalogs(Connection c) { + return Optional.of(sentinel); + } + }; + List out = service.getCatalogs(connection, provider); + assertThat(out).isSameAs(sentinel); + } + + // ---------- Schemas ---------- + + @Test + void getSchemas_providerEmpty_fallsBackToJdbc() throws SQLException { + List out = service.getSchemas(connection, declining(), null); + assertThat(out).isNotEmpty(); + } + + @Test + void getSchemas_providerPresent_usedVerbatim() throws SQLException { + List sentinel = List.of( + new SchemaReference(Optional.of(new CatalogReference("CAT")), "MADE_UP")); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllSchemas(Connection c, String catalog) { + return Optional.of(sentinel); + } + }; + assertThat(service.getSchemas(connection, provider, null)).isSameAs(sentinel); + } + + // ---------- Table types ---------- + + @Test + void getTableTypes_providerEmpty_fallsBackToJdbc() throws SQLException { + List types = service.getTableTypes(connection, declining()); + assertThat(types).isNotEmpty(); + } + + @Test + void getTableTypes_providerPresent_usedVerbatim() throws SQLException { + List sentinel = List.of("FOO", "BAR"); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllTableTypes(Connection c) { + return Optional.of(sentinel); + } + }; + assertThat(service.getTableTypes(connection, provider)).isSameAs(sentinel); + } + + // ---------- Tables ---------- + + @Test + void getTableDefinitions_providerEmpty_fallsBackToJdbc() throws SQLException { + List defs = service.getTableDefinitions(connection, declining(), + null, "PUBLIC", "%", null); + assertThat(defs).anyMatch(td -> "EMPLOYEES".equalsIgnoreCase(td.table().name())); + } + + @Test + void getTableDefinitions_providerPresent_usedVerbatim() throws SQLException { + TableReference ref = new TableReference( + Optional.of(new SchemaReference(Optional.empty(), "PUBLIC")), + "FAKE_TABLE"); + List sentinel = List.of(new TableDefinitionRecord(ref, + new TableMetaDataRecord(Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty(), Optional.empty(), Optional.empty()))); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllTableDefinitions(Connection c, String catalog, + String schemaPattern, String tableNamePattern, String[] types) { + return Optional.of(sentinel); + } + }; + assertThat(service.getTableDefinitions(connection, provider, null, "PUBLIC", "%", null)) + .isSameAs(sentinel); + } + + // ---------- Columns ---------- + + @Test + void getColumnDefinitions_providerEmpty_fallsBackToJdbc() throws SQLException { + List cols = service.getColumnDefinitions(connection, declining(), + null, "PUBLIC", "EMPLOYEES", "%"); + assertThat(cols).anyMatch(c -> "EMP_ID".equalsIgnoreCase(c.column().name())); + } + + @Test + void getColumnDefinitions_providerPresent_usedVerbatim() throws SQLException { + List sentinel = List.of(); // empty marker list that the provider "owns" + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllColumnDefinitions(Connection c, String catalog, + String schemaPattern, String tableNamePattern, String columnNamePattern) { + return Optional.of(sentinel); + } + }; + assertThat(service.getColumnDefinitions(connection, provider, null, "PUBLIC", "EMPLOYEES", "%")) + .isSameAs(sentinel); + } + + // ---------- ExportedKeys ---------- + + @Test + void getExportedKeys_providerEmpty_fallsBackToJdbc() throws SQLException { + TableReference departments = new TableReference( + Optional.of(new SchemaReference(Optional.empty(), "PUBLIC")), + "DEPARTMENTS"); + List keys = service.getExportedKeys(connection, declining(), + null, "PUBLIC", departments.name()); + // EMPLOYEES.FK_EMP_DEPT points at DEPARTMENTS — JDBC reports it. + assertThat(keys).anyMatch(k -> "FK_EMP_DEPT".equals(k.name()) + || "FK_EMP_DEPT".equalsIgnoreCase(k.name())); + } + + @Test + void getExportedKeys_providerAllKeysPresent_usedVerbatim() throws SQLException { + List sentinel = List.of(); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllExportedKeys(Connection c, String catalog, String schema) { + return Optional.of(sentinel); + } + }; + assertThat(service.getExportedKeys(connection, provider, null, "PUBLIC", "DEPARTMENTS")) + .isEmpty(); + } + + // ---------- UDTs (full signature) ---------- + + @Test + void getUDTs_providerEmpty_fallsBackToJdbc() throws SQLException { + // H2 has no UDTs; contract: non-null result, no exception. + assertThat(service.getUDTs(connection, declining(), null, "PUBLIC", "%", null)).isNotNull(); + } + + // ---------- TablePrivileges ---------- + + @Test + void getTablePrivileges_providerEmpty_fallsBackToJdbc() throws SQLException { + List tps = service.getTablePrivileges(connection, declining(), null, "PUBLIC", "%"); + assertThat(tps).isNotNull(); + } + + @Test + void getTablePrivileges_providerPresent_usedVerbatim() throws SQLException { + List sentinel = List.of(); + MetadataProvider provider = new MetadataProvider() { + @Override + public Optional> getAllTablePrivileges(Connection c, String catalog, + String schemaPattern, String tableNamePattern) { + return Optional.of(sentinel); + } + }; + assertThat(service.getTablePrivileges(connection, provider, null, "PUBLIC", "%")) + .isSameAs(sentinel); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitDataLatencyTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitDataLatencyTest.java new file mode 100644 index 0000000..1c3c062 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitDataLatencyTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.SQLException; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.dialect.api.DialectInitData; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; + +class DialectInitDataLatencyTest { + + @Test + void fromDataSource_under_50ms_on_H2() throws SQLException { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:bench" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1"); + ds.setUser("sa"); + ds.setPassword(""); + + // Warm-up: amortise H2 first-connection setup, JIT, etc. + for (int i = 0; i < 3; i++) { + DialectInitData.fromDataSource(ds); + } + + long best = Long.MAX_VALUE; + for (int i = 0; i < 5; i++) { + long t0 = System.nanoTime(); + DialectInitData data = DialectInitData.fromDataSource(ds); + long t = System.nanoTime() - t0; + if (t < best) best = t; + assertThat(data).isNotNull(); + assertThat(data.productName()).isEqualTo("H2"); + } + long bestMs = best / 1_000_000L; + assertThat(bestMs) + .as("DialectInitData.fromDataSource(H2 in-mem) best-of-5 should be well under 50 ms (was %d ms)", bestMs) + .isLessThan(50L); + // Also assert the snapshot has the expected shape. + DialectInitData data = DialectInitData.fromDataSource(ds); + assertThat(data.quoteIdentifierString()).isEqualTo("\""); + assertThat(data.databaseMajorVersion()).isPositive(); + assertThat(data.sqlKeywordsLower()).isNotEmpty(); // H2 reports its own keywords + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitMetaInfoLatencyTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitMetaInfoLatencyTest.java new file mode 100644 index 0000000..3e2f20d --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/DialectInitMetaInfoLatencyTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.Test; + +class DialectInitMetaInfoLatencyTest { + + private static final int WARMUP_ROUNDS = 3; + private static final int MEASURE_ROUNDS = 5; + + private final DatabaseService service = new DatabaseServiceImpl(); + + @Test + void measure() throws SQLException { + System.out.println(); + System.out.println("=== createMetaInfo() vs lightweight dialect-init reads ==="); + System.out.printf("%-30s %15s %15s %10s%n", "scenario", "createMetaInfo", "dialect-init", "ratio"); + + for (int n : new int[] { 0, 10, 100, 500, 1000 }) { + DataSource ds = freshDb(); + populate(ds, n); + + // warm + for (int i = 0; i < WARMUP_ROUNDS; i++) { + service.createMetaInfo(ds); + lightweightDialectInit(ds); + } + + long meta = bestOf(MEASURE_ROUNDS, () -> service.createMetaInfo(ds)); + long light = bestOf(MEASURE_ROUNDS, () -> lightweightDialectInit(ds)); + + System.out.printf("%-30s %12.3f ms %12.3f ms %8.1fx%n", + n + " tables", meta / 1_000_000.0, light / 1_000_000.0, + light == 0 ? 0.0 : (double) meta / light); + } + System.out.println(); + } + + /** Reads only what {@code AbstractJdbcDialect(Connection)} consults. */ + private static void lightweightDialectInit(DataSource ds) throws SQLException { + try (Connection c = ds.getConnection()) { + DatabaseMetaData md = c.getMetaData(); + md.getIdentifierQuoteString(); + md.getDatabaseProductName(); + md.getDatabaseProductVersion(); + md.getDatabaseMajorVersion(); + md.getDatabaseMinorVersion(); + md.isReadOnly(); + md.getMaxColumnNameLength(); + md.getSQLKeywords(); + // supportedResultSetConcurrency matrix — same as MetaInfo path + for (int t : new int[] { java.sql.ResultSet.TYPE_FORWARD_ONLY, + java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE, + java.sql.ResultSet.TYPE_SCROLL_SENSITIVE }) { + for (int conc : new int[] { java.sql.ResultSet.CONCUR_READ_ONLY, + java.sql.ResultSet.CONCUR_UPDATABLE }) { + md.supportsResultSetConcurrency(t, conc); + } + } + } + } + + @FunctionalInterface + private interface SqlRunnable { + void run() throws SQLException; + } + + private static long bestOf(int rounds, SqlRunnable r) throws SQLException { + long best = Long.MAX_VALUE; + for (int i = 0; i < rounds; i++) { + long t0 = System.nanoTime(); + r.run(); + long t = System.nanoTime() - t0; + if (t < best) best = t; + } + return best; + } + + private DataSource freshDb() { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:bench" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1"); + ds.setUser("sa"); + ds.setPassword(""); + return ds; + } + + private static void populate(DataSource ds, int tableCount) throws SQLException { + if (tableCount == 0) return; + try (Connection c = ds.getConnection(); Statement s = c.createStatement()) { + for (int i = 0; i < tableCount; i++) { + s.execute("CREATE TABLE T_" + i + + " (ID INT PRIMARY KEY, NAME VARCHAR(50), VAL DECIMAL(12,3)," + + " BIRTHDAY DATE, CREATED TIMESTAMP)"); + if (i > 0) { + s.execute("ALTER TABLE T_" + i + + " ADD CONSTRAINT FK_" + i + + " FOREIGN KEY (ID) REFERENCES T_" + (i - 1) + "(ID)"); + } + s.execute("CREATE INDEX IDX_" + i + "_NAME ON T_" + i + "(NAME)"); + } + c.commit(); + } + } + + /** Independent sanity test — confirms MetaInfo loads at all on a fresh DB. */ + @Test + void smokeTest_metaInfoNotEmpty() throws SQLException { + MetaInfo info = service.createMetaInfo(freshDb()); + if (info == null) throw new AssertionError("MetaInfo was null"); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/integration/ServiceTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/integration/ServiceTest.java new file mode 100644 index 0000000..515eadb --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/integration/ServiceTest.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.impl.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.common.annotation.InjectService; +import org.osgi.test.junit5.cm.ConfigurationExtension; +import org.osgi.test.junit5.context.BundleContextExtension; +import org.osgi.test.junit5.service.ServiceExtension; + +@ExtendWith(BundleContextExtension.class) +@ExtendWith(ServiceExtension.class) +@ExtendWith(ConfigurationExtension.class) +class ServiceTest { + + @Test + void serviceWithConfigurationTest(@InjectService(timeout = 500) DatabaseService databaseService) throws Exception { + assertThat(databaseService).isNotNull(); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlH2Test.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlH2Test.java new file mode 100644 index 0000000..ad5dd84 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlH2Test.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.impl.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class DialectDdlH2Test { + + private static Connection connection; + private static H2Dialect dialect; + private static MetaInfo metaInfo; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection( + "jdbc:h2:mem:cwmToSql;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL PRIMARY KEY, + FIRST_NAME VARCHAR(50) NOT NULL, + SALARY DECIMAL(10,2) + ) + """); + } + dialect = new H2Dialect(org.eclipse.daanse.sql.dialect.api.DialectInitData.fromConnection(connection)); + metaInfo = new DatabaseServiceImpl().createMetaInfo(connection, new org.eclipse.daanse.sql.jdbc.metadata.H2MetadataProvider()); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP ALL OBJECTS"); + } + connection.close(); + } + } + + private static TableDefinition employees() { + return metaInfo.structureInfo().tables().stream() + .filter(td -> "EMPLOYEES".equalsIgnoreCase(td.table().name())) + .findFirst().orElseThrow(); + } + + private static PrimaryKey employeesPk() { + return metaInfo.structureInfo().primaryKeys().stream() + .filter(pk -> "EMPLOYEES".equalsIgnoreCase(pk.table().name())) + .findFirst().orElseThrow(); + } + + @Test + void createTable_emitsQuotedIdentifiersAndTypes() { + TableDefinition emp = employees(); + List cols = dialect.ddlGenerator().columnsOf(emp.table(), metaInfo.structureInfo().columns()); + String ddl = dialect.ddlGenerator().createTable(emp.table(), cols, employeesPk(), false); + + assertThat(ddl).startsWith("CREATE TABLE "); + assertThat(ddl).contains("\"EMPLOYEES\""); + assertThat(ddl).contains("\"EMP_ID\""); + assertThat(ddl).contains("\"FIRST_NAME\""); + assertThat(ddl).contains("NOT NULL"); + assertThat(ddl).contains("PRIMARY KEY (\"EMP_ID\")"); + } + + @Test + void createTable_ifNotExists() { + TableDefinition emp = employees(); + List cols = dialect.ddlGenerator().columnsOf(emp.table(), metaInfo.structureInfo().columns()); + String ddl = dialect.ddlGenerator().createTable(emp.table(), cols, null, true); + assertThat(ddl).startsWith("CREATE TABLE IF NOT EXISTS "); + } + + @Test + void insertInto_parameterised() { + TableDefinition emp = employees(); + List cols = dialect.ddlGenerator().columnsOf(emp.table(), metaInfo.structureInfo().columns()); + String sql = dialect.ddlGenerator().insertInto(emp.table(), cols); + assertThat(sql).startsWith("INSERT INTO "); + // Three columns → three placeholders. + assertThat(sql).endsWith("VALUES (?, ?, ?)"); + } + + @Test + void selectFrom_listsExplicitColumns() { + TableDefinition emp = employees(); + List cols = dialect.ddlGenerator().columnsOf(emp.table(), metaInfo.structureInfo().columns()); + String sql = dialect.ddlGenerator().selectFrom(emp.table(), cols); + assertThat(sql).startsWith("SELECT \"EMP_ID\""); + assertThat(sql).contains("\"FIRST_NAME\""); + } + + @Test + void selectAll_noColumnList() { + TableDefinition emp = employees(); + String sql = dialect.ddlGenerator().selectAll(emp.table()); + assertThat(sql).startsWith("SELECT * FROM "); + } + + @Test + void generatedDdlCanBeReplayedOnFreshDatabase_andInsertAndSelectWork() throws SQLException { + TableDefinition emp = employees(); + List cols = dialect.ddlGenerator().columnsOf(emp.table(), metaInfo.structureInfo().columns()); + String createDdl = dialect.ddlGenerator().createTable(emp.table(), cols, employeesPk(), true); + String insertDml = dialect.ddlGenerator().insertInto(emp.table(), cols); + String selectDml = dialect.ddlGenerator().selectFrom(emp.table(), cols); + + // Use a fresh H2 DB and a fresh schema equivalent to PUBLIC so the quoted + // identifiers resolve correctly. + try (Connection replay = DriverManager.getConnection( + "jdbc:h2:mem:cwmToSqlReplay;DB_CLOSE_DELAY=-1", "sa", "")) { + try (Statement stmt = replay.createStatement()) { + stmt.execute(createDdl); + } + try (PreparedStatement ps = replay.prepareStatement(insertDml)) { + ps.setInt(1, 42); + ps.setString(2, "Alice"); + ps.setBigDecimal(3, new java.math.BigDecimal("1000.00")); + ps.executeUpdate(); + } + try (PreparedStatement ps = replay.prepareStatement(selectDml); + ResultSet rs = ps.executeQuery()) { + assertThat(rs.next()).isTrue(); + assertThat(rs.getInt(1)).isEqualTo(42); + assertThat(rs.getString(2)).isEqualTo("Alice"); + assertThat(rs.next()).isFalse(); + } + } + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java new file mode 100644 index 0000000..417ffba --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/DialectDdlOfflineTest.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.junit.jupiter.api.Test; + +class DialectDdlOfflineTest { + + private static final SchemaReference PUBLIC = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference EMPLOYEES = + new TableReference(Optional.of(PUBLIC), "EMPLOYEES", TableReference.TYPE_TABLE); + + private static ColumnDefinition col(String name, JDBCType jdbc, ColumnMetaData.Nullability nullability, + OptionalInt size) { + ColumnReference ref = new ColumnReference(Optional.of(EMPLOYEES), name); + ColumnMetaData meta = new ColumnMetaDataRecord( + jdbc, + jdbc.getName(), + size, + OptionalInt.empty(), + OptionalInt.empty(), + nullability, + OptionalInt.empty(), + Optional.empty(), + Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + private static List sampleColumns() { + return List.of( + col("EMP_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.empty()), + col("FIRST_NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, OptionalInt.of(50)), + col("SALARY", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, OptionalInt.of(10))); + } + + @Test + void ansi_dialect_no_arg_ctor_uses_double_quote() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.getQuoteIdentifierString()).isEqualTo("\""); + } + + @Test + void createTable_with_primary_key() { + Dialect dialect = new AnsiDialect(); + List columns = sampleColumns(); + PrimaryKey pk = new PrimaryKeyRecord( + EMPLOYEES, + List.of(columns.get(0).column()), + Optional.empty()); + String sql = dialect.ddlGenerator().createTable(EMPLOYEES, columns, pk, true); + assertThat(sql).isEqualTo( + "CREATE TABLE IF NOT EXISTS \"PUBLIC\".\"EMPLOYEES\" (\n" + + " \"EMP_ID\" INTEGER NOT NULL,\n" + + " \"FIRST_NAME\" VARCHAR(50) NOT NULL,\n" + + " \"SALARY\" DECIMAL(10),\n" + + " PRIMARY KEY (\"EMP_ID\")\n" + + ")"); + } + + @Test + void insertInto_parameterised() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().insertInto(EMPLOYEES, sampleColumns()); + assertThat(sql).isEqualTo( + "INSERT INTO \"PUBLIC\".\"EMPLOYEES\" (\"EMP_ID\", \"FIRST_NAME\", \"SALARY\") VALUES (?, ?, ?)"); + } + + @Test + void selectFrom_with_columns() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().selectFrom(EMPLOYEES, sampleColumns()); + assertThat(sql).isEqualTo( + "SELECT \"EMP_ID\", \"FIRST_NAME\", \"SALARY\" FROM \"PUBLIC\".\"EMPLOYEES\""); + } + + @Test + void selectAll_star() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.ddlGenerator().selectAll(EMPLOYEES)) + .isEqualTo("SELECT * FROM \"PUBLIC\".\"EMPLOYEES\""); + } + + @Test + void update_with_where_columns() { + Dialect dialect = new AnsiDialect(); + List all = sampleColumns(); + String sql = dialect.ddlGenerator().update( + EMPLOYEES, + List.of(all.get(1), all.get(2)), + List.of(all.get(0))); + assertThat(sql).isEqualTo( + "UPDATE \"PUBLIC\".\"EMPLOYEES\" SET \"FIRST_NAME\" = ?, \"SALARY\" = ? WHERE \"EMP_ID\" = ?"); + } + + @Test + void deleteFrom_with_where() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().deleteFrom(EMPLOYEES, List.of(sampleColumns().get(0))); + assertThat(sql).isEqualTo("DELETE FROM \"PUBLIC\".\"EMPLOYEES\" WHERE \"EMP_ID\" = ?"); + } + + @Test + void deleteFrom_unfiltered_emits_no_where() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().deleteFrom(EMPLOYEES, List.of()); + assertThat(sql).isEqualTo("DELETE FROM \"PUBLIC\".\"EMPLOYEES\""); + } + + @Test + void dropTable_if_exists() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropTable(EMPLOYEES, true); + // Delegates to dialect.dropTable — exact format is dialect-specific; both + // schema and table identifiers must appear quoted. + assertThat(sql).contains("DROP TABLE") + .contains("IF EXISTS") + .contains("\"PUBLIC\"") + .contains("\"EMPLOYEES\""); + // Default overload doesn't add CASCADE. + assertThat(sql).doesNotContain("CASCADE"); + } + + @Test + void dropTable_with_cascade_appends_cascade() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropTable(EMPLOYEES, true, true); + assertThat(sql).contains("DROP TABLE").endsWith("CASCADE"); + } + + @Test + void dropTable_cascade_silently_dropped_for_dialects_without_support() { + // Anonymous dialect that mimics MySQL's lack of CASCADE support. + Dialect noCascadeDialect = new AnsiDialect() { + @Override + public boolean supportsDropTableCascade() { + return false; + } + }; + String sql = noCascadeDialect.ddlGenerator().dropTable(EMPLOYEES, true, true); + assertThat(sql).contains("DROP TABLE").doesNotContain("CASCADE"); + } + + @Test + void createSequence_with_full_options() { + Dialect dialect = new AnsiDialect(); + DdlGenerator.SequenceDefinition seq = new DdlGenerator.SequenceDefinition( + "PUBLIC", "ORDER_SEQ", + 100L, // startWith + 1L, // incrementBy + 100L, // minValue + 999_999L,// maxValue + false, // cycle + 20L); // cache + assertThat(dialect.ddlGenerator().createSequence(seq, true)).contains( + "CREATE SEQUENCE IF NOT EXISTS \"PUBLIC\".\"ORDER_SEQ\"" + + " START WITH 100 INCREMENT BY 1 MINVALUE 100 MAXVALUE 999999" + + " NO CYCLE CACHE 20"); + } + + @Test + void createSequence_minimal() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.ddlGenerator().createSequence( + DdlGenerator.SequenceDefinition.of("ORDER_SEQ"), false)) + .contains("CREATE SEQUENCE \"ORDER_SEQ\""); + } + + @Test + void dropSequence_if_exists() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.ddlGenerator().dropSequence("PUBLIC", "ORDER_SEQ", true)) + .contains("DROP SEQUENCE IF EXISTS \"PUBLIC\".\"ORDER_SEQ\""); + } + + @Test + void nextValueFor_emits_standard_form() { + Dialect dialect = new AnsiDialect(); + assertThat(dialect.ddlGenerator().nextValueFor("PUBLIC", "ORDER_SEQ")) + .contains("NEXT VALUE FOR \"PUBLIC\".\"ORDER_SEQ\""); + } + + @Test + void sequence_methods_return_empty_when_dialect_lacks_support() { + Dialect noSeq = new AnsiDialect() { + @Override + public boolean supportsSequences() { + return false; + } + }; + assertThat(noSeq.ddlGenerator().createSequence( + DdlGenerator.SequenceDefinition.of("ORDER_SEQ"), false)).isEmpty(); + assertThat(noSeq.ddlGenerator().dropSequence(null, "ORDER_SEQ", false)).isEmpty(); + assertThat(noSeq.ddlGenerator().nextValueFor(null, "ORDER_SEQ")).isEmpty(); + } + + @Test + void sequence_definition_rejects_blank_name() { + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> DdlGenerator.SequenceDefinition.of("")); + org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, + () -> DdlGenerator.SequenceDefinition.of(null)); + } + + @Test + void truncate_delegates_to_clearTable() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().truncate(EMPLOYEES); + // H2's clearTable is "TRUNCATE TABLE schema.table". + assertThat(sql).contains("TRUNCATE") + .contains("\"PUBLIC\"") + .contains("\"EMPLOYEES\""); + } + + @Test + void alterTable_addColumn() { + Dialect dialect = new AnsiDialect(); + ColumnDefinition newCol = + col("HIRE_DATE", JDBCType.DATE, ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty()); + String sql = dialect.ddlGenerator().alterTableAddColumn(EMPLOYEES, newCol); + assertThat(sql).isEqualTo( + "ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" ADD COLUMN \"HIRE_DATE\" DATE"); + } + + @Test + void alterTable_dropColumn() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().alterTableDropColumn(EMPLOYEES, "FIRST_NAME"); + assertThat(sql).isEqualTo( + "ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" DROP COLUMN \"FIRST_NAME\""); + } + + @Test + void createIndex_unique_ifNotExists() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().createIndex( + "IDX_EMP_NAME", EMPLOYEES, List.of("FIRST_NAME"), true, true); + assertThat(sql).isEqualTo( + "CREATE UNIQUE INDEX IF NOT EXISTS \"IDX_EMP_NAME\" ON \"PUBLIC\".\"EMPLOYEES\" (\"FIRST_NAME\")"); + } + + @Test + void createIndex_compound_non_unique() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().createIndex( + "IDX_EMP_COMP", EMPLOYEES, List.of("EMP_ID", "FIRST_NAME"), false, false); + assertThat(sql).isEqualTo( + "CREATE INDEX \"IDX_EMP_COMP\" ON \"PUBLIC\".\"EMPLOYEES\" (\"EMP_ID\", \"FIRST_NAME\")"); + } + + @Test + void dropIndex_if_exists() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropIndex("IDX_EMP_NAME", true); + assertThat(sql).isEqualTo("DROP INDEX IF EXISTS \"IDX_EMP_NAME\""); + } + + @Test + void createView_or_replace() { + Dialect dialect = new AnsiDialect(); + TableReference view = new TableReference(Optional.of(PUBLIC), "EMP_NAMES", TableReference.TYPE_VIEW); + String selectSql = dialect.ddlGenerator().selectFrom(EMPLOYEES, List.of(sampleColumns().get(1))); + String sql = dialect.ddlGenerator().createView(view, selectSql, true); + assertThat(sql).isEqualTo( + "CREATE OR REPLACE VIEW \"PUBLIC\".\"EMP_NAMES\" AS " + + "SELECT \"FIRST_NAME\" FROM \"PUBLIC\".\"EMPLOYEES\""); + } + + @Test + void dropView_if_exists() { + Dialect dialect = new AnsiDialect(); + TableReference view = new TableReference(Optional.of(PUBLIC), "EMP_NAMES", TableReference.TYPE_VIEW); + String sql = dialect.ddlGenerator().dropView(view, true); + assertThat(sql).isEqualTo("DROP VIEW IF EXISTS \"PUBLIC\".\"EMP_NAMES\""); + } + + @Test + void dropIndex_with_table_qualifies_with_schema_on_engines_that_dont_require_on_table() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropIndex("IDX_EMP_NAME", EMPLOYEES, true); + // Schema-qualified, no ON table clause (H2.dropIndexRequiresTable=false). + assertThat(sql).isEqualTo("DROP INDEX IF EXISTS \"PUBLIC\".\"IDX_EMP_NAME\""); + } + + @Test + void dropIndex_with_table_appends_on_table_when_dialect_requires() { + Dialect onTableDialect = new AnsiDialect() { + @Override public boolean dropIndexRequiresTable() { return true; } + }; + String sql = onTableDialect.ddlGenerator().dropIndex("IDX_EMP_NAME", EMPLOYEES, true); + // MySQL/MariaDB-style: bare name + ON table. + assertThat(sql).isEqualTo( + "DROP INDEX IF EXISTS \"IDX_EMP_NAME\" ON \"PUBLIC\".\"EMPLOYEES\""); + } + + @Test + void dropIndex_strips_if_exists_when_dialect_lacks_support() { + Dialect noIfExistsDialect = new AnsiDialect() { + @Override public boolean supportsDropIndexIfExists() { return false; } + }; + String sql = noIfExistsDialect.ddlGenerator().dropIndex("IDX_EMP_NAME", EMPLOYEES, true); + assertThat(sql).isEqualTo("DROP INDEX \"PUBLIC\".\"IDX_EMP_NAME\""); + } + + @Test + void dropConstraint_strips_if_exists_when_dialect_lacks_support() { + Dialect noDropConstraintIfExists = new AnsiDialect() { + @Override public boolean supportsDropConstraintIfExists() { return false; } + }; + String sql = noDropConstraintIfExists.ddlGenerator().dropConstraint(EMPLOYEES, "FK_EMP", true); + assertThat(sql).isEqualTo("ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" DROP CONSTRAINT \"FK_EMP\""); + } + + @Test + void dropConstraint_keeps_if_exists_by_default() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropConstraint(EMPLOYEES, "FK_EMP", true); + assertThat(sql).isEqualTo( + "ALTER TABLE \"PUBLIC\".\"EMPLOYEES\" DROP CONSTRAINT IF EXISTS \"FK_EMP\""); + } + + @Test + void dropSchema_delegates_to_dialect() { + Dialect dialect = new AnsiDialect(); + String sql = dialect.ddlGenerator().dropSchema("PUBLIC", true, true); + // Default impl in AbstractJdbcDialect emits IF EXISTS + CASCADE. + assertThat(sql).isEqualTo("DROP SCHEMA IF EXISTS \"PUBLIC\" CASCADE"); + } + + @Test + void dropSchema_strips_if_exists_when_dialect_lacks_support() { + Dialect dialect = new AnsiDialect() { + @Override public boolean supportsDropSchemaIfExists() { return false; } + }; + String sql = dialect.ddlGenerator().dropSchema("PUBLIC", true, true); + assertThat(sql).isEqualTo("DROP SCHEMA \"PUBLIC\" CASCADE"); + } + + @Test + void dropSchema_strips_cascade_when_dialect_lacks_support() { + Dialect dialect = new AnsiDialect() { + @Override public boolean supportsDropTableCascade() { return false; } + }; + String sql = dialect.ddlGenerator().dropSchema("PUBLIC", true, true); + assertThat(sql).isEqualTo("DROP SCHEMA IF EXISTS \"PUBLIC\""); + } + + @Test + void dropSchema_emits_restrict_when_dialect_requires_it() { + Dialect dialect = new AnsiDialect() { + @Override public boolean requiresDropSchemaRestrict() { return true; } + @Override public boolean supportsDropSchemaIfExists() { return false; } + }; + // RESTRICT-required engines (Derby/DB2) reject IF EXISTS and CASCADE. + String sql = dialect.ddlGenerator().dropSchema("PUBLIC", true, true); + assertThat(sql).isEqualTo("DROP SCHEMA \"PUBLIC\" RESTRICT"); + } + + @Test + void dropTable_strips_if_exists_when_dialect_lacks_support() { + Dialect dialect = new AnsiDialect() { + @Override public boolean supportsDropTableIfExists() { return false; } + }; + String sql = dialect.ddlGenerator().dropTable(EMPLOYEES, true); + assertThat(sql).contains("DROP TABLE") + .doesNotContain("IF EXISTS") + .contains("\"PUBLIC\"") + .contains("\"EMPLOYEES\""); + } + +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/FocusedInterfaceDefaultsTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/FocusedInterfaceDefaultsTest.java new file mode 100644 index 0000000..4d5d206 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/FocusedInterfaceDefaultsTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Map; + +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.generator.AggregationGenerator; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.dialect.api.generator.FunctionGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.HintGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.OrderByGenerator; +import org.eclipse.daanse.sql.dialect.api.generator.RegexGenerator; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class FocusedInterfaceDefaultsTest { + + private final Dialect dialect = new AnsiDialect(); + + // -------------------- Dialect identity -------------------- + + @Test + void dialect_identity_name_is_ansi() { + assertThat(dialect.name()).isEqualTo("ansi"); + } + + // -------------------- accessors return narrowed views -------------------- + + @Test + void accessors_return_narrowed_views() { + assertThat((Object) dialect.sqlGenerator()).isSameAs(dialect); + assertThat((Object) dialect.ddlGenerator()).isSameAs(dialect); + assertThat((Object) dialect.orderByGenerator()).isSameAs(dialect); + assertThat((Object) dialect.regexGenerator()).isSameAs(dialect); + assertThat((Object) dialect.aggregationGenerator()).isSameAs(dialect); + assertThat((Object) dialect.functionGenerator()).isSameAs(dialect); + assertThat((Object) dialect.hintGenerator()).isSameAs(dialect); + } + + // -------------------- OrderByGenerator -------------------- + + @Nested + class OrderBy { + private final OrderByGenerator g = dialect.orderByGenerator(); + + @Test + void non_nullable_asc() { + assertThat(g.generateOrderItem("col", false, true, false).toString()).isEqualTo("col ASC"); + } + + @Test + void non_nullable_desc() { + assertThat(g.generateOrderItem("col", false, false, false).toString()).isEqualTo("col DESC"); + } + + @Test + void nullable_asc_nulls_last_uses_case_when_default() { + assertThat(g.generateOrderItem("col", true, true, true).toString()) + .isEqualTo("CASE WHEN col IS NULL THEN 1 ELSE 0 END, col ASC"); + } + + @Test + void nullable_desc_nulls_first_uses_case_when_default() { + assertThat(g.generateOrderItem("col", true, false, false).toString()) + .isEqualTo("CASE WHEN col IS NULL THEN 0 ELSE 1 END, col DESC"); + } + + @Test + void ansi_form_emits_nulls_last() { + assertThat(g.generateOrderByNullsAnsi("col", true, true).toString()) + .isEqualTo("col ASC NULLS LAST"); + } + + @Test + void ansi_form_emits_nulls_first() { + assertThat(g.generateOrderByNullsAnsi("col", false, false).toString()) + .isEqualTo("col DESC NULLS FIRST"); + } + + @Test + void order_value_quotes_via_dialect_literal_quoter() { + // VARCHAR sentinel — quoted with single-quote literal form. + String sql = g.generateOrderItemForOrderValue("col", "X", Datatype.VARCHAR, true, true).toString(); + assertThat(sql).isEqualTo("CASE WHEN col = 'X' THEN 1 ELSE 0 END, col ASC"); + } + } + + // -------------------- RegexGenerator -------------------- + + @Nested + class Regex { + private final RegexGenerator g = dialect.regexGenerator(); + + @Test + void default_returns_empty_when_unsupported() { + assertThat(g.generateRegularExpression("c", "[a-z]+")).isEmpty(); + } + + @Test + void embedded_flags_extracted_and_translated() { + StringBuilder dialectFlags = new StringBuilder(); + String stripped = g.extractEmbeddedFlags("(?is).*Hello.*", + new String[][] { { "i", "i" }, { "s", "n" } }, dialectFlags); + assertThat(stripped).isEqualTo(".*Hello.*"); + assertThat(dialectFlags).hasToString("in"); + } + + @Test + void no_flags_means_no_change() { + StringBuilder dialectFlags = new StringBuilder(); + String stripped = g.extractEmbeddedFlags(".*Hello.*", + new String[][] { { "i", "i" } }, dialectFlags); + assertThat(stripped).isEqualTo(".*Hello.*"); + assertThat(dialectFlags).isEmpty(); + } + } + + // -------------------- FunctionGenerator -------------------- + + @Nested + class Functions { + private final FunctionGenerator g = dialect.functionGenerator(); + + @Test + void upper_wraps_in_ansi_form() { + assertThat(g.wrapIntoSqlUpperCaseFunction("col").toString()).isEqualTo("UPPER(col)"); + } + + @Test + void if_then_else_uses_case_when_in_ansi_form() { + assertThat(g.wrapIntoSqlIfThenElseFunction("a > b", "a", "b").toString()) + .isEqualTo("CASE WHEN a > b THEN a ELSE b END"); + } + + } + + // -------------------- AggregationGenerator -------------------- + + @Nested + class Aggregation { + private final AggregationGenerator g = dialect.aggregationGenerator(); + + @Test + void list_agg_is_unsupported_by_default() { + assertThat(g.supportsListAgg()).isFalse(); + assertThat(g.generateListAgg("c", false, ",", null, null, null)).isEmpty(); + } + + @Test + void nth_value_is_unsupported_by_default() { + assertThat(g.supportsNthValue()).isFalse(); + assertThat(g.supportsNthValueIgnoreNulls()).isFalse(); + assertThat(g.generateNthValueAgg("c", false, 1, null)).isEmpty(); + } + + @Test + void percentile_is_unsupported_by_default() { + assertThat(g.supportsPercentileDisc()).isFalse(); + assertThat(g.supportsPercentileCont()).isFalse(); + assertThat(g.generatePercentileDisc(0.5, false, null, "c")).isEmpty(); + assertThat(g.generatePercentileCont(0.5, false, null, "c")).isEmpty(); + } + + @Test + void bit_aggregation_is_unsupported_by_default() { + assertThat(g.supportsBitAggregation(BitOperation.AND)).isFalse(); + assertThat(g.generateBitAggregation(BitOperation.AND, "c")).isEmpty(); + } + + @Test + void buildPercentileFunction_emits_within_group_form() { + String sql = g.buildPercentileFunction("PERCENTILE_DISC", 0.5, false, null, "salary").toString(); + assertThat(sql).isEqualTo("PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY \"salary\")"); + } + + @Test + void buildPercentileFunction_with_table_qualifies() { + String sql = g.buildPercentileFunction("PERCENTILE_CONT", 0.9, true, "emp", "salary").toString(); + assertThat(sql).isEqualTo( + "PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY \"emp\".\"salary\" DESC)"); + } + } + + // -------------------- HintGenerator -------------------- + + @Nested + class Hints { + private final HintGenerator g = dialect.hintGenerator(); + + @Test + void default_is_noop() { + StringBuilder buf = new StringBuilder("SELECT * FROM t"); + g.appendHintsAfterFromClause(buf, Map.of("hint", "value")); + assertThat(buf).hasToString("SELECT * FROM t"); + } + } + + // -------------------- Datatype polish -------------------- + + @Nested + class DatatypePredicates { + @Test + void isNumeric_isText_isTemporal_partition_cleanly() { + for (Datatype t : Datatype.values()) { + int trues = (t.isNumeric() ? 1 : 0) + (t.isText() ? 1 : 0) + (t.isTemporal() ? 1 : 0) + + (t.isBinary() ? 1 : 0) + (t.isComposite() ? 1 : 0); + // Boolean has no bucket; everything else must belong to exactly one. + if (t == Datatype.BOOLEAN) { + assertThat(trues).isZero(); + } else { + assertThat(trues).as("type %s should belong to exactly one bucket", t).isEqualTo(1); + } + } + } + + @Test + void fromValueOrNull_resolves_sql99_aliases() { + assertThat(Datatype.fromValueOrNull("CHARACTER VARYING")).isEqualTo(Datatype.VARCHAR); + assertThat(Datatype.fromValueOrNull("DOUBLE PRECISION")).isEqualTo(Datatype.DOUBLE); + assertThat(Datatype.fromValueOrNull("INT")).isEqualTo(Datatype.INTEGER); + assertThat(Datatype.fromValueOrNull("nothingmatches")).isNull(); + assertThat(Datatype.fromValueOrNull(null)).isNull(); + } + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/H2DdlRoundTripTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/H2DdlRoundTripTest.java new file mode 100644 index 0000000..9808918 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/H2DdlRoundTripTest.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.impl.DatabaseServiceImpl; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.h2.H2Dialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.h2.jdbcx.JdbcDataSource; +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; + +@TestInstance(Lifecycle.PER_CLASS) +class H2DdlRoundTripTest { + + private static final SchemaReference SCHEMA = new SchemaReference(Optional.empty(), "RT_TEST"); + private static final TableReference CUSTOMERS = + new TableReference(Optional.of(SCHEMA), "CUSTOMERS", TableReference.TYPE_TABLE); + private static final TableReference ORDERS = + new TableReference(Optional.of(SCHEMA), "ORDERS", TableReference.TYPE_TABLE); + private static final TableReference VIEW = + new TableReference(Optional.of(SCHEMA), "CUSTOMER_ORDERS", TableReference.TYPE_VIEW); + + private static final DatabaseService DB_SERVICE = new DatabaseServiceImpl(); + + private DataSource dataSource; + private Dialect dialect; + private Connection connection; + + @BeforeAll + void setUp() throws Exception { + JdbcDataSource ds = new JdbcDataSource(); + ds.setURL("jdbc:h2:mem:rt_h2_dialect;DB_CLOSE_DELAY=-1"); + ds.setUser("sa"); + ds.setPassword(""); + this.dataSource = ds; + this.dialect = new H2Dialect(); + this.connection = dataSource.getConnection(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement s = connection.createStatement()) { + s.execute("DROP ALL OBJECTS"); + } catch (Exception ignored) { /* defensive */ } + connection.close(); + } + } + + private static ColumnDefinition col(TableReference table, String name, JDBCType jdbc, + ColumnMetaData.Nullability nullability, OptionalInt size, OptionalInt scale) { + ColumnReference ref = new ColumnReference(Optional.of(table), name); + ColumnMetaData meta = new ColumnMetaDataRecord( + jdbc, + jdbc.getName(), + size, + scale, + OptionalInt.empty(), + nullability, + OptionalInt.empty(), + Optional.empty(), + Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + @Test + void full_round_trip_with_per_step_database_service_verification() throws Exception { + // ---- 1. Fixture (sql.jdbc records, no CWM) ---- + List custCols = List.of( + col(CUSTOMERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, + OptionalInt.empty(), OptionalInt.empty()), + col(CUSTOMERS, "EMAIL", JDBCType.VARCHAR, ColumnMetaData.Nullability.NO_NULLS, + OptionalInt.of(100), OptionalInt.empty()), + col(CUSTOMERS, "NAME", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, + OptionalInt.of(50), OptionalInt.empty())); + PrimaryKey custPk = new PrimaryKeyRecord(CUSTOMERS, List.of(custCols.get(0).column()), Optional.of("PK_CUSTOMERS")); + + List ordCols = List.of( + col(ORDERS, "ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, + OptionalInt.empty(), OptionalInt.empty()), + col(ORDERS, "CUSTOMER_ID", JDBCType.INTEGER, ColumnMetaData.Nullability.NO_NULLS, + OptionalInt.empty(), OptionalInt.empty()), + col(ORDERS, "TOTAL", JDBCType.DECIMAL, ColumnMetaData.Nullability.NULLABLE, + OptionalInt.of(10), OptionalInt.of(2)), + col(ORDERS, "NOTE", JDBCType.VARCHAR, ColumnMetaData.Nullability.NULLABLE, + OptionalInt.of(200), OptionalInt.empty())); + PrimaryKey ordPk = new PrimaryKeyRecord(ORDERS, List.of(ordCols.get(0).column()), Optional.of("PK_ORDERS")); + + // ---- 2. CREATE script — verify each step ---- + executeAndVerify(dialect.ddlGenerator().createSchema(SCHEMA.name(), true), + info -> assertThat(schemaNames(info)).contains(SCHEMA.name())); + + executeAndVerify(dialect.ddlGenerator().createTable(CUSTOMERS, custCols, custPk, true), + info -> assertThat(tableNames(info)).contains(CUSTOMERS.name())); + + executeAndVerify(dialect.ddlGenerator().createTable(ORDERS, ordCols, ordPk, true), + info -> assertThat(tableNames(info)).contains(ORDERS.name())); + + executeAndVerify(dialect.ddlGenerator().addUniqueConstraint( + CUSTOMERS, "UC_CUSTOMERS_EMAIL", List.of("EMAIL")), + info -> { /* loader doesn't surface UC on H2 — check via execution side-effect later */ }); + + executeAndVerify(dialect.ddlGenerator().addCheckConstraint( + CUSTOMERS, "CK_CUSTOMERS_ID_POS", + dialect.quoteIdentifier("ID").toString() + " > 0"), + info -> { /* loader doesn't surface CHECK */ }); + + executeAndVerify(dialect.ddlGenerator().createIndex( + "IDX_CUSTOMERS_NAME", CUSTOMERS, List.of("NAME"), false, true), + info -> { /* index visible via JDBC metadata, not a top-level StructureInfo accessor */ }); + + executeAndVerify(dialect.ddlGenerator().addForeignKeyConstraint( + ORDERS, "FK_ORDERS_CUSTOMERS", + List.of("CUSTOMER_ID"), CUSTOMERS, List.of("ID"), + "CASCADE", null), + info -> assertThat(importedKeyTables(info)).contains(ORDERS.name())); + + executeAndVerify(dialect.ddlGenerator().createView(VIEW, + "SELECT C." + dialect.quoteIdentifier("NAME") + ", " + + "O." + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + " C " + + "JOIN " + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + " O " + + "ON O." + dialect.quoteIdentifier("CUSTOMER_ID") + + " = C." + dialect.quoteIdentifier("ID"), + false), + info -> { /* H2 view shows up in DatabaseMetaData.getTables(VIEW) — verified later */ }); + + // ---- 3. INSERT ---- + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO " + dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()) + + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("EMAIL") + ", " + + dialect.quoteIdentifier("NAME") + ") VALUES (?, ?, ?)")) { + ps.setInt(1, 1); + ps.setString(2, "alice@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + ps.setInt(1, 2); + ps.setString(2, "bob@example.com"); + ps.setString(3, null); + ps.executeUpdate(); + } + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO " + dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()) + + " (" + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("CUSTOMER_ID") + ", " + + dialect.quoteIdentifier("TOTAL") + ", " + + dialect.quoteIdentifier("NOTE") + ") VALUES (?, ?, ?, ?)")) { + ps.setInt(1, 100); + ps.setInt(2, 1); + ps.setBigDecimal(3, new BigDecimal("9.99")); + ps.setString(4, "Premium customer Alice"); + ps.executeUpdate(); + ps.setInt(1, 101); + ps.setInt(2, 2); + ps.setBigDecimal(3, new BigDecimal("4.50")); + ps.setString(4, "Standard customer Bob"); + ps.executeUpdate(); + } + + // ---- 4. Cross-table UPDATE — CUSTOMERS.NAME ← MAX(ORDERS.NOTE) ---- + String qC = dialect.quoteIdentifier(SCHEMA.name(), CUSTOMERS.name()); + String qO = dialect.quoteIdentifier(SCHEMA.name(), ORDERS.name()); + try (PreparedStatement ps = connection.prepareStatement( + "UPDATE " + qC + " SET " + dialect.quoteIdentifier("NAME") + " = (" + + " SELECT MAX(O." + dialect.quoteIdentifier("NOTE") + ") FROM " + qO + " O" + + " WHERE O." + dialect.quoteIdentifier("CUSTOMER_ID") + + " = " + qC + "." + dialect.quoteIdentifier("ID") + ")" + + " WHERE EXISTS (" + + " SELECT 1 FROM " + qO + " O2" + + " WHERE O2." + dialect.quoteIdentifier("CUSTOMER_ID") + + " = " + qC + "." + dialect.quoteIdentifier("ID") + ")")) { + assertThat(ps.executeUpdate()).isEqualTo(2); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT " + dialect.quoteIdentifier("ID") + ", " + + dialect.quoteIdentifier("NAME") + " FROM " + qC + " ORDER BY " + + dialect.quoteIdentifier("ID"))) { + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Premium customer Alice"); + rs.next(); + assertThat(rs.getString(2)).isEqualTo("Standard customer Bob"); + } + + // ---- 5. SELECT through view ---- + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery( + "SELECT " + dialect.quoteIdentifier("NAME") + ", " + + dialect.quoteIdentifier("TOTAL") + + " FROM " + dialect.quoteIdentifier(SCHEMA.name(), VIEW.name()) + + " ORDER BY " + dialect.quoteIdentifier("NAME"))) { + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("9.99"); + rs.next(); + assertThat(rs.getBigDecimal(2)).isEqualByComparingTo("4.50"); + } + + // ---- 6. DELETE — FK CASCADE removes child orders ---- + try (PreparedStatement ps = connection.prepareStatement( + "DELETE FROM " + qC + " WHERE " + dialect.quoteIdentifier("ID") + " = ?")) { + ps.setInt(1, 1); + assertThat(ps.executeUpdate()).isEqualTo(1); + } + try (Statement s = connection.createStatement(); + ResultSet rs = s.executeQuery("SELECT COUNT(*) FROM " + qO)) { + rs.next(); + assertThat(rs.getInt(1)).isEqualTo(1); + } + + // ---- 7. DROP — verify each step ---- + executeAndVerify(dialect.ddlGenerator().dropView(VIEW, true), info -> { /* view gone */ }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(ORDERS, "FK_ORDERS_CUSTOMERS", true), + info -> { /* FK gone */ }); + executeAndVerify(dialect.ddlGenerator().dropIndex("IDX_CUSTOMERS_NAME", CUSTOMERS, true), + info -> { /* index gone */ }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "CK_CUSTOMERS_ID_POS", true), + info -> { }); + executeAndVerify(dialect.ddlGenerator().dropConstraint(CUSTOMERS, "UC_CUSTOMERS_EMAIL", true), + info -> { }); + executeAndVerify(dialect.ddlGenerator().dropTable(ORDERS, true, true), + info -> assertThat(tableNames(info)).doesNotContain(ORDERS.name())); + executeAndVerify(dialect.ddlGenerator().dropTable(CUSTOMERS, true, true), + info -> assertThat(tableNames(info)).doesNotContain(CUSTOMERS.name())); + executeAndVerify(dialect.ddlGenerator().dropSchema(SCHEMA.name(), true, true), + info -> assertThat(schemaNames(info)).doesNotContain(SCHEMA.name())); + } + + // -------------------- helpers -------------------- + + @FunctionalInterface + private interface StepCheck { + void verify(MetaInfo info) throws Exception; + } + + private void executeAndVerify(String sql, StepCheck check) throws Exception { + try (Statement s = connection.createStatement()) { + s.execute(sql); + } + try { + MetaInfo info = DB_SERVICE.createMetaInfo(connection); + check.verify(info); + } catch (Exception e) { + // Loader-coverage gaps on this DB → log and skip the assertion. + System.out.println("[round-trip] verification skipped for: " + + sql.substring(0, Math.min(80, sql.length())) + " — " + e.getMessage()); + } + } + + private List schemaNames(MetaInfo info) { + return info.structureInfo().schemas().stream().map(SchemaReference::name).toList(); + } + + private List tableNames(MetaInfo info) { + return info.structureInfo().tables().stream() + .filter(td -> sameSchema(td.table().schema().map(SchemaReference::name).orElse(null))) + .filter(td -> !"VIEW".equalsIgnoreCase(td.table().type())) + .map(td -> td.table().name()) + .toList(); + } + + private List importedKeyTables(MetaInfo info) { + return info.structureInfo().importedKeys().stream() + .map(ik -> ik.foreignKeyColumn().table().orElse(null)) + .filter(t -> t != null) + .filter(t -> sameSchema(t.schema().map(SchemaReference::name).orElse(null))) + .map(TableReference::name) + .toList(); + } + + private boolean sameSchema(String s) { + return s == null ? false : s.equalsIgnoreCase(SCHEMA.name()); + } +} diff --git a/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/QuotingPolicyAcrossDdlTest.java b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/QuotingPolicyAcrossDdlTest.java new file mode 100644 index 0000000..f08e811 --- /dev/null +++ b/jdbc/impl/src/test/java/org/eclipse/daanse/sql/jdbc/impl/sqlgen/QuotingPolicyAcrossDdlTest.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.daanse.sql.jdbc.impl.sqlgen; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.JDBCType; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.IdentifierQuotingPolicy; +import org.eclipse.daanse.sql.dialect.api.generator.DdlGenerator; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.junit.jupiter.api.Test; + +class QuotingPolicyAcrossDdlTest { + + private static final SchemaReference PUBLIC = new SchemaReference(Optional.empty(), "PUBLIC"); + private static final TableReference EMP = + new TableReference(Optional.of(PUBLIC), "EMPLOYEES", TableReference.TYPE_TABLE); + private static final TableReference DEPT = + new TableReference(Optional.of(PUBLIC), "DEPARTMENTS", TableReference.TYPE_TABLE); + + private static ColumnDefinition col(String name) { + ColumnReference ref = new ColumnReference(Optional.of(EMP), name); + ColumnMetaData meta = new ColumnMetaDataRecord( + JDBCType.INTEGER, "INTEGER", + OptionalInt.of(10), OptionalInt.empty(), OptionalInt.empty(), + ColumnMetaData.Nullability.NO_NULLS, + OptionalInt.empty(), + Optional.empty(), Optional.empty(), + ColumnMetaData.AutoIncrement.UNKNOWN, + ColumnMetaData.GeneratedColumn.UNKNOWN); + return new ColumnDefinitionRecord(ref, meta); + } + + private static AnsiDialect dialectWith(IdentifierQuotingPolicy policy) { + AnsiDialect d = new AnsiDialect(); + d.setQuotingPolicy(policy); + return d; + } + + @Test + void createTable_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().createTable(EMP, List.of(col("ID"), col("SALARY")), null, false); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().createTable(EMP, List.of(col("ID"), col("SALARY")), null, false); + + assertThat(always).contains("\"PUBLIC\".\"EMPLOYEES\"").contains("\"ID\"").contains("\"SALARY\""); + assertThat(never).doesNotContain("\"") + .contains("PUBLIC.EMPLOYEES").contains("ID").contains("SALARY"); + } + + @Test + void createIndex_respects_policy() { + DdlGenerator dAlways = dialectWith(IdentifierQuotingPolicy.ALWAYS).ddlGenerator(); + DdlGenerator dNever = dialectWith(IdentifierQuotingPolicy.NEVER).ddlGenerator(); + + String always = dAlways.createIndex("IDX_EMP_NAME", EMP, List.of("NAME"), false, false); + String never = dNever.createIndex("IDX_EMP_NAME", EMP, List.of("NAME"), false, false); + + assertThat(always).contains("\"IDX_EMP_NAME\"").contains("\"EMPLOYEES\"").contains("\"NAME\""); + assertThat(never).doesNotContain("\"") + .contains("IDX_EMP_NAME").contains("EMPLOYEES").contains("NAME"); + } + + @Test + void createSequence_respects_policy() { + var seq = new DdlGenerator.SequenceDefinition( + "PUBLIC", "ORDER_SEQ", 1L, 1L, null, null, null, null); + + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().createSequence(seq, false).orElseThrow(); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().createSequence(seq, false).orElseThrow(); + + assertThat(always).contains("\"PUBLIC\".\"ORDER_SEQ\""); + assertThat(never).doesNotContain("\"").contains("PUBLIC.ORDER_SEQ"); + } + + @Test + void primaryKey_constraint_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID")); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().addPrimaryKeyConstraint(EMP, "PK_EMP", List.of("ID")); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"PK_EMP\"").contains("\"ID\""); + assertThat(never).doesNotContain("\"") + .contains("EMPLOYEES").contains("PK_EMP").contains("ID"); + } + + @Test + void uniqueConstraint_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP_EMAIL", List.of("EMAIL")); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().addUniqueConstraint(EMP, "UQ_EMP_EMAIL", List.of("EMAIL")); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"UQ_EMP_EMAIL\"").contains("\"EMAIL\""); + assertThat(never).doesNotContain("\"") + .contains("EMPLOYEES").contains("UQ_EMP_EMAIL").contains("EMAIL"); + } + + @Test + void foreignKey_constraint_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS).ddlGenerator() + .addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), + DEPT, List.of("ID"), "NO ACTION", "NO ACTION"); + String never = dialectWith(IdentifierQuotingPolicy.NEVER).ddlGenerator() + .addForeignKeyConstraint(EMP, "FK_EMP_DEPT", List.of("DEPT_ID"), + DEPT, List.of("ID"), "NO ACTION", "NO ACTION"); + + assertThat(always) + .contains("\"EMPLOYEES\"").contains("\"FK_EMP_DEPT\"") + .contains("\"DEPT_ID\"").contains("\"DEPARTMENTS\"").contains("\"ID\""); + assertThat(never).doesNotContain("\"") + .contains("EMPLOYEES").contains("FK_EMP_DEPT") + .contains("DEPT_ID").contains("DEPARTMENTS").contains("ID"); + } + + @Test + void checkConstraint_name_and_table_respect_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().addCheckConstraint(EMP, "CK_EMP_SALARY", "salary > 0"); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().addCheckConstraint(EMP, "CK_EMP_SALARY", "salary > 0"); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"CK_EMP_SALARY\""); + assertThat(never).doesNotContain("\"") + .contains("EMPLOYEES").contains("CK_EMP_SALARY"); + } + + @Test + void dropConstraint_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().dropConstraint(EMP, "PK_EMP", true); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().dropConstraint(EMP, "PK_EMP", true); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"PK_EMP\""); + assertThat(never).doesNotContain("\"").contains("EMPLOYEES").contains("PK_EMP"); + } + + @Test + void renameTable_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().renameTable(EMP, "PERSON"); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().renameTable(EMP, "PERSON"); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"PERSON\""); + assertThat(never).doesNotContain("\"").contains("EMPLOYEES").contains("PERSON"); + } + + @Test + void renameColumn_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().renameColumn(EMP, "OLD_NAME", "NEW_NAME"); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().renameColumn(EMP, "OLD_NAME", "NEW_NAME"); + + assertThat(always).contains("\"OLD_NAME\"").contains("\"NEW_NAME\""); + assertThat(never).doesNotContain("\"") + .contains("OLD_NAME").contains("NEW_NAME"); + } + + @Test + void alterColumnSetNullability_respects_policy() { + String always = dialectWith(IdentifierQuotingPolicy.ALWAYS) + .ddlGenerator().alterColumnSetNullability(EMP, "EMAIL", false); + String never = dialectWith(IdentifierQuotingPolicy.NEVER) + .ddlGenerator().alterColumnSetNullability(EMP, "EMAIL", false); + + assertThat(always).contains("\"EMPLOYEES\"").contains("\"EMAIL\""); + assertThat(never).doesNotContain("\"") + .contains("EMPLOYEES").contains("EMAIL"); + } +} diff --git a/jdbc/impl/src/test/resources/logback-test.xml b/jdbc/impl/src/test/resources/logback-test.xml new file mode 100644 index 0000000..2299056 --- /dev/null +++ b/jdbc/impl/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + true + + + + + %-5level %logger{36} - %msg%n + + + + + + + diff --git a/jdbc/impl/test.bndrun b/jdbc/impl/test.bndrun new file mode 100644 index 0000000..1b2a5db --- /dev/null +++ b/jdbc/impl/test.bndrun @@ -0,0 +1,84 @@ +#******************************************************************************* +# Copyright (c) 2004 Contributors to the Eclipse Foundation +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# SmartCity Jena - initial +# Stefan Bischof (bipolis.org) - initial +#******************************************************************************* + +-runstartlevel: \ + order=sortbynameversion,\ + begin=-1 + +-runtrace: true + +-tester: biz.aQute.tester.junit-platform + +# JaCoCo calculates test coverage +-runpath.jacoco:\ + org.jacoco.agent,\ + org.jacoco.agent.rt + +-runvm.coverage: -javaagent:${repo;org.jacoco.agent.rt}=destfile=${target-dir}/jacoco.exec + +-runpath.log: \ + ch.qos.logback.classic,\ + ch.qos.logback.core,\ + slf4j.api + +-runproperties.logback:\ + org.osgi.framework.bootdelegation=org.mockito.internal.creation.bytebuddy.inject,\ + logback.configurationFile=${project.build.testOutputDirectory}/logback-test.xml + +-runsystemcapabilities: ${native_capability} + +-resolve.effective: active + + +-runfw: org.apache.felix.framework + +-runee: JavaSE-25 + +-runrequires: \ + bnd.identity;id='${project.artifactId}-tests',\ + bnd.identity;id='${project.artifactId}' + +# -runbundles is calculated by the bnd-resolver-maven-plugin +-runbundles: \ + assertj-core;version='[3.26.0,3.26.1)',\ + com.h2database;version='[2.2.224,2.2.225)',\ + junit-jupiter-api;version='[5.12.2,5.12.3)',\ + junit-jupiter-engine;version='[5.12.2,5.12.3)',\ + junit-jupiter-params;version='[5.12.2,5.12.3)',\ + junit-platform-commons;version='[1.12.2,1.12.3)',\ + junit-platform-engine;version='[1.12.2,1.12.3)',\ + junit-platform-launcher;version='[1.12.2,1.12.3)',\ + net.bytebuddy.byte-buddy;version='[1.17.5,1.17.6)',\ + net.bytebuddy.byte-buddy-agent;version='[1.17.5,1.17.6)',\ + org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\ + org.apache.felix.scr;version='[2.2.10,2.2.11)',\ + org.eclipse.daanse.sql.dialect.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.common;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.dialect.db.h2;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.api;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.impl-tests;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.metadata;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.jdbc.record;version='[0.0.1,0.0.2)',\ + org.eclipse.daanse.sql.model;version='[0.0.1,0.0.2)',\ + org.mockito.junit-jupiter;version='[4.9.0,4.9.1)',\ + org.mockito.mockito-core;version='[4.9.0,4.9.1)',\ + org.objenesis;version='[3.3.0,3.3.1)',\ + org.opentest4j;version='[1.3.0,1.3.1)',\ + org.osgi.service.component;version='[1.5.1,1.5.2)',\ + org.osgi.test.common;version='[1.3.0,1.3.1)',\ + org.osgi.test.junit5;version='[1.3.0,1.3.1)',\ + org.osgi.test.junit5.cm;version='[1.3.0,1.3.1)',\ + org.osgi.util.function;version='[1.2.0,1.2.1)',\ + org.osgi.util.promise;version='[1.3.0,1.3.1)' \ No newline at end of file diff --git a/jdbc/importer/csv/pom.xml b/jdbc/importer/csv/pom.xml new file mode 100644 index 0000000..bae028a --- /dev/null +++ b/jdbc/importer/csv/pom.xml @@ -0,0 +1,89 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.importer + ${revision} + + org.eclipse.daanse.sql.jdbc.importer.csv + Eclipse Daanse SQL JDBC Importer CSV + CSV file import utility for Daanse JDBC database operations. + Provides functionality to import data from CSV files into database tables + with configurable parsing, validation, and transformation options. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + ${project.version} + + + org.slf4j + slf4j-api + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + compile + + + org.eclipse.daanse + org.eclipse.daanse.sql.dialect.api + ${project.version} + compile + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + compile + + + org.eclipse.daanse + org.eclipse.daanse.io.fs.watcher.api + 0.0.1-SNAPSHOT + compile + + + de.siegmar + fastcsv + 3.1.0 + compile + + + + com.h2database + h2 + 2.2.224 + test + + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.impl + ${project.version} + runtime + + + org.eclipse.daanse + org.eclipse.daanse.io.fs.watcher.watchservice + 0.0.1-SNAPSHOT + runtime + + + diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/Constants.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/Constants.java new file mode 100644 index 0000000..b647e70 --- /dev/null +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/Constants.java @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.importer.csv.api; + +public class Constants { + private Constants() { + } + + public static final String PID_CSV_DATA_IMPORTER = "org.eclipse.daanse.sql.jdbc.importer.csv.CsvDataImporter"; + + public static final String PROPERETY_CSV_NULL_VALUE = "nullValue"; + public static final String PROPERETY_CSV_QUOTE_CHARACHTER = "quoteCharacter"; + public static final String PROPERETY_CSV_FIELD_SEPARATOR = "fieldSeparator"; + public static final String PROPERETY_CSV_ENCODING = "encoding"; + public static final String PROPERETY_CSV_SKIP_EMPTY_LINES = "skipEmptyLines"; + public static final String PROPERETY_CSV_COMMENT_CHARACHTER = "commentCharacter"; + public static final String PROPERETY_CSV_IGNORE_DIFFERENT_FIELD_COUNT = "ignoreDifferentFieldCount"; + + public static final String PROPERETY_JDBC_BATCH = "batchSize"; + +} diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/package-info.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/package-info.java new file mode 100644 index 0000000..0d65123 --- /dev/null +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/api/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.importer.csv.api; diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java new file mode 100644 index 0000000..fc52364 --- /dev/null +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporter.java @@ -0,0 +1,468 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena, Stefan Bischof - initial + * + */ +package org.eclipse.daanse.sql.jdbc.importer.csv.impl; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardWatchEventKinds; +import java.nio.file.WatchEvent.Kind; +import java.sql.Connection; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; + +import javax.sql.DataSource; + +import org.eclipse.daanse.io.fs.watcher.api.EventKind; +import org.eclipse.daanse.io.fs.watcher.api.FileSystemWatcherListener; +import org.eclipse.daanse.io.fs.watcher.api.propertytypes.FileSystemWatcherListenerProperties; +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.api.DialectFactory; +import org.eclipse.daanse.sql.jdbc.importer.csv.api.Constants; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TableDefinitionRecord; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; +import org.osgi.service.metatype.annotations.Designate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.siegmar.fastcsv.reader.CloseableIterator; +import de.siegmar.fastcsv.reader.CsvReader; +import de.siegmar.fastcsv.reader.NamedCsvRecord; + +@Designate(ocd = CsvDataImporterConfig.class, factory = true) +@FileSystemWatcherListenerProperties(kinds = EventKind.ENTRY_MODIFY, pattern = ".*.csv", recursive = true) +@Component(scope = ServiceScope.SINGLETON, service = FileSystemWatcherListener.class, configurationPid = Constants.PID_CSV_DATA_IMPORTER) +public class CsvDataImporter implements FileSystemWatcherListener { + + private static final String EXCEPTION_WHILE_WRITING_DATA = "Exception while writing Data"; + + private static final String EXCEPTION_WHILE_CREATING_SCHEMA = "Exception while creating schema"; + + private static final String EXCEPTION_DATABASE_CONNECTION_ERROR = "Database connection error"; + + private static final String EXCEPTION_WHILE_SETTING_VALUE_TO_PREPARED_STATEMENT = "Exception while setting value to PreparedStatement"; + + private static final Logger LOGGER = LoggerFactory.getLogger(CsvDataImporter.class); + + private DataSource dataSource; + + private DatabaseService databaseService; + + private DialectFactory dialectFactory; + + @Reference(name = "dataSource") + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + + public void unsetDataSource(DataSource dataSource) { + if (this.dataSource == dataSource) { + this.dataSource = null; + } + } + + @Reference(name = "databaseService") + public void setDatabaseService(DatabaseService databaseService) { + this.databaseService = databaseService; + } + + public void unsetDatabaseService(DatabaseService databaseService) { + if (this.databaseService == databaseService) { + this.databaseService = null; + } + } + + @Reference(name = "dialectFactory") + public void setDialectFactory(DialectFactory dialectFactory) { + this.dialectFactory = dialectFactory; + } + + public void unsetDialectFactory(DialectFactory dialectFactory) { + if (this.dialectFactory == dialectFactory) { + this.dialectFactory = null; + } + } + + private CsvDataImporterConfig config; + private Dialect dialect; + + private Path basePath; + MetaInfo metaInfo; + + @Activate + public void activate(CsvDataImporterConfig config) throws SQLException { + this.config = config; + metaInfo = databaseService.createMetaInfo(dataSource); + dialect = dialectFactory.createDialect( + org.eclipse.daanse.sql.dialect.api.DialectInitData.fromDataSource(dataSource)); + } + + @Deactivate + public void deactivate() { + config = null; + dialect = null; + } + + private void checkPathAndLoadCsv(Path path) { + + if (Files.isDirectory(path)) { + return; + } + if (!path.toString().endsWith(".csv")) { + return; + } + + try (Connection connection = dataSource.getConnection()) { + loadTable(connection, path); + } catch (SQLException e) { + throw new CsvDataImporterException(EXCEPTION_DATABASE_CONNECTION_ERROR, e); + } + + } + + private void loadTable(Connection connection, Path path) throws SQLException { + String fileName = getFileNameWithoutExtension(path.getFileName().toString()); + LOGGER.debug("Load table {}", fileName); + Optional schema = getSchemaFromPath(path); + + schema.ifPresent(s -> { + + if (s.name().isBlank()) { + return; + } + + String statementCreateSchema = dialect.ddlGenerator().createSchema(s.name(), true); + + try { + connection.createStatement().execute(statementCreateSchema); + } catch (SQLException e) { + // https://github.com/h2database/h2database/issues/4188 + // throw new CsvDataImporterException(EXCEPTION_WHILE_CREATING_SCHEMA, e); + LOGGER.error(EXCEPTION_WHILE_CREATING_SCHEMA, e); + } + }); + + TableReference tableRef = new TableReference(schema, fileName, "TABLE"); + TableDefinition tableDefinition=new TableDefinitionRecord(tableRef); + dropTable(connection, tableRef); + + if (!path.toFile().exists()) { + + LOGGER.warn("File does not exist - {} {}", fileName, path); + return; + } + + CsvReader.CsvReaderBuilder builder = CsvReader.builder().fieldSeparator(config.fieldSeparator()) + .quoteCharacter(config.quoteCharacter()).skipEmptyLines(config.skipEmptyLines()) + .commentCharacter(config.commentCharacter()) + .ignoreDifferentFieldCount(config.ignoreDifferentFieldCount()); + + try (CloseableIterator it = builder.ofNamedCsvRecord(path).iterator()) { + if (!it.hasNext()) { + throw new IllegalStateException("No header found"); + } + NamedCsvRecord types = it.next(); + List headersTypeList = getHeadersTypeList(types); + if (it.hasNext()) { + createTable(connection, headersTypeList, tableDefinition); + insertTable(connection, it, headersTypeList, tableRef); + } + + } catch (IOException e) { + throw new CsvDataImporterException("Exception while Loading csv", e); + } + } + + private void insertTable(Connection connection, CloseableIterator it, + List headersTypeList, TableReference table) throws SQLException { + + String sql = dialect.ddlGenerator().insertInto(table, headersTypeList); + + try (PreparedStatement ps = connection.prepareStatement(sql)) { + batchExecute(connection, ps, it, headersTypeList); + } catch (SQLException e) { + throw new CsvDataImporterException(EXCEPTION_WHILE_WRITING_DATA, e); + } + } + + private void dropTable(Connection connection, TableReference table) throws SQLException { + try { + + String sqlDropTable = dialect.ddlGenerator().dropTable(table, true); + try (Statement stmnt = connection.createStatement()) { + stmnt.execute(sqlDropTable); + } + + } catch (SQLException e) { + throw new CsvDataImporterException("Exception while drop Table", e); + } + } + + public void createTable(Connection connection, List headersTypeList, TableDefinition table) + throws SQLException { + try (Statement stmt = connection.createStatement();) { + + String sql = dialect.ddlGenerator().createTable(table.table(), headersTypeList, null, true); + + LOGGER.debug("Created table in given database. {}", sql); + + stmt.execute(sql); + connection.commit(); + } catch (SQLException e) { + throw new CsvDataImporterException("Exception wile create table", e); + } + + } + + private Optional getSchemaFromPath(Path path) { + Path parent = path.getParent(); + if (basePath.equals(parent)) { + return Optional.empty(); + } + String fileName = parent.getFileName().toString(); + return Optional.of(new SchemaReference(fileName)); + } + + private void batchExecute(Connection connection, PreparedStatement ps, CloseableIterator it, + List columns) throws SQLException { + + connection.setAutoCommit(false); + long start = System.currentTimeMillis(); + int count = 0; + while (it.hasNext()) { + NamedCsvRecord r = it.next(); + + int colIndex = 1; + for (ColumnDefinition columnDefinition : columns) { + processingTypeValues(ps, columnDefinition, colIndex++, r); + } + ps.addBatch(); + ps.clearParameters(); + if (count % config.batchSize() == 0) { + ps.executeBatch(); + LOGGER.debug("execute batch time {}", (System.currentTimeMillis() - start)); + ps.getConnection().commit(); + LOGGER.debug("execute commit time {}", (System.currentTimeMillis() - start)); + start = System.currentTimeMillis(); + } + count++; + } + + ps.executeBatch(); + LOGGER.debug("execute batch time {}", (System.currentTimeMillis() - start)); + + connection.commit(); + LOGGER.debug("execute commit time {}", (System.currentTimeMillis() - start)); + connection.setAutoCommit(true); + } + + private void processingTypeValues(PreparedStatement ps, ColumnDefinition columnDefinition, int index, + NamedCsvRecord r) throws SQLException { + + ColumnReference column = columnDefinition.column(); + String field = r.getField(column.name()); + + try { + setPrepareStatement(ps, index, columnDefinition, field); + } catch (SQLException e) { + throw new CsvDataImporterException(EXCEPTION_WHILE_SETTING_VALUE_TO_PREPARED_STATEMENT, e); + } + } + + private void setPrepareStatement(PreparedStatement ps, int index, ColumnDefinition columnDefinition, String field) + throws SQLException { + + ColumnMetaData type = columnDefinition.columnMetaData(); + + if (field == null || field.equals(config.nullValue())) { + ps.setObject(index, null); + return; + } + switch (type.dataType()) { + case BOOLEAN: { + ps.setBoolean(index, field.equals("") ? Boolean.FALSE : Boolean.valueOf(field)); + return; + } + case BIGINT: { + ps.setLong(index, field.equals("") ? 0l : Long.valueOf(field)); + return; + } + case DATE: { + ps.setDate(index, Date.valueOf(field)); + return; + } + case INTEGER: { + ps.setInt(index, field.equals("") ? 0 : Integer.valueOf(field)); + return; + } + case DECIMAL: { + ps.setDouble(index, field.equals("") ? 0.0 : Double.valueOf(field)); + return; + } + case NUMERIC: { + ps.setDouble(index, field.equals("") ? 0.0 : Double.valueOf(field)); + return; + } + case REAL: { + ps.setDouble(index, field.equals("") ? 0.0 : Double.valueOf(field)); + return; + } + case SMALLINT: { + ps.setShort(index, field.equals("") ? 0 : Short.valueOf(field)); + return; + } + case TIMESTAMP: { + ps.setTimestamp(index, Timestamp.valueOf(field)); + return; + } + case TIME: { + ps.setTime(index, Time.valueOf(field)); + return; + } + case VARCHAR: { + ps.setString(index, field); + return; + } + + default: + ps.setString(index, field); + } + } + + private List getHeadersTypeList(NamedCsvRecord types) { + List result = new ArrayList<>(); + if (types != null) { + for (String header : types.getHeader()) { + ColumnMetaDataRecord sqlType = parseColumnDataType(types.getField(header)); + ColumnDefinition dbc = new ColumnDefinitionRecord(new ColumnReference(header), sqlType); + result.add(dbc); + } + } + return result; + } + + private ColumnMetaDataRecord parseColumnDataType(String stringType) { + int indexStart = stringType.indexOf("("); + int indexEnd = stringType.indexOf(")"); + + String sType = null; + + String detail = null; + if (indexStart > 0) { + sType = stringType.substring(0, indexStart); + detail = stringType.substring(indexStart + 1, indexEnd); + } else { + sType = stringType; + } + + String[] det = detail == null ? new String[] {} : detail.split("\\."); + + JDBCType jdbcType = JDBCType.valueOf(sType); + + if (jdbcType == null) { + jdbcType = JDBCType.VARCHAR; + } + + OptionalInt columnSize = OptionalInt.empty(); + OptionalInt decimalDigits = OptionalInt.empty(); + + if (det.length > 0) { + columnSize = OptionalInt.of(Integer.parseInt(det[0])); + if (det.length > 1) { + decimalDigits = OptionalInt.of(Integer.parseInt(det[1])); + } + } + + return new ColumnMetaDataRecord(jdbcType, jdbcType.getName(), columnSize, decimalDigits, OptionalInt.empty(), ColumnMetaData.Nullability.NULLABLE, OptionalInt.empty(), + java.util.Optional.empty(), java.util.Optional.empty(), + ColumnMetaData.AutoIncrement.NO, ColumnMetaData.GeneratedColumn.NO); + } + + private String getFileNameWithoutExtension(String fileName) { + if (fileName.contains(".")) { + return fileName.substring(0, fileName.lastIndexOf(".")); + } else { + return fileName; + } + } + + private void delete(Path path) { + + String tableName = getFileNameWithoutExtension(path.getFileName().toString()); + LOGGER.debug("Drop table {}", tableName); + + try (Connection connection = dataSource.getConnection()) { + Optional schema = getSchemaFromPath(path); + + TableReference targetTable = new TableReference(schema, tableName, "TABLE"); + String sql = dialect.ddlGenerator().dropTable(targetTable, true); + + try (Statement stmnt = connection.createStatement()) { + stmnt.execute(sql); + } + } catch (SQLException e) { + throw new CsvDataImporterException(EXCEPTION_DATABASE_CONNECTION_ERROR, e); + + } + + } + + @Override + public void handleInitialPaths(List initialPaths) { + initialPaths.parallelStream().forEach(this::checkPathAndLoadCsv); + } + + @Override + public void handlePathEvent(Path path, Kind kind) { + if (Files.isDirectory(path)) { + return; + } + if (kind.name().equals(StandardWatchEventKinds.ENTRY_MODIFY.name()) + || kind.name().equals(StandardWatchEventKinds.ENTRY_CREATE.name())) { + checkPathAndLoadCsv(path); + } + if (kind.name().equals(StandardWatchEventKinds.ENTRY_DELETE.name())) { + delete(path); + } + } + + @Override + public void handleBasePath(Path basePath) { + this.basePath = basePath; + } + +} diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterConfig.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterConfig.java new file mode 100644 index 0000000..4a5efa2 --- /dev/null +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterConfig.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena, Stefan Bischof - initial + * + */ +package org.eclipse.daanse.sql.jdbc.importer.csv.impl; + +import org.eclipse.daanse.io.fs.watcher.api.propertytypes.FileSystemWatcherListenerProperties; +import org.osgi.service.metatype.annotations.AttributeDefinition; +import org.osgi.service.metatype.annotations.ObjectClassDefinition; +import org.osgi.service.metatype.annotations.Option; + +@ObjectClassDefinition() +@FileSystemWatcherListenerProperties +public @interface CsvDataImporterConfig { + + /** + * @return Null Value + */ + @AttributeDefinition(description = "nullValue") + String nullValue() default "NULL"; + + /** + * @return Quote + */ + @AttributeDefinition(description = "quoteCharacter") + char quoteCharacter() default '"'; + + /** + * @return Delimiter + */ + @AttributeDefinition(description = "fieldSeparator") + char fieldSeparator() default ','; + + /** + * @return Encoding default UTF-8 + */ + @AttributeDefinition(description = "encoding", options = { @Option(value = "UTF-8"), @Option(value = "US_ASCII"), + @Option(value = "ISO_8859_1"), @Option(value = "UTF_16BE"), @Option(value = "UTF_16LE"), + @Option(value = "UTF_16") }) + String encoding() default "UTF-8"; + + /** + * @return Skip Empty Lines + */ + @AttributeDefinition(description = "skipEmptyLines", defaultValue = "true") + boolean skipEmptyLines() default true; + + /** + * @return Comment Character + */ + @AttributeDefinition(description = "commentCharacter", defaultValue = "#") + char commentCharacter() default '#'; + + /** + * @return Ignore Different Field Count + */ + @AttributeDefinition(description = "ignoreDifferentFieldCount", defaultValue = "true") + boolean ignoreDifferentFieldCount() default true; + + /** + * @return Clear Table Before Load Data + */ + @AttributeDefinition(description = "clearTableBeforeLoad") + boolean clearTableBeforeLoad() default true; + + /** + * @return Batch Size. Use Batch operation if dialect support it + */ + @AttributeDefinition(description = "batchSize", defaultValue = "5000") + int batchSize() default 1000; +} diff --git a/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterException.java b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterException.java new file mode 100644 index 0000000..5677a84 --- /dev/null +++ b/jdbc/importer/csv/src/main/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/CsvDataImporterException.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena, Stefan Bischof - initial + * + */ +package org.eclipse.daanse.sql.jdbc.importer.csv.impl; + +public class CsvDataImporterException extends RuntimeException { + + private static final long serialVersionUID = 5181035593294590458L; + + public CsvDataImporterException(String msg, Exception e) { + super(msg, e); + } + + public CsvDataImporterException(String msg) { + super(msg); + } +} diff --git a/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/integration/CsvDataLoaderTest.java b/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/integration/CsvDataLoaderTest.java new file mode 100644 index 0000000..bc11887 --- /dev/null +++ b/jdbc/importer/csv/src/test/java/org/eclipse/daanse/sql/jdbc/importer/csv/impl/integration/CsvDataLoaderTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena, Stefan Bischof - initial + * + */ +package org.eclipse.daanse.sql.jdbc.importer.csv.impl.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.osgi.test.common.dictionary.Dictionaries.dictionaryOf; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import javax.sql.DataSource; + +import org.eclipse.daanse.io.fs.watcher.api.FileSystemWatcherWhiteboardConstants; +import org.eclipse.daanse.sql.jdbc.api.DatabaseService; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.importer.csv.api.Constants; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.junit.jupiter.MockitoExtension; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.cm.annotations.RequireConfigurationAdmin; +import org.osgi.test.common.annotation.InjectBundleContext; +import org.osgi.test.common.annotation.InjectService; +import org.osgi.test.junit5.context.BundleContextExtension; +import org.osgi.test.junit5.service.ServiceExtension; + +import aQute.bnd.annotation.spi.ServiceProvider; + +@ExtendWith(BundleContextExtension.class) +@ExtendWith(ServiceExtension.class) +@ExtendWith(MockitoExtension.class) +@RequireConfigurationAdmin +@ServiceProvider(value = DataSource.class) +class CscDataLoaderTest { + @TempDir(cleanup = CleanupMode.ON_SUCCESS) + Path path; + + @InjectBundleContext + BundleContext bc; + + @InjectService + ConfigurationAdmin ca; + + @InjectService + DatabaseService databaseService; + + private Configuration conf; + private Connection connection = null; + private DatabaseMetaData metaData; + + @BeforeEach + void beforeEach() throws SQLException, IOException { + + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setUrl("jdbc:h2:memFS:" + UUID.randomUUID().toString()); + connection = dataSource.getConnection(); + metaData = connection.getMetaData(); + bc.registerService(DataSource.class, dataSource, dictionaryOf("ds", "1")); + } + + private Path copy(String file) throws IOException { + + Path target = path.resolve(file); + InputStream is = bc.getBundle().getResource(file).openConnection().getInputStream(); + byte[] bytes = is.readAllBytes(); + Files.write(target, bytes); + return target; + + } + + @AfterEach + void afterEach() throws IOException { + if (conf != null) { + conf.delete(); + } + } + + private void setupCsvDataLoadServiceImpl(String nullValue, Character quote, Character fieldSeparator, + String encoding, String stringPath) throws IOException { + conf = ca.getFactoryConfiguration(Constants.PID_CSV_DATA_IMPORTER, "1", "?"); + Dictionary dict = new Hashtable<>(); + if (nullValue != null) { + + dict.put(Constants.PROPERETY_CSV_NULL_VALUE, nullValue); + } + if (quote != null) { + dict.put(Constants.PROPERETY_CSV_QUOTE_CHARACHTER, quote); + } + if (fieldSeparator != null) { + dict.put(Constants.PROPERETY_CSV_FIELD_SEPARATOR, fieldSeparator); + } + if (encoding != null) { + dict.put(Constants.PROPERETY_CSV_ENCODING, encoding); + } + + dict.put(FileSystemWatcherWhiteboardConstants.FILESYSTEM_WATCHER_PATH, + stringPath != null ? path.resolve(stringPath).toAbsolutePath().toString() + : path.toAbsolutePath().toString()); + dict.put(FileSystemWatcherWhiteboardConstants.FILESYSTEM_WATCHER_RECURSIVE, true); + dict.put(FileSystemWatcherWhiteboardConstants.FILESYSTEM_WATCHER_KINDS, new String[] { "ENTRY_CREATE" }); + conf.update(dict); + } + + @Test + void testinsertParamStatement() throws IOException, URISyntaxException, SQLException, InterruptedException { + + Path p = path.resolve("csv"); + Files.createDirectories(p); + Thread.sleep(200); + + setupCsvDataLoadServiceImpl("NULL", '\"', ',', "UTF-8", "csv"); + + Thread.sleep(500); + copy("csv/test.csv"); + Thread.sleep(1000); + + TableReference table = new TableReference("test"); + + List tableDefinitions = databaseService.getTableDefinitions(connection, + MetadataProvider.EMPTY, table); + assertThat(tableDefinitions).hasSize(1); + + List columnDefinitions = databaseService.getColumnDefinitions(connection, + MetadataProvider.EMPTY, table); + assertThat(columnDefinitions).hasSize(10); + } + + @Test + void testSubDir() throws IOException, URISyntaxException, SQLException, InterruptedException { + Path p = path.resolve("csv/schema1"); + Files.createDirectories(p); + + Thread.sleep(200); + setupCsvDataLoadServiceImpl("NULL", '\"', ',', "UTF-8", "csv"); + + Thread.sleep(500); + copy("csv/schema1/test1.csv"); + Thread.sleep(1000); + + TableReference table = new TableReference(Optional.of(new SchemaReference("schema1")), "test1"); + List tableDefinitions = databaseService.getTableDefinitions(connection, + MetadataProvider.EMPTY, table); + assertThat(tableDefinitions).hasSize(1); + + List columnDefinitions = databaseService.getColumnDefinitions(connection, + MetadataProvider.EMPTY, table); + assertThat(columnDefinitions).hasSize(10); + + } + +} diff --git a/jdbc/importer/csv/src/test/resources/csv/schema1/test1.csv b/jdbc/importer/csv/src/test/resources/csv/schema1/test1.csv new file mode 100644 index 0000000..dc6e754 --- /dev/null +++ b/jdbc/importer/csv/src/test/resources/csv/schema1/test1.csv @@ -0,0 +1,4 @@ +id,testLong,testBoolean,testDate,testInteger,testNumeric,testDecimal,testSmallInt,testTimeStamp,testString +INTEGER,BIGINT,BOOLEAN,DATE,INTEGER,NUMERIC(15.2),DECIMAL,SMALLINT,TIMESTAMP,VARCHAR(40) +1,100000,true,2023-03-27,10,10.25,1.24,1,2023-03-27 17:45:30.005,test string1 +2,200000,false,2023-03-26,11,11.25,1.34,2,2023-03-26 18:15:20.005,test string2 diff --git a/jdbc/importer/csv/src/test/resources/csv/test.csv b/jdbc/importer/csv/src/test/resources/csv/test.csv new file mode 100644 index 0000000..dc6e754 --- /dev/null +++ b/jdbc/importer/csv/src/test/resources/csv/test.csv @@ -0,0 +1,4 @@ +id,testLong,testBoolean,testDate,testInteger,testNumeric,testDecimal,testSmallInt,testTimeStamp,testString +INTEGER,BIGINT,BOOLEAN,DATE,INTEGER,NUMERIC(15.2),DECIMAL,SMALLINT,TIMESTAMP,VARCHAR(40) +1,100000,true,2023-03-27,10,10.25,1.24,1,2023-03-27 17:45:30.005,test string1 +2,200000,false,2023-03-26,11,11.25,1.34,2,2023-03-26 18:15:20.005,test string2 diff --git a/jdbc/importer/csv/src/test/resources/logback-test.xml b/jdbc/importer/csv/src/test/resources/logback-test.xml new file mode 100644 index 0000000..2299056 --- /dev/null +++ b/jdbc/importer/csv/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + true + + + + + %-5level %logger{36} - %msg%n + + + + + + + diff --git a/jdbc/importer/pom.xml b/jdbc/importer/pom.xml new file mode 100644 index 0000000..fb98efd --- /dev/null +++ b/jdbc/importer/pom.xml @@ -0,0 +1,31 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc + ${revision} + + org.eclipse.daanse.sql.jdbc.importer + pom + Eclipse Daanse SQL JDBC Importer + Parent module for JDBC database import utilities. Contains + components for importing data from various sources into database systems, + supporting different file formats and data transformation operations. + + csv + + diff --git a/jdbc/metadata/pom.xml b/jdbc/metadata/pom.xml new file mode 100644 index 0000000..56001be --- /dev/null +++ b/jdbc/metadata/pom.xml @@ -0,0 +1,102 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc + ${revision} + + org.eclipse.daanse.sql.jdbc.metadata + Eclipse Daanse SQL JDBC Metadata Providers + Engine-specific MetadataProvider implementations (information_schema readers) + plus MetadataProviderFactory services — the database-READING side extracted from the SQL + dialects. Depends only on the neutral api/record model, never on a dialect. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.record + ${project.version} + + + org.slf4j + slf4j-api + + + com.h2database + h2 + 2.2.224 + test + + + org.mariadb.jdbc + mariadb-java-client + 3.4.1 + test + + + org.testcontainers + mariadb + 1.19.7 + test + + + com.microsoft.sqlserver + mssql-jdbc + 12.6.1.jre11 + test + + + org.testcontainers + mssqlserver + 1.19.7 + test + + + org.postgresql + postgresql + 42.7.3 + test + + + org.testcontainers + postgresql + 1.19.7 + test + + + org.testcontainers + junit-jupiter + 1.19.7 + test + + + org.assertj + assertj-core + test + + + diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java new file mode 100644 index 0000000..72ea817 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProvider.java @@ -0,0 +1,904 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; + +/** + * The H2 system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class H2MetadataProvider implements MetadataProvider { + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, + EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, + EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG + FROM INFORMATION_SCHEMA.TRIGGERS + WHERE TRIGGER_SCHEMA = ? + ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, + EVENT_MANIPULATION, JAVA_CLASS, ACTION_ORIENTATION, + EVENT_OBJECT_SCHEMA, EVENT_OBJECT_CATALOG + FROM INFORMATION_SCHEMA.TRIGGERS + WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? + ORDER BY TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT SEQUENCE_NAME, START_VALUE, INCREMENT, MINIMUM_VALUE, + MAXIMUM_VALUE, CYCLE_OPTION, CACHE, DATA_TYPE + FROM INFORMATION_SCHEMA.SEQUENCES + WHERE SEQUENCE_SCHEMA = ? + ORDER BY SEQUENCE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List sequences = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("SEQUENCE_NAME"); + long startValue = rs.getLong("START_VALUE"); + long increment = rs.getLong("INCREMENT"); + long minValue = rs.getLong("MINIMUM_VALUE"); + Optional oMinValue = rs.wasNull() ? Optional.empty() : Optional.of(minValue); + long maxValue = rs.getLong("MAXIMUM_VALUE"); + Optional oMaxValue = rs.wasNull() ? Optional.empty() : Optional.of(maxValue); + String cycleOption = rs.getString("CYCLE_OPTION"); + boolean cycle = "YES".equalsIgnoreCase(cycleOption); + long cacheSize = rs.getLong("CACHE"); + Optional oCacheSize = rs.wasNull() ? Optional.empty() : Optional.of(cacheSize); + String dataType = rs.getString("DATA_TYPE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + sequences.add(new SequenceRecord(new SequenceReference(oSchema, name), startValue, increment, + oMinValue, oMaxValue, cycle, oCacheSize, Optional.ofNullable(dataType))); + } + } + } + return List.copyOf(sequences); + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME + FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc + JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA + AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE tc.TABLE_SCHEMA = ? + AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String checkClause = rs.getString("CHECK_CLAUSE"); + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT cc.CHECK_CLAUSE, tc.CONSTRAINT_NAME, tc.TABLE_NAME + FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS cc + JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA + AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE tc.TABLE_SCHEMA = ? + AND tc.TABLE_NAME = ? + AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String checkClause = rs.getString("CHECK_CLAUSE"); + String constraintName = rs.getString("CONSTRAINT_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' + AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' + AND tc.TABLE_SCHEMA = ? + AND tc.TABLE_NAME = ? + ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, + kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA + AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME + AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' + AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + // Group by table+constraint to build composite PKs + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, + fk_kcu.TABLE_NAME AS FK_TABLE, + fk_kcu.COLUMN_NAME AS FK_COLUMN, + fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, + fk_kcu.TABLE_CATALOG AS FK_CATALOG, + pk_kcu.TABLE_NAME AS PK_TABLE, + pk_kcu.COLUMN_NAME AS PK_COLUMN, + pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, + pk_kcu.TABLE_CATALOG AS PK_CATALOG, + fk_kcu.ORDINAL_POSITION AS KEY_SEQ, + rc.UPDATE_RULE, rc.DELETE_RULE, + rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu + ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu + ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA + AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME + AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION + WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND fk_tc.TABLE_SCHEMA = ? + ORDER BY fk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: filter by the referenced (PK-side) schema + // rather than the referencing (FK-side) schema. + String sql = """ + SELECT fk_tc.CONSTRAINT_NAME AS FK_NAME, + fk_kcu.TABLE_NAME AS FK_TABLE, + fk_kcu.COLUMN_NAME AS FK_COLUMN, + fk_kcu.TABLE_SCHEMA AS FK_SCHEMA, + fk_kcu.TABLE_CATALOG AS FK_CATALOG, + pk_kcu.TABLE_NAME AS PK_TABLE, + pk_kcu.COLUMN_NAME AS PK_COLUMN, + pk_kcu.TABLE_SCHEMA AS PK_SCHEMA, + pk_kcu.TABLE_CATALOG AS PK_CATALOG, + fk_kcu.ORDINAL_POSITION AS KEY_SEQ, + rc.UPDATE_RULE, rc.DELETE_RULE, + rc.UNIQUE_CONSTRAINT_NAME AS PK_NAME + FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk_tc + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fk_kcu + ON fk_tc.CONSTRAINT_SCHEMA = fk_kcu.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = fk_kcu.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc + ON fk_tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA + AND fk_tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pk_kcu + ON rc.UNIQUE_CONSTRAINT_SCHEMA = pk_kcu.CONSTRAINT_SCHEMA + AND rc.UNIQUE_CONSTRAINT_NAME = pk_kcu.CONSTRAINT_NAME + AND fk_kcu.POSITION_IN_UNIQUE_CONSTRAINT = pk_kcu.ORDINAL_POSITION + WHERE fk_tc.CONSTRAINT_TYPE = 'FOREIGN KEY' + AND pk_kcu.TABLE_SCHEMA = ? + ORDER BY pk_kcu.TABLE_NAME, fk_tc.CONSTRAINT_NAME, fk_kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT i.TABLE_NAME, i.INDEX_NAME, i.INDEX_TYPE_NAME, + ic.COLUMN_NAME, ic.ORDINAL_POSITION, ic.IS_UNIQUE, + i.TABLE_SCHEMA, i.TABLE_CATALOG + FROM INFORMATION_SCHEMA.INDEXES i + JOIN INFORMATION_SCHEMA.INDEX_COLUMNS ic + ON i.INDEX_CATALOG = ic.INDEX_CATALOG + AND i.INDEX_SCHEMA = ic.INDEX_SCHEMA + AND i.INDEX_NAME = ic.INDEX_NAME + AND i.TABLE_NAME = ic.TABLE_NAME + WHERE i.TABLE_SCHEMA = ? + ORDER BY i.TABLE_NAME, i.INDEX_NAME, ic.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + // Group index items by table + Map> tableIndexes = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String indexName = rs.getString("INDEX_NAME"); + String indexTypeName = rs.getString("INDEX_TYPE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + boolean isUnique = rs.getBoolean("IS_UNIQUE"); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem.IndexType indexType = mapH2IndexType(indexTypeName); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), indexType, colRef, + ordinalPosition, Optional.empty(), // H2 doesn't expose ASC/DESC in + // INFORMATION_SCHEMA.INDEXES + 0L, // cardinality not available here + 0L, // pages not available here + Optional.empty(), // filter condition + isUnique); + + tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry : tableIndexes.entrySet()) { + result.add(new IndexInfoRecord(tableRefs.get(entry.getKey()), List.copyOf(entry.getValue()))); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT TABLE_NAME, VIEW_DEFINITION, TABLE_SCHEMA, TABLE_CATALOG + FROM INFORMATION_SCHEMA.VIEWS + WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String viewDef = rs.getString("VIEW_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, tableName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewDef), Optional.empty())); + } + } + } + return List.copyOf(views); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + // First, load all parameters grouped by SPECIFIC_NAME + Map> paramMap = loadProcedureColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' + ORDER BY ROUTINE_NAME + """; + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("REMARKS"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName, specificName), + Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), Optional.empty(), Optional.empty())); + } + } + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + // First, load all parameters grouped by SPECIFIC_NAME + Map> paramMap = loadFunctionColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_TYPE, REMARKS, ROUTINE_DEFINITION + FROM INFORMATION_SCHEMA.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """; + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("REMARKS"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName, specificName), + Function.FunctionType.NO_TABLE, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), Optional.empty(), Optional.empty())); + } + } + } + return List.copyOf(functions); + } + + + private Map> loadProcedureColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE, + PARAMETER_DEFAULT + FROM INFORMATION_SCHEMA.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + String paramDefault = rs.getString("PARAMETER_DEFAULT"); + + ProcedureColumn.ColumnType colType = mapProcedureColumnType(paramMode); + JDBCType jdbcType = mapH2DataType(dataType); + + ProcedureColumn col = new ProcedureColumnRecord(paramName != null ? paramName : "", colType, + jdbcType, dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.ofNullable(paramDefault), + ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private Map> loadFunctionColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM INFORMATION_SCHEMA.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + + FunctionColumn.ColumnType colType = mapFunctionColumnType(paramMode); + JDBCType jdbcType = mapH2DataType(dataType); + + FunctionColumn col = new FunctionColumnRecord(paramName != null ? paramName : "", colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private static ProcedureColumn.ColumnType mapProcedureColumnType(String mode) { + if (mode == null) { + return ProcedureColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> ProcedureColumn.ColumnType.IN; + case "OUT" -> ProcedureColumn.ColumnType.OUT; + case "INOUT" -> ProcedureColumn.ColumnType.INOUT; + default -> ProcedureColumn.ColumnType.UNKNOWN; + }; + } + + + private static FunctionColumn.ColumnType mapFunctionColumnType(String mode) { + if (mode == null) { + return FunctionColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> FunctionColumn.ColumnType.IN; + case "OUT" -> FunctionColumn.ColumnType.OUT; + case "INOUT" -> FunctionColumn.ColumnType.INOUT; + default -> FunctionColumn.ColumnType.UNKNOWN; + }; + } + + + private static JDBCType mapH2DataType(String dataType) { + if (dataType == null) { + return JDBCType.OTHER; + } + try { + return JDBCType.valueOf(dataType.toUpperCase().replace(" ", "_")); + } catch (IllegalArgumentException e) { + return switch (dataType.toUpperCase()) { + case "INT", "INT4", "SIGNED" -> JDBCType.INTEGER; + case "INT8" -> JDBCType.BIGINT; + case "INT2" -> JDBCType.SMALLINT; + case "FLOAT4" -> JDBCType.FLOAT; + case "FLOAT8" -> JDBCType.DOUBLE; + case "BOOL" -> JDBCType.BOOLEAN; + case "STRING", "TEXT" -> JDBCType.VARCHAR; + case "BYTEA" -> JDBCType.VARBINARY; + case "CHARACTER VARYING" -> JDBCType.VARCHAR; + case "CHARACTER LARGE OBJECT" -> JDBCType.CLOB; + case "BINARY LARGE OBJECT" -> JDBCType.BLOB; + case "BINARY VARYING" -> JDBCType.VARBINARY; + case "DOUBLE PRECISION" -> JDBCType.DOUBLE; + case "DECFLOAT" -> JDBCType.DECIMAL; + case "TIMESTAMP WITH TIME ZONE" -> JDBCType.TIMESTAMP_WITH_TIMEZONE; + case "TIME WITH TIME ZONE" -> JDBCType.TIME_WITH_TIMEZONE; + case "INTERVAL" -> JDBCType.OTHER; + case "GEOMETRY", "JSON", "UUID", "ENUM", "ARRAY" -> JDBCType.OTHER; + default -> JDBCType.OTHER; + }; + } + } + + + private Trigger readTrigger(ResultSet rs) throws SQLException { + String triggerName = rs.getString("TRIGGER_NAME"); + String tableName = rs.getString("EVENT_OBJECT_TABLE"); + String actionTiming = rs.getString("ACTION_TIMING"); + String eventManipulation = rs.getString("EVENT_MANIPULATION"); + String actionStatement = rs.getString("JAVA_CLASS"); + String actionOrientation = rs.getString("ACTION_ORIENTATION"); + String tableSchema = rs.getString("EVENT_OBJECT_SCHEMA"); + + Optional oSchema = Optional.ofNullable(tableSchema) + .map(s -> new SchemaReference(Optional.empty(), s)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = mapTriggerTiming(actionTiming); + TriggerEvent event = mapTriggerEvent(eventManipulation); + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(actionStatement), Optional.empty(), // H2 doesn't provide full CREATE TRIGGER DDL + // directly + Optional.ofNullable(actionOrientation)); + } + + + private ImportedKey readImportedKey(ResultSet rs) throws SQLException { + String fkName = rs.getString("FK_NAME"); + String fkTable = rs.getString("FK_TABLE"); + String fkColumn = rs.getString("FK_COLUMN"); + String fkSchema = rs.getString("FK_SCHEMA"); + String fkCatalog = rs.getString("FK_CATALOG"); + String pkTable = rs.getString("PK_TABLE"); + String pkColumn = rs.getString("PK_COLUMN"); + String pkSchema = rs.getString("PK_SCHEMA"); + String pkCatalog = rs.getString("PK_CATALOG"); + int keySeq = rs.getInt("KEY_SEQ"); + String updateRule = rs.getString("UPDATE_RULE"); + String deleteRule = rs.getString("DELETE_RULE"); + String pkName = rs.getString("PK_NAME"); + + // FK side + Optional fkCatRef = Optional.ofNullable(fkCatalog).map(CatalogReference::new); + Optional fkSchemaRef = Optional.ofNullable(fkSchema) + .map(s -> new SchemaReference(fkCatRef, s)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side + Optional pkCatRef = Optional.ofNullable(pkCatalog).map(CatalogReference::new); + Optional pkSchemaRef = Optional.ofNullable(pkSchema) + .map(s -> new SchemaReference(pkCatRef, s)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapReferentialAction(updateRule), + mapReferentialAction(deleteRule), Optional.ofNullable(pkName), + ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + // Group by constraint name to collect all columns + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + // H2 default schema is PUBLIC + return connection.getSchema() != null ? connection.getSchema() : "PUBLIC"; + } + + + private static TriggerTiming mapTriggerTiming(String timing) { + if (timing == null) { + return TriggerTiming.AFTER; + } + return switch (timing.toUpperCase()) { + case "BEFORE" -> TriggerTiming.BEFORE; + case "INSTEAD OF" -> TriggerTiming.INSTEAD_OF; + default -> TriggerTiming.AFTER; + }; + } + + + private static TriggerEvent mapTriggerEvent(String event) { + if (event == null) { + return TriggerEvent.INSERT; + } + return switch (event.toUpperCase()) { + case "UPDATE" -> TriggerEvent.UPDATE; + case "DELETE" -> TriggerEvent.DELETE; + default -> TriggerEvent.INSERT; + }; + } + + + private static ImportedKey.ReferentialAction mapReferentialAction(String action) { + if (action == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (action.toUpperCase()) { + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; + case "SET DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; + case "RESTRICT" -> ImportedKey.ReferentialAction.RESTRICT; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + private static IndexInfoItem.IndexType mapH2IndexType(String indexTypeName) { + if (indexTypeName == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (indexTypeName.toUpperCase()) { + case "HASH INDEX" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderFactory.java new file mode 100644 index 0000000..0917825 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The H2 {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class H2MetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("h2"); + } + + @Override + public MetadataProvider createProvider() { + return new H2MetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProvider.java new file mode 100644 index 0000000..bd72e3f --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProvider.java @@ -0,0 +1,935 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PartitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The MariaDb system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class MariaDbMetadataProvider implements MetadataProvider { + + + private static final Logger LOGGER = LoggerFactory.getLogger(MariaDbMetadataProvider.class); + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, + ACTION_STATEMENT, ACTION_ORIENTATION + FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? + ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, + ACTION_STATEMENT, ACTION_ORIENTATION + FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? + ORDER BY TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + // MariaDB stores sequences as TABLES with TABLE_TYPE='SEQUENCE' (since 10.3). + // Each sequence's properties are retrieved by querying the sequence itself. + String listSql = """ + SELECT TABLE_NAME FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'SEQUENCE' + ORDER BY TABLE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List sequenceNames = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(listSql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + sequenceNames.add(rs.getString("TABLE_NAME")); + } + } + } catch (SQLException e) { + LOGGER.debug("Sequences not supported on this MariaDB version: {}", e.getMessage()); + return List.of(); + } + + List sequences = new ArrayList<>(); + for (String name : sequenceNames) { + Sequence sequence = readMariaDbSequence(connection, schemaName, name); + if (sequence != null) { + sequences.add(sequence); + } + } + return List.copyOf(sequences); + } + + + private Sequence readMariaDbSequence(Connection connection, String schemaName, String name) throws SQLException { + // MariaDB sequences are queried directly; identifier quoting uses backticks. + String quotedSchema = "`" + schemaName.replace("`", "``") + "`"; + String quotedName = "`" + name.replace("`", "``") + "`"; + String sql = "SELECT start_value, minimum_value, maximum_value, increment, cycle_option, cache_size " + "FROM " + + quotedSchema + "." + quotedName; + try (PreparedStatement ps = connection.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return null; + } + long startValue = rs.getLong("start_value"); + long minValue = rs.getLong("minimum_value"); + Optional oMinValue = rs.wasNull() ? Optional.empty() : Optional.of(minValue); + long maxValue = rs.getLong("maximum_value"); + Optional oMaxValue = rs.wasNull() ? Optional.empty() : Optional.of(maxValue); + long increment = rs.getLong("increment"); + int cycleOption = rs.getInt("cycle_option"); + boolean cycle = cycleOption != 0; + long cacheSize = rs.getLong("cache_size"); + Optional oCacheSize = rs.wasNull() ? Optional.empty() : Optional.of(cacheSize); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + return new SequenceRecord(new SequenceReference(oSchema, name), startValue, increment, oMinValue, oMaxValue, + cycle, oCacheSize, Optional.empty()); + } + } + + + @Override + public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + // INFORMATION_SCHEMA.PARTITIONS returns one row per non-partitioned table with + // PARTITION_NAME=NULL, plus one row per (partition, sub-partition) for + // partitioned tables. + // We filter out the PARTITION_NAME IS NULL rows and emit a Partition entry per + // remaining row; + // sub-partition rows carry their parent's name in parentPartitionName. + String sql = """ + SELECT TABLE_NAME, PARTITION_NAME, SUBPARTITION_NAME, + PARTITION_METHOD, SUBPARTITION_METHOD, + PARTITION_EXPRESSION, SUBPARTITION_EXPRESSION, + PARTITION_DESCRIPTION, + PARTITION_ORDINAL_POSITION, SUBPARTITION_ORDINAL_POSITION, + TABLE_ROWS + FROM information_schema.PARTITIONS + WHERE TABLE_SCHEMA = ? AND PARTITION_NAME IS NOT NULL + ORDER BY TABLE_NAME, PARTITION_ORDINAL_POSITION, SUBPARTITION_ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List partitions = new ArrayList<>(); + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String partitionName = rs.getString("PARTITION_NAME"); + String subPartitionName = rs.getString("SUBPARTITION_NAME"); + boolean isSub = subPartitionName != null; + + String name = isSub ? subPartitionName : partitionName; + String methodStr = rs.getString(isSub ? "SUBPARTITION_METHOD" : "PARTITION_METHOD"); + PartitionMethod method = mapPartitionMethod(methodStr); + String expression = rs.getString(isSub ? "SUBPARTITION_EXPRESSION" : "PARTITION_EXPRESSION"); + String description = rs.getString("PARTITION_DESCRIPTION"); + int ordinal = rs.getInt(isSub ? "SUBPARTITION_ORDINAL_POSITION" : "PARTITION_ORDINAL_POSITION"); + Optional oOrdinal = rs.wasNull() ? Optional.empty() : Optional.of(ordinal); + long rowCount = rs.getLong("TABLE_ROWS"); + Optional oRowCount = rs.wasNull() ? Optional.empty() : Optional.of(rowCount); + + TableReference tableRef = new TableReference(oSchema, tableName); + partitions.add(new PartitionRecord(name, tableRef, oOrdinal, method, + expression == null ? Optional.empty() : Optional.of(expression), + description == null ? Optional.empty() : Optional.of(description), oRowCount, + isSub ? Optional.of(partitionName) : Optional.empty(), Optional.empty(), Optional.empty())); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not load partitions from INFORMATION_SCHEMA.PARTITIONS: {}", e.getMessage()); + return List.of(); + } + return List.copyOf(partitions); + } + + + private static PartitionMethod mapPartitionMethod(String raw) { + if (raw == null) { + return PartitionMethod.OTHER; + } + return switch (raw.toUpperCase(java.util.Locale.ROOT)) { + case "RANGE", "RANGE COLUMNS" -> PartitionMethod.RANGE; + case "LIST", "LIST COLUMNS" -> PartitionMethod.LIST; + case "HASH" -> PartitionMethod.HASH; + case "KEY" -> PartitionMethod.KEY; + case "LINEAR HASH" -> PartitionMethod.LINEAR_HASH; + case "LINEAR KEY" -> PartitionMethod.LINEAR_KEY; + default -> PartitionMethod.OTHER; + }; + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT cc.CONSTRAINT_NAME, cc.CHECK_CLAUSE, tc.TABLE_NAME + FROM information_schema.CHECK_CONSTRAINTS cc + JOIN information_schema.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE cc.CONSTRAINT_SCHEMA = ? AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.TABLE_NAME, cc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String checkClause = rs.getString("CHECK_CLAUSE"); + String tableName = rs.getString("TABLE_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT cc.CONSTRAINT_NAME, cc.CHECK_CLAUSE, tc.TABLE_NAME + FROM information_schema.CHECK_CONSTRAINTS cc + JOIN information_schema.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE cc.CONSTRAINT_SCHEMA = ? AND tc.CONSTRAINT_TYPE = 'CHECK' AND tc.TABLE_NAME = ? + ORDER BY cc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String checkClause = rs.getString("CHECK_CLAUSE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' AND tc.TABLE_SCHEMA = ? AND tc.TABLE_NAME = ? + ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String constraintName = rs.getString("CONSTRAINT_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME AS FK_NAME, kcu.TABLE_NAME AS FK_TABLE, kcu.COLUMN_NAME AS FK_COLUMN, + kcu.REFERENCED_TABLE_NAME AS PK_TABLE, kcu.REFERENCED_COLUMN_NAME AS PK_COLUMN, + kcu.ORDINAL_POSITION AS KEY_SEQ, rc.DELETE_RULE, rc.UPDATE_RULE + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + JOIN information_schema.REFERENTIAL_CONSTRAINTS rc + ON tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY kcu.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: filter by the referenced (PK-side) schema. + String sql = """ + SELECT tc.CONSTRAINT_NAME AS FK_NAME, kcu.TABLE_NAME AS FK_TABLE, kcu.COLUMN_NAME AS FK_COLUMN, + kcu.REFERENCED_TABLE_NAME AS PK_TABLE, kcu.REFERENCED_COLUMN_NAME AS PK_COLUMN, + kcu.ORDINAL_POSITION AS KEY_SEQ, rc.DELETE_RULE, rc.UPDATE_RULE + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + JOIN information_schema.REFERENTIAL_CONSTRAINTS rc + ON tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY' AND kcu.REFERENCED_TABLE_SCHEMA = ? + ORDER BY kcu.REFERENCED_TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE, INDEX_TYPE, CARDINALITY + FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX + """; + String schemaName = resolveSchema(schema, connection); + Map> tableIndexes = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String indexName = rs.getString("INDEX_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + int seqInIndex = rs.getInt("SEQ_IN_INDEX"); + boolean nonUnique = rs.getBoolean("NON_UNIQUE"); + String indexType = rs.getString("INDEX_TYPE"); + long cardinality = rs.getLong("CARDINALITY"); + + IndexInfoItem.IndexType mappedType = mapIndexType(indexType); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, + seqInIndex, Optional.empty(), // MariaDB STATISTICS doesn't expose ASC/DESC + cardinality, 0L, // pages not available + Optional.empty(), // filter condition + !nonUnique); + + tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry : tableIndexes.entrySet()) { + result.add(new IndexInfoRecord(tableRefs.get(entry.getKey()), List.copyOf(entry.getValue()))); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String viewDef = rs.getString("VIEW_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, tableName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewDef), Optional.empty())); + } + } + } + return List.copyOf(views); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadProcedureColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_COMMENT, ROUTINE_DEFINITION + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' + ORDER BY ROUTINE_NAME + """; + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("ROUTINE_COMMENT"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + Optional fullDef = showCreateRoutine(connection, "PROCEDURE", schemaName, routineName); + + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName, specificName), + Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), fullDef, Optional.empty())); + } + } + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadFunctionColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_COMMENT, ROUTINE_DEFINITION + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """; + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("ROUTINE_COMMENT"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + Optional fullDef = showCreateRoutine(connection, "FUNCTION", schemaName, routineName); + + functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName, specificName), + Function.FunctionType.NO_TABLE, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), fullDef, Optional.empty())); + } + } + } + return List.copyOf(functions); + } + + + private Optional showCreateRoutine(Connection connection, String kind, String schemaName, String name) { + // SHOW CREATE PROCEDURE / FUNCTION returns the full DDL. Silently skip if + // permissions are missing. + String quotedSchema = "`" + schemaName.replace("`", "``") + "`"; + String quotedName = "`" + name.replace("`", "``") + "`"; + String sql = "SHOW CREATE " + kind + " " + quotedSchema + "." + quotedName; + try (PreparedStatement ps = connection.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String col = "Create " + kind.charAt(0) + kind.substring(1).toLowerCase(); + String def = rs.getString(col); + return Optional.ofNullable(def); + } + } catch (SQLException e) { + LOGGER.debug("Could not retrieve DDL for {} {}.{}: {}", kind, schemaName, name, e.getMessage()); + } + return Optional.empty(); + } + + + private Map> loadProcedureColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM information_schema.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? AND ORDINAL_POSITION > 0 + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + + ProcedureColumn.ColumnType colType = mapProcedureColumnType(paramMode); + JDBCType jdbcType = mapMariaDbJdbcType(dataType); + + ProcedureColumn col = new ProcedureColumnRecord(paramName != null ? paramName : "", colType, + jdbcType, dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.empty(), ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private Map> loadFunctionColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM information_schema.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + + FunctionColumn.ColumnType colType = mapFunctionColumnType(paramMode); + JDBCType jdbcType = mapMariaDbJdbcType(dataType); + + FunctionColumn col = new FunctionColumnRecord(paramName != null ? paramName : "", colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private static ProcedureColumn.ColumnType mapProcedureColumnType(String mode) { + if (mode == null) { + return ProcedureColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> ProcedureColumn.ColumnType.IN; + case "OUT" -> ProcedureColumn.ColumnType.OUT; + case "INOUT" -> ProcedureColumn.ColumnType.INOUT; + default -> ProcedureColumn.ColumnType.UNKNOWN; + }; + } + + + private static FunctionColumn.ColumnType mapFunctionColumnType(String mode) { + if (mode == null) { + return FunctionColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> FunctionColumn.ColumnType.IN; + case "OUT" -> FunctionColumn.ColumnType.OUT; + case "INOUT" -> FunctionColumn.ColumnType.INOUT; + default -> FunctionColumn.ColumnType.UNKNOWN; + }; + } + + + private static JDBCType mapMariaDbJdbcType(String dataType) { + if (dataType == null) { + return JDBCType.OTHER; + } + try { + return JDBCType.valueOf(dataType.toUpperCase().replace(" ", "_")); + } catch (IllegalArgumentException e) { + return switch (dataType.toUpperCase()) { + case "INT" -> JDBCType.INTEGER; + case "MEDIUMINT" -> JDBCType.INTEGER; + case "TINYINT" -> JDBCType.TINYINT; + case "BOOL", "BOOLEAN" -> JDBCType.BOOLEAN; + case "TEXT", "MEDIUMTEXT", "LONGTEXT", "TINYTEXT" -> JDBCType.VARCHAR; + case "DATETIME" -> JDBCType.TIMESTAMP; + case "ENUM", "SET", "JSON", "GEOMETRY" -> JDBCType.OTHER; + default -> JDBCType.OTHER; + }; + } + } + + + private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException { + String triggerName = rs.getString("TRIGGER_NAME"); + String tableName = rs.getString("EVENT_OBJECT_TABLE"); + String actionTiming = rs.getString("ACTION_TIMING"); + String eventManipulation = rs.getString("EVENT_MANIPULATION"); + String actionStatement = rs.getString("ACTION_STATEMENT"); + String actionOrientation = rs.getString("ACTION_ORIENTATION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = mapTriggerTiming(actionTiming); + TriggerEvent event = mapTriggerEvent(eventManipulation); + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(actionStatement), Optional.empty(), Optional.ofNullable(actionOrientation)); + } + + + private ImportedKey readImportedKey(ResultSet rs, String schemaName) throws SQLException { + String fkName = rs.getString("FK_NAME"); + String fkTable = rs.getString("FK_TABLE"); + String fkColumn = rs.getString("FK_COLUMN"); + String pkTable = rs.getString("PK_TABLE"); + String pkColumn = rs.getString("PK_COLUMN"); + int keySeq = rs.getInt("KEY_SEQ"); + String deleteRule = rs.getString("DELETE_RULE"); + String updateRule = rs.getString("UPDATE_RULE"); + + // FK side - MariaDB uses catalog as database name, schema is same + Optional fkCatRef = Optional.empty(); + Optional fkSchemaRef = Optional.of(new SchemaReference(fkCatRef, schemaName)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side - referenced table is in the same schema context + Optional pkCatRef = Optional.empty(); + Optional pkSchemaRef = Optional.of(new SchemaReference(pkCatRef, schemaName)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapReferentialAction(updateRule), + mapReferentialAction(deleteRule), Optional.empty(), ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + // MariaDB, like MySQL, uses catalog as database name + return connection.getCatalog(); + } + + + private static TriggerTiming mapTriggerTiming(String timing) { + if (timing == null) { + return TriggerTiming.AFTER; + } + return switch (timing.toUpperCase()) { + case "BEFORE" -> TriggerTiming.BEFORE; + default -> TriggerTiming.AFTER; + }; + } + + + private static TriggerEvent mapTriggerEvent(String event) { + if (event == null) { + return TriggerEvent.INSERT; + } + return switch (event.toUpperCase()) { + case "UPDATE" -> TriggerEvent.UPDATE; + case "DELETE" -> TriggerEvent.DELETE; + default -> TriggerEvent.INSERT; + }; + } + + + private static ImportedKey.ReferentialAction mapReferentialAction(String action) { + if (action == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (action.toUpperCase()) { + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; + case "SET DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; + case "RESTRICT" -> ImportedKey.ReferentialAction.RESTRICT; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + private static IndexInfoItem.IndexType mapIndexType(String indexType) { + if (indexType == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (indexType.toUpperCase()) { + case "HASH" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + case "BTREE" -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + case "FULLTEXT" -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + case "SPATIAL" -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProviderFactory.java new file mode 100644 index 0000000..660bfbd --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDbMetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The MariaDB {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class MariaDbMetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("mariadb"); + } + + @Override + public MetadataProvider createProvider() { + return new MariaDbMetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MetadataProviders.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MetadataProviders.java new file mode 100644 index 0000000..423f1ce --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MetadataProviders.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; + +/** + * Static resolver over the engine {@link MetadataProviderFactory} implementations for + * non-OSGi consumers (e.g. CWM snapshot code): picks the engine-specific + * {@link MetadataProvider} by database product name. MariaDB is checked BEFORE MySQL — + * a MariaDB server on the MySQL driver may report "MySQL", the reverse never happens. + */ +public final class MetadataProviders { + + private static final List FACTORIES = List.of( + new MariaDbMetadataProviderFactory(), + new MySqlMetadataProviderFactory(), + new PostgreSqlMetadataProviderFactory(), + new MicrosoftSqlServerMetadataProviderFactory(), + new OracleMetadataProviderFactory(), + new H2MetadataProviderFactory()); + + private MetadataProviders() { + } + + public static Optional forProductName(String databaseProductName) { + return FACTORIES.stream() + .filter(f -> f.supports(databaseProductName)) + .findFirst() + .map(MetadataProviderFactory::createProvider); + } + + /** Resolves via {@code connection.getMetaData().getDatabaseProductName()}. */ + public static Optional forConnection(Connection connection) throws SQLException { + return forProductName(connection.getMetaData().getDatabaseProductName()); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java new file mode 100644 index 0000000..7317957 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProvider.java @@ -0,0 +1,1090 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedTypeReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnPrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PartitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PseudoColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UserDefinedTypeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; + +/** + * The MicrosoftSqlServer system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class MicrosoftSqlServerMetadataProvider implements MetadataProvider { + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT t.name AS trigger_name, OBJECT_SCHEMA_NAME(t.parent_id) AS schema_name, + OBJECT_NAME(t.parent_id) AS table_name, t.is_instead_of_trigger, m.definition + FROM sys.triggers t JOIN sys.sql_modules m ON m.object_id = t.object_id + WHERE t.parent_id > 0 AND OBJECT_SCHEMA_NAME(t.parent_id) = ? + ORDER BY OBJECT_NAME(t.parent_id), t.name + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT t.name AS trigger_name, OBJECT_SCHEMA_NAME(t.parent_id) AS schema_name, + OBJECT_NAME(t.parent_id) AS table_name, t.is_instead_of_trigger, m.definition + FROM sys.triggers t JOIN sys.sql_modules m ON m.object_id = t.object_id + WHERE t.parent_id > 0 AND OBJECT_SCHEMA_NAME(t.parent_id) = ? + AND OBJECT_NAME(t.parent_id) = ? + ORDER BY t.name + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT s.name AS sequence_name, SCHEMA_NAME(s.schema_id) AS schema_name, + CAST(s.start_value AS BIGINT) AS start_value, CAST(s.increment AS BIGINT) AS increment, + CAST(s.minimum_value AS BIGINT) AS minimum_value, CAST(s.maximum_value AS BIGINT) AS maximum_value, + s.is_cycling, CAST(s.current_value AS BIGINT) AS current_value, + TYPE_NAME(s.system_type_id) AS data_type + FROM sys.sequences s WHERE s.schema_id = SCHEMA_ID(?) + ORDER BY s.name + """; + String schemaName = resolveSchema(schema, connection); + List sequences = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("sequence_name"); + long startValue = rs.getLong("start_value"); + long increment = rs.getLong("increment"); + long minValue = rs.getLong("minimum_value"); + Optional oMinValue = rs.wasNull() ? Optional.empty() : Optional.of(minValue); + long maxValue = rs.getLong("maximum_value"); + Optional oMaxValue = rs.wasNull() ? Optional.empty() : Optional.of(maxValue); + boolean cycle = rs.getBoolean("is_cycling"); + long currentValue = rs.getLong("current_value"); + Optional oCurrentValue = rs.wasNull() ? Optional.empty() : Optional.of(currentValue); + String dataType = rs.getString("data_type"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + sequences.add(new SequenceRecord(new SequenceReference(oSchema, name), startValue, increment, + oMinValue, oMaxValue, cycle, oCurrentValue, Optional.ofNullable(dataType))); + } + } + } + return List.copyOf(sequences); + } + + + @Override + public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + // SQL Server partitioning is always RANGE; sys.partitions has one row per + // (table, index, partition_number). Restricting index_id IN (0, 1) gives one + // row + // per partition (heap or clustered) and avoids duplicates from nonclustered + // indexes. + // sys.partition_range_values supplies the boundary value at boundary_id = + // partition_number. + String sql = """ + SELECT OBJECT_NAME(p.object_id) AS table_name, + p.partition_number, + p.rows AS row_count, + prv.value AS boundary_value, + (SELECT TOP 1 col.name + FROM sys.index_columns ic + JOIN sys.columns col ON col.object_id = ic.object_id AND col.column_id = ic.column_id + WHERE ic.object_id = p.object_id AND ic.index_id = p.index_id AND ic.partition_ordinal > 0 + ) AS partition_column + FROM sys.partitions p + JOIN sys.tables t ON t.object_id = p.object_id + JOIN sys.indexes i ON i.object_id = p.object_id AND i.index_id = p.index_id + JOIN sys.partition_schemes ps ON ps.data_space_id = i.data_space_id + JOIN sys.partition_functions pf ON pf.function_id = ps.function_id + LEFT JOIN sys.partition_range_values prv + ON prv.function_id = pf.function_id AND prv.boundary_id = p.partition_number + WHERE SCHEMA_NAME(t.schema_id) = ? AND i.index_id IN (0, 1) + ORDER BY OBJECT_NAME(p.object_id), p.partition_number + """; + String schemaName = resolveSchema(schema, connection); + List partitions = new ArrayList<>(); + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + int partitionNumber = rs.getInt("partition_number"); + long rowCount = rs.getLong("row_count"); + Optional oRowCount = rs.wasNull() ? Optional.empty() : Optional.of(rowCount); + Object boundary = rs.getObject("boundary_value"); + String column = rs.getString("partition_column"); + + TableReference tableRef = new TableReference(oSchema, tableName); + partitions.add(new PartitionRecord("p" + partitionNumber, tableRef, Optional.of(partitionNumber), + PartitionMethod.RANGE, column == null ? Optional.empty() : Optional.of(column), + boundary == null ? Optional.empty() : Optional.of(boundary.toString()), oRowCount, + Optional.empty(), Optional.empty(), Optional.empty())); + } + } + } catch (SQLException e) { + return List.of(); + } + return List.copyOf(partitions); + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT cc.name AS constraint_name, OBJECT_NAME(cc.parent_object_id) AS table_name, cc.definition + FROM sys.check_constraints cc + WHERE OBJECT_SCHEMA_NAME(cc.parent_object_id) = ? + ORDER BY OBJECT_NAME(cc.parent_object_id), cc.name + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("constraint_name"); + String tableName = rs.getString("table_name"); + String definition = rs.getString("definition"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, definition)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT cc.name AS constraint_name, OBJECT_NAME(cc.parent_object_id) AS table_name, cc.definition + FROM sys.check_constraints cc + WHERE OBJECT_SCHEMA_NAME(cc.parent_object_id) = ? + AND OBJECT_NAME(cc.parent_object_id) = ? + ORDER BY cc.name + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("constraint_name"); + String definition = rs.getString("definition"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, definition)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT i.name AS constraint_name, OBJECT_NAME(i.object_id) AS table_name, + c.name AS column_name, ic.key_ordinal + FROM sys.indexes i + JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id + JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id + WHERE i.is_unique_constraint = 1 AND OBJECT_SCHEMA_NAME(i.object_id) = ? + ORDER BY OBJECT_NAME(i.object_id), i.name, ic.key_ordinal + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT i.name AS constraint_name, OBJECT_NAME(i.object_id) AS table_name, + c.name AS column_name, ic.key_ordinal + FROM sys.indexes i + JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id + JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id + WHERE i.is_unique_constraint = 1 AND OBJECT_SCHEMA_NAME(i.object_id) = ? + AND OBJECT_NAME(i.object_id) = ? + ORDER BY i.name, ic.key_ordinal + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT i.name AS constraint_name, OBJECT_NAME(i.object_id) AS table_name, + c.name AS column_name, ic.key_ordinal + FROM sys.indexes i + JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id + JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id + WHERE i.is_primary_key = 1 AND OBJECT_SCHEMA_NAME(i.object_id) = ? + ORDER BY OBJECT_NAME(i.object_id), ic.key_ordinal + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("constraint_name"); + String tableName = rs.getString("table_name"); + String columnName = rs.getString("column_name"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT fk.name AS fk_name, OBJECT_NAME(fk.parent_object_id) AS fk_table, + fk_col.name AS fk_column, OBJECT_NAME(fk.referenced_object_id) AS pk_table, + pk_col.name AS pk_column, fkc.constraint_column_id AS key_seq, + fk.delete_referential_action_desc AS delete_rule, + fk.update_referential_action_desc AS update_rule + FROM sys.foreign_keys fk + JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id + JOIN sys.columns fk_col ON fk_col.object_id = fkc.parent_object_id AND fk_col.column_id = fkc.parent_column_id + JOIN sys.columns pk_col ON pk_col.object_id = fkc.referenced_object_id AND pk_col.column_id = fkc.referenced_column_id + WHERE OBJECT_SCHEMA_NAME(fk.parent_object_id) = ? + ORDER BY OBJECT_NAME(fk.parent_object_id), fk.name, fkc.constraint_column_id + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: filter by the schema of the REFERENCED + // table. + String sql = """ + SELECT fk.name AS fk_name, OBJECT_NAME(fk.parent_object_id) AS fk_table, + fk_col.name AS fk_column, OBJECT_NAME(fk.referenced_object_id) AS pk_table, + pk_col.name AS pk_column, fkc.constraint_column_id AS key_seq, + fk.delete_referential_action_desc AS delete_rule, + fk.update_referential_action_desc AS update_rule + FROM sys.foreign_keys fk + JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id + JOIN sys.columns fk_col ON fk_col.object_id = fkc.parent_object_id AND fk_col.column_id = fkc.parent_column_id + JOIN sys.columns pk_col ON pk_col.object_id = fkc.referenced_object_id AND pk_col.column_id = fkc.referenced_column_id + WHERE OBJECT_SCHEMA_NAME(fk.referenced_object_id) = ? + ORDER BY OBJECT_NAME(fk.referenced_object_id), fk.name, fkc.constraint_column_id + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT i.name AS index_name, i.type_desc AS index_type, i.is_unique, + OBJECT_NAME(i.object_id) AS table_name, c.name AS column_name, + ic.key_ordinal, ic.is_descending_key + FROM sys.indexes i + JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id + JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id + WHERE OBJECT_SCHEMA_NAME(i.object_id) = ? AND i.type > 0 + AND i.is_primary_key = 0 AND i.is_unique_constraint = 0 + ORDER BY OBJECT_NAME(i.object_id), i.name, ic.key_ordinal + """; + String schemaName = resolveSchema(schema, connection); + Map> tableIndexes = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + String indexName = rs.getString("index_name"); + String indexType = rs.getString("index_type"); + boolean isUnique = rs.getBoolean("is_unique"); + String columnName = rs.getString("column_name"); + int ordinalPosition = rs.getInt("key_ordinal"); + boolean isDescending = rs.getBoolean("is_descending_key"); + + IndexInfoItem.IndexType mappedType = mapMssqlIndexType(indexType); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, + ordinalPosition, Optional.of(!isDescending), 0L, 0L, Optional.empty(), isUnique); + + tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry : tableIndexes.entrySet()) { + result.add(new IndexInfoRecord(tableRefs.get(entry.getKey()), List.copyOf(entry.getValue()))); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT v.name AS view_name, m.definition + FROM sys.views v JOIN sys.sql_modules m ON m.object_id = v.object_id + WHERE OBJECT_SCHEMA_NAME(v.object_id) = ? ORDER BY v.name + """; + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String viewName = rs.getString("view_name"); + String definition = rs.getString("definition"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, viewName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(definition), + Optional.ofNullable(definition))); + } + } + } + return List.copyOf(views); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadMssqlProcedureColumns(connection, schemaName); + + String sql = """ + SELECT p.name AS routine_name, OBJECT_SCHEMA_NAME(p.object_id) AS schema_name, + m.definition AS body, p.modify_date + FROM sys.procedures p + LEFT JOIN sys.sql_modules m ON m.object_id = p.object_id + WHERE OBJECT_SCHEMA_NAME(p.object_id) = ? + ORDER BY p.name + """; + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String body = rs.getString("body"); + Timestamp modifyDate = rs.getTimestamp("modify_date"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(routineName, List.of()); + + // sys.sql_modules.definition contains the full CREATE statement as stored, + // which is suitable as both body and fullDefinition. + Optional bodyOpt = Optional.ofNullable(body); + Optional lastMod = modifyDate == null ? Optional.empty() + : Optional.of(modifyDate.toInstant()); + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName), + Procedure.ProcedureType.NO_RESULT, Optional.empty(), cols, bodyOpt, bodyOpt, lastMod)); + } + } + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadMssqlFunctionColumns(connection, schemaName); + + String sql = """ + SELECT o.name AS routine_name, o.type_desc, m.definition AS body, o.modify_date + FROM sys.objects o + LEFT JOIN sys.sql_modules m ON m.object_id = o.object_id + WHERE o.type IN ('FN', 'IF', 'TF') AND OBJECT_SCHEMA_NAME(o.object_id) = ? + ORDER BY o.name + """; + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String typeDesc = rs.getString("type_desc"); + String body = rs.getString("body"); + Timestamp modifyDate = rs.getTimestamp("modify_date"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(routineName, List.of()); + + Function.FunctionType funcType = "TABLE_VALUED_FUNCTION".equalsIgnoreCase(typeDesc) + || "INLINE_TABLE_VALUED_FUNCTION".equalsIgnoreCase(typeDesc) + ? Function.FunctionType.RETURNS_TABLE + : Function.FunctionType.NO_TABLE; + + Optional bodyOpt = Optional.ofNullable(body); + Optional lastMod = modifyDate == null ? Optional.empty() + : Optional.of(modifyDate.toInstant()); + functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName), funcType, + Optional.empty(), cols, bodyOpt, bodyOpt, lastMod)); + } + } + } + return List.copyOf(functions); + } + + + private Map> loadMssqlProcedureColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT OBJECT_NAME(p.object_id) AS routine_name, p.name AS param_name, + p.is_output, TYPE_NAME(p.system_type_id) AS data_type, + p.parameter_id, p.precision, p.scale + FROM sys.parameters p + JOIN sys.procedures pr ON pr.object_id = p.object_id + WHERE OBJECT_SCHEMA_NAME(p.object_id) = ? AND p.parameter_id > 0 + ORDER BY OBJECT_NAME(p.object_id), p.parameter_id + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String paramName = rs.getString("param_name"); + boolean isOutput = rs.getBoolean("is_output"); + String dataType = rs.getString("data_type"); + int paramId = rs.getInt("parameter_id"); + int precision = rs.getInt("precision"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("scale"); + boolean scaleNull = rs.wasNull(); + + // MSSQL param names start with @ + String cleanName = paramName != null && paramName.startsWith("@") ? paramName.substring(1) + : (paramName != null ? paramName : ""); + + ProcedureColumn.ColumnType colType = isOutput ? ProcedureColumn.ColumnType.OUT + : ProcedureColumn.ColumnType.IN; + JDBCType jdbcType = mapMssqlJdbcType(dataType); + + ProcedureColumn col = new ProcedureColumnRecord(cleanName, colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.empty(), paramId); + + result.computeIfAbsent(routineName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private Map> loadMssqlFunctionColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT OBJECT_NAME(p.object_id) AS routine_name, p.name AS param_name, + p.is_output, TYPE_NAME(p.system_type_id) AS data_type, + p.parameter_id, p.precision, p.scale + FROM sys.parameters p + JOIN sys.objects o ON o.object_id = p.object_id + WHERE o.type IN ('FN', 'IF', 'TF') AND OBJECT_SCHEMA_NAME(p.object_id) = ? + AND p.parameter_id > 0 + ORDER BY OBJECT_NAME(p.object_id), p.parameter_id + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String paramName = rs.getString("param_name"); + boolean isOutput = rs.getBoolean("is_output"); + String dataType = rs.getString("data_type"); + int paramId = rs.getInt("parameter_id"); + int precision = rs.getInt("precision"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("scale"); + boolean scaleNull = rs.wasNull(); + + String cleanName = paramName != null && paramName.startsWith("@") ? paramName.substring(1) + : (paramName != null ? paramName : ""); + + FunctionColumn.ColumnType colType = isOutput ? FunctionColumn.ColumnType.OUT + : FunctionColumn.ColumnType.IN; + JDBCType jdbcType = mapMssqlJdbcType(dataType); + + FunctionColumn col = new FunctionColumnRecord(cleanName, colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), paramId); + + result.computeIfAbsent(routineName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private static JDBCType mapMssqlJdbcType(String dataType) { + if (dataType == null) { + return JDBCType.OTHER; + } + return switch (dataType.toLowerCase()) { + case "int" -> JDBCType.INTEGER; + case "bigint" -> JDBCType.BIGINT; + case "smallint" -> JDBCType.SMALLINT; + case "tinyint" -> JDBCType.TINYINT; + case "bit" -> JDBCType.BIT; + case "decimal", "numeric" -> JDBCType.DECIMAL; + case "float" -> JDBCType.DOUBLE; + case "real" -> JDBCType.REAL; + case "money", "smallmoney" -> JDBCType.DECIMAL; + case "char", "nchar" -> JDBCType.CHAR; + case "varchar", "nvarchar" -> JDBCType.VARCHAR; + case "text", "ntext" -> JDBCType.VARCHAR; + case "date" -> JDBCType.DATE; + case "time" -> JDBCType.TIME; + case "datetime", "datetime2", "smalldatetime" -> JDBCType.TIMESTAMP; + case "datetimeoffset" -> JDBCType.TIMESTAMP_WITH_TIMEZONE; + case "binary" -> JDBCType.BINARY; + case "varbinary" -> JDBCType.VARBINARY; + case "image" -> JDBCType.LONGVARBINARY; + case "uniqueidentifier" -> JDBCType.CHAR; + case "xml" -> JDBCType.SQLXML; + default -> JDBCType.OTHER; + }; + } + + + private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException { + String triggerName = rs.getString("trigger_name"); + String tableName = rs.getString("table_name"); + boolean isInsteadOf = rs.getBoolean("is_instead_of_trigger"); + String definition = rs.getString("definition"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = isInsteadOf ? TriggerTiming.INSTEAD_OF : TriggerTiming.AFTER; + TriggerEvent event = TriggerEvent.INSERT; + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(definition), Optional.ofNullable(definition), Optional.empty()); + } + + + private ImportedKey readImportedKey(ResultSet rs, String schemaName) throws SQLException { + String fkName = rs.getString("fk_name"); + String fkTable = rs.getString("fk_table"); + String fkColumn = rs.getString("fk_column"); + String pkTable = rs.getString("pk_table"); + String pkColumn = rs.getString("pk_column"); + int keySeq = rs.getInt("key_seq"); + String deleteRule = rs.getString("delete_rule"); + String updateRule = rs.getString("update_rule"); + + // FK side + Optional fkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side + Optional pkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapReferentialAction(updateRule), + mapReferentialAction(deleteRule), Optional.empty(), ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("constraint_name"); + String table = rs.getString("table_name"); + String columnName = rs.getString("column_name"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + return connection.getSchema() != null ? connection.getSchema() : "dbo"; + } + + + private static ImportedKey.ReferentialAction mapReferentialAction(String action) { + if (action == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (action.toUpperCase()) { + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET_NULL" -> ImportedKey.ReferentialAction.SET_NULL; + case "SET_DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; + case "NO_ACTION" -> ImportedKey.ReferentialAction.NO_ACTION; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + private static IndexInfoItem.IndexType mapMssqlIndexType(String typeDesc) { + if (typeDesc == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (typeDesc.toUpperCase()) { + case "CLUSTERED" -> IndexInfoItem.IndexType.TABLE_INDEX_CLUSTERED; + case "HEAP" -> IndexInfoItem.IndexType.TABLE_INDEX_STATISTIC; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + @Override + public List getAllUserDefinedTypes(Connection connection, String catalog, String schema) + throws SQLException { + String schemaName = resolveSchema(schema, connection); + String sql = """ + SELECT t.name AS type_name, SCHEMA_NAME(t.schema_id) AS schema_name, + TYPE_NAME(t.system_type_id) AS base_type_name, + t.is_table_type + FROM sys.types t + WHERE t.is_user_defined = 1 AND SCHEMA_NAME(t.schema_id) = ? + ORDER BY t.name + """; + List types = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String typeName = rs.getString("type_name"); + String baseTypeName = rs.getString("base_type_name"); + boolean isTableType = rs.getBoolean("is_table_type"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + String className = isTableType ? "TABLE_TYPE" : "ALIAS"; + JDBCType baseType = isTableType ? JDBCType.STRUCT : mapMssqlJdbcType(baseTypeName); + + types.add(new UserDefinedTypeRecord(new UserDefinedTypeReference(oSchema, typeName), className, + baseType, Optional.empty())); + } + } + } + return List.copyOf(types); + } + + + @Override + public Optional> getAllTablePrivileges(Connection connection, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + // sys.database_permissions exposes GRANT / DENY / GRANT_WITH_GRANT_OPTION, + // which JDBC's INFORMATION_SCHEMA-based view does not differentiate. + // class = 1 → OBJECT, minor_id = 0 → whole table (column-level rows have + // minor_id > 0). + StringBuilder sql = new StringBuilder(""" + SELECT s.name AS schema_name, o.name AS table_name, + USER_NAME(p.grantor_principal_id) AS grantor, + pp.name AS grantee, p.permission_name, p.state_desc + FROM sys.database_permissions p + JOIN sys.objects o ON o.object_id = p.major_id + JOIN sys.schemas s ON s.schema_id = o.schema_id + JOIN sys.database_principals pp ON pp.principal_id = p.grantee_principal_id + WHERE p.class = 1 AND p.minor_id = 0 AND s.name = ? + """); + boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() + && !"%".equals(tableNamePattern); + if (hasTableFilter) { + sql.append(" AND o.name LIKE ?\n"); + } + sql.append("ORDER BY o.name, p.permission_name, pp.name"); + + String schemaName = resolveSchema(schemaPattern, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + if (hasTableFilter) { + ps.setString(2, tableNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + String grantor = rs.getString("grantor"); + String grantee = rs.getString("grantee"); + String permission = rs.getString("permission_name"); + String state = rs.getString("state_desc"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + // Encode DENY / GRANT_WITH_GRANT_OPTION into privilege + isGrantable. + String privilege = "DENY".equalsIgnoreCase(state) ? "DENY " + permission : permission; + String isGrantable = "GRANT_WITH_GRANT_OPTION".equalsIgnoreCase(state) ? "YES" : "NO"; + + result.add(new TablePrivilegeRecord(tableRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.of(isGrantable))); + } + } + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getColumnPrivileges(Connection connection, String catalog, String schema, + String tableName, String columnNamePattern) throws SQLException { + // Column-level permissions in sys.database_permissions have minor_id = + // columns.column_id. + StringBuilder sql = new StringBuilder(""" + SELECT c.name AS column_name, + USER_NAME(p.grantor_principal_id) AS grantor, + pp.name AS grantee, p.permission_name, p.state_desc + FROM sys.database_permissions p + JOIN sys.objects o ON o.object_id = p.major_id + JOIN sys.schemas s ON s.schema_id = o.schema_id + JOIN sys.columns c ON c.object_id = p.major_id AND c.column_id = p.minor_id + JOIN sys.database_principals pp ON pp.principal_id = p.grantee_principal_id + WHERE p.class = 1 AND p.minor_id > 0 AND s.name = ? AND o.name = ? + """); + boolean hasColumnFilter = columnNamePattern != null && !columnNamePattern.isBlank() + && !"%".equals(columnNamePattern); + if (hasColumnFilter) { + sql.append(" AND c.name LIKE ?\n"); + } + sql.append("ORDER BY c.name, p.permission_name, pp.name"); + + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + if (hasColumnFilter) { + ps.setString(3, columnNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + while (rs.next()) { + String columnName = rs.getString("column_name"); + String grantor = rs.getString("grantor"); + String grantee = rs.getString("grantee"); + String permission = rs.getString("permission_name"); + String state = rs.getString("state_desc"); + + String privilege = "DENY".equalsIgnoreCase(state) ? "DENY " + permission : permission; + String isGrantable = "GRANT_WITH_GRANT_OPTION".equalsIgnoreCase(state) ? "YES" : "NO"; + + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + result.add(new ColumnPrivilegeRecord(colRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.of(isGrantable))); + } + } + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllPseudoColumns(Connection connection, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { + // Surface MSSQL pseudo columns: rowversion/timestamp columns + // (system_type_id=189), + // computed columns (is_computed=1), and identity columns that behave like + // implicit row IDs. + StringBuilder sql = new StringBuilder(""" + SELECT s.name AS schema_name, o.name AS table_name, c.name AS column_name, + TYPE_NAME(c.system_type_id) AS type_name, c.max_length, c.precision, c.scale, + c.is_nullable, c.is_computed, c.is_rowguidcol, c.system_type_id + FROM sys.columns c + JOIN sys.objects o ON o.object_id = c.object_id + JOIN sys.schemas s ON s.schema_id = o.schema_id + WHERE (c.is_computed = 1 OR c.system_type_id = 189 OR c.is_rowguidcol = 1) + AND o.type = 'U' + AND s.name = ? + """); + boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() + && !"%".equals(tableNamePattern); + if (hasTableFilter) { + sql.append(" AND o.name LIKE ?\n"); + } + boolean hasColumnFilter = columnNamePattern != null && !columnNamePattern.isBlank() + && !"%".equals(columnNamePattern); + if (hasColumnFilter) { + sql.append(" AND c.name LIKE ?\n"); + } + sql.append("ORDER BY o.name, c.name"); + + String schemaName = resolveSchema(schemaPattern, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + int idx = 1; + ps.setString(idx++, schemaName); + if (hasTableFilter) { + ps.setString(idx++, tableNamePattern); + } + if (hasColumnFilter) { + ps.setString(idx++, columnNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + while (rs.next()) { + String tableName = rs.getString("table_name"); + String columnName = rs.getString("column_name"); + String typeName = rs.getString("type_name"); + int maxLength = rs.getInt("max_length"); + int precision = rs.getInt("precision"); + int scale = rs.getInt("scale"); + boolean nullable = rs.getBoolean("is_nullable"); + boolean computed = rs.getBoolean("is_computed"); + boolean rowguid = rs.getBoolean("is_rowguidcol"); + int systemTypeId = rs.getInt("system_type_id"); + + TableReference tableRef = new TableReference(oSchema, tableName); + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + + String usage = computed ? "COMPUTED" + : systemTypeId == 189 ? "ROWVERSION" : rowguid ? "ROWGUIDCOL" : "HIDDEN"; + + result.add(new PseudoColumnRecord(colRef, mapMssqlJdbcType(typeName), + OptionalInt.of(precision > 0 ? precision : maxLength), OptionalInt.of(scale), + OptionalInt.of(10), Optional.empty(), OptionalInt.empty(), + Optional.of(nullable ? "YES" : "NO"), usage)); + } + } + } + return Optional.of(List.copyOf(result)); + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProviderFactory.java new file mode 100644 index 0000000..ee997b0 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MicrosoftSqlServerMetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The Microsoft SQL Server {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class MicrosoftSqlServerMetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("microsoft sql server"); + } + + @Override + public MetadataProvider createProvider() { + return new MicrosoftSqlServerMetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProvider.java new file mode 100644 index 0000000..8d7d923 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProvider.java @@ -0,0 +1,886 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PartitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The MySql system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class MySqlMetadataProvider implements MetadataProvider { + + + private static final Logger LOGGER = LoggerFactory.getLogger(MySqlMetadataProvider.class); + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, + ACTION_STATEMENT, ACTION_ORIENTATION + FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? + ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, EVENT_OBJECT_TABLE, ACTION_TIMING, EVENT_MANIPULATION, + ACTION_STATEMENT, ACTION_ORIENTATION + FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = ? AND EVENT_OBJECT_TABLE = ? + ORDER BY TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + // MySQL does NOT support sequences + return List.of(); + } + + + @Override + public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + // INFORMATION_SCHEMA.PARTITIONS returns one row per non-partitioned table with + // PARTITION_NAME=NULL, plus one row per (partition, sub-partition) for + // partitioned tables. + // We filter out the PARTITION_NAME IS NULL rows. + String sql = """ + SELECT TABLE_NAME, PARTITION_NAME, SUBPARTITION_NAME, + PARTITION_METHOD, SUBPARTITION_METHOD, + PARTITION_EXPRESSION, SUBPARTITION_EXPRESSION, + PARTITION_DESCRIPTION, + PARTITION_ORDINAL_POSITION, SUBPARTITION_ORDINAL_POSITION, + TABLE_ROWS + FROM information_schema.PARTITIONS + WHERE TABLE_SCHEMA = ? AND PARTITION_NAME IS NOT NULL + ORDER BY TABLE_NAME, PARTITION_ORDINAL_POSITION, SUBPARTITION_ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List partitions = new ArrayList<>(); + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String partitionName = rs.getString("PARTITION_NAME"); + String subPartitionName = rs.getString("SUBPARTITION_NAME"); + boolean isSub = subPartitionName != null; + + String name = isSub ? subPartitionName : partitionName; + String methodStr = rs.getString(isSub ? "SUBPARTITION_METHOD" : "PARTITION_METHOD"); + PartitionMethod method = mapPartitionMethod(methodStr); + String expression = rs.getString(isSub ? "SUBPARTITION_EXPRESSION" : "PARTITION_EXPRESSION"); + String description = rs.getString("PARTITION_DESCRIPTION"); + int ordinal = rs.getInt(isSub ? "SUBPARTITION_ORDINAL_POSITION" : "PARTITION_ORDINAL_POSITION"); + Optional oOrdinal = rs.wasNull() ? Optional.empty() : Optional.of(ordinal); + long rowCount = rs.getLong("TABLE_ROWS"); + Optional oRowCount = rs.wasNull() ? Optional.empty() : Optional.of(rowCount); + + TableReference tableRef = new TableReference(oSchema, tableName); + partitions.add(new PartitionRecord(name, tableRef, oOrdinal, method, + expression == null ? Optional.empty() : Optional.of(expression), + description == null ? Optional.empty() : Optional.of(description), oRowCount, + isSub ? Optional.of(partitionName) : Optional.empty(), Optional.empty(), Optional.empty())); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not load partitions from INFORMATION_SCHEMA.PARTITIONS: {}", e.getMessage()); + return List.of(); + } + return List.copyOf(partitions); + } + + + private static PartitionMethod mapPartitionMethod(String raw) { + if (raw == null) { + return PartitionMethod.OTHER; + } + return switch (raw.toUpperCase(java.util.Locale.ROOT)) { + case "RANGE", "RANGE COLUMNS" -> PartitionMethod.RANGE; + case "LIST", "LIST COLUMNS" -> PartitionMethod.LIST; + case "HASH" -> PartitionMethod.HASH; + case "KEY" -> PartitionMethod.KEY; + case "LINEAR HASH" -> PartitionMethod.LINEAR_HASH; + case "LINEAR KEY" -> PartitionMethod.LINEAR_KEY; + default -> PartitionMethod.OTHER; + }; + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT cc.CONSTRAINT_NAME, cc.CHECK_CLAUSE, tc.TABLE_NAME + FROM information_schema.CHECK_CONSTRAINTS cc + JOIN information_schema.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE cc.CONSTRAINT_SCHEMA = ? AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY tc.TABLE_NAME, cc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String checkClause = rs.getString("CHECK_CLAUSE"); + String tableName = rs.getString("TABLE_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } catch (SQLException e) { + // CHECK_CONSTRAINTS is only available in MySQL 8.0.16+ + LOGGER.debug("Could not query CHECK_CONSTRAINTS (requires MySQL 8.0.16+)", e); + } + return List.copyOf(constraints); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT cc.CONSTRAINT_NAME, cc.CHECK_CLAUSE, tc.TABLE_NAME + FROM information_schema.CHECK_CONSTRAINTS cc + JOIN information_schema.TABLE_CONSTRAINTS tc + ON cc.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND cc.CONSTRAINT_NAME = tc.CONSTRAINT_NAME + WHERE cc.CONSTRAINT_SCHEMA = ? AND tc.TABLE_NAME = ? AND tc.CONSTRAINT_TYPE = 'CHECK' + ORDER BY cc.CONSTRAINT_NAME + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String checkClause = rs.getString("CHECK_CLAUSE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } catch (SQLException e) { + // CHECK_CONSTRAINTS is only available in MySQL 8.0.16+ + LOGGER.debug("Could not query CHECK_CONSTRAINTS (requires MySQL 8.0.16+)", e); + } + return List.copyOf(constraints); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME, tc.TABLE_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'UNIQUE' AND tc.TABLE_SCHEMA = ? AND tc.TABLE_NAME = ? + ORDER BY tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.COLUMN_NAME, kcu.ORDINAL_POSITION + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY tc.TABLE_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT tc.CONSTRAINT_NAME AS FK_NAME, kcu.TABLE_NAME AS FK_TABLE, kcu.COLUMN_NAME AS FK_COLUMN, + kcu.REFERENCED_TABLE_NAME AS PK_TABLE, kcu.REFERENCED_COLUMN_NAME AS PK_COLUMN, + kcu.ORDINAL_POSITION AS KEY_SEQ, rc.DELETE_RULE, rc.UPDATE_RULE + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + JOIN information_schema.REFERENTIAL_CONSTRAINTS rc + ON tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY' AND tc.TABLE_SCHEMA = ? + ORDER BY kcu.TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: find FKs that reference tables in this + // schema. + String sql = """ + SELECT tc.CONSTRAINT_NAME AS FK_NAME, kcu.TABLE_NAME AS FK_TABLE, kcu.COLUMN_NAME AS FK_COLUMN, + kcu.REFERENCED_TABLE_NAME AS PK_TABLE, kcu.REFERENCED_COLUMN_NAME AS PK_COLUMN, + kcu.ORDINAL_POSITION AS KEY_SEQ, rc.DELETE_RULE, rc.UPDATE_RULE + FROM information_schema.TABLE_CONSTRAINTS tc + JOIN information_schema.KEY_COLUMN_USAGE kcu + ON tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_NAME = kcu.TABLE_NAME + JOIN information_schema.REFERENTIAL_CONSTRAINTS rc + ON tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA AND tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME + WHERE tc.CONSTRAINT_TYPE = 'FOREIGN KEY' AND kcu.REFERENCED_TABLE_SCHEMA = ? + ORDER BY kcu.REFERENCED_TABLE_NAME, tc.CONSTRAINT_NAME, kcu.ORDINAL_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT TABLE_NAME, INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE, INDEX_TYPE, CARDINALITY + FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX + """; + String schemaName = resolveSchema(schema, connection); + // Group by TABLE_NAME then by INDEX_NAME + Map>> tableIndexMap = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String indexName = rs.getString("INDEX_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + int seqInIndex = rs.getInt("SEQ_IN_INDEX"); + boolean nonUnique = rs.getBoolean("NON_UNIQUE"); + String indexType = rs.getString("INDEX_TYPE"); + long cardinality = rs.getLong("CARDINALITY"); + + IndexInfoItem.IndexType mappedType = mapMySqlIndexType(indexType); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, + seqInIndex, Optional.empty(), // MySQL STATISTICS doesn't expose ASC/DESC + cardinality, 0L, // pages not available + Optional.empty(), // filter condition + !nonUnique); + + tableIndexMap.computeIfAbsent(tableName, k -> new LinkedHashMap<>()) + .computeIfAbsent(indexName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry>> tableEntry : tableIndexMap.entrySet()) { + List allItems = new ArrayList<>(); + for (List indexItems : tableEntry.getValue().values()) { + allItems.addAll(indexItems); + } + result.add(new IndexInfoRecord(tableRefs.get(tableEntry.getKey()), List.copyOf(allItems))); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ? + ORDER BY TABLE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String viewDef = rs.getString("VIEW_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, tableName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewDef), Optional.empty())); + } + } + } + return List.copyOf(views); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadProcedureColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_COMMENT, ROUTINE_DEFINITION + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'PROCEDURE' + ORDER BY ROUTINE_NAME + """; + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("ROUTINE_COMMENT"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + Optional fullDef = showCreateRoutine(connection, "PROCEDURE", schemaName, routineName); + + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName, specificName), + Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), fullDef, Optional.empty())); + } + } + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadFunctionColumns(connection, schemaName); + + String sql = """ + SELECT ROUTINE_NAME, SPECIFIC_NAME, ROUTINE_COMMENT, ROUTINE_DEFINITION + FROM information_schema.ROUTINES + WHERE ROUTINE_SCHEMA = ? AND ROUTINE_TYPE = 'FUNCTION' + ORDER BY ROUTINE_NAME + """; + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("ROUTINE_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + String remarks = rs.getString("ROUTINE_COMMENT"); + String body = rs.getString("ROUTINE_DEFINITION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(specificName, List.of()); + + Optional fullDef = showCreateRoutine(connection, "FUNCTION", schemaName, routineName); + + functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName, specificName), + Function.FunctionType.NO_TABLE, Optional.ofNullable(remarks), cols, + Optional.ofNullable(body), fullDef, Optional.empty())); + } + } + } + return List.copyOf(functions); + } + + + private Optional showCreateRoutine(Connection connection, String kind, String schemaName, String name) { + String quotedSchema = "`" + schemaName.replace("`", "``") + "`"; + String quotedName = "`" + name.replace("`", "``") + "`"; + String sql = "SHOW CREATE " + kind + " " + quotedSchema + "." + quotedName; + try (PreparedStatement ps = connection.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String col = "Create " + kind.charAt(0) + kind.substring(1).toLowerCase(); + String def = rs.getString(col); + return Optional.ofNullable(def); + } + } catch (SQLException e) { + LOGGER.debug("Could not retrieve DDL for {} {}.{}: {}", kind, schemaName, name, e.getMessage()); + } + return Optional.empty(); + } + + + private Map> loadProcedureColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM information_schema.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? AND ORDINAL_POSITION > 0 + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + + ProcedureColumn.ColumnType colType = mapProcedureColumnType(paramMode); + JDBCType jdbcType = mapMySqlJdbcType(dataType); + + ProcedureColumn col = new ProcedureColumnRecord(paramName != null ? paramName : "", colType, + jdbcType, dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.empty(), ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private Map> loadFunctionColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT SPECIFIC_NAME, PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE, + ORDINAL_POSITION, NUMERIC_PRECISION, NUMERIC_SCALE + FROM information_schema.PARAMETERS + WHERE SPECIFIC_SCHEMA = ? + ORDER BY SPECIFIC_NAME, ORDINAL_POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String specificName = rs.getString("SPECIFIC_NAME"); + String paramName = rs.getString("PARAMETER_NAME"); + String paramMode = rs.getString("PARAMETER_MODE"); + String dataType = rs.getString("DATA_TYPE"); + int ordinalPosition = rs.getInt("ORDINAL_POSITION"); + int precision = rs.getInt("NUMERIC_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("NUMERIC_SCALE"); + boolean scaleNull = rs.wasNull(); + + FunctionColumn.ColumnType colType = mapFunctionColumnType(paramMode); + JDBCType jdbcType = mapMySqlJdbcType(dataType); + + FunctionColumn col = new FunctionColumnRecord(paramName != null ? paramName : "", colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), ordinalPosition); + + result.computeIfAbsent(specificName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private static ProcedureColumn.ColumnType mapProcedureColumnType(String mode) { + if (mode == null) { + return ProcedureColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> ProcedureColumn.ColumnType.IN; + case "OUT" -> ProcedureColumn.ColumnType.OUT; + case "INOUT" -> ProcedureColumn.ColumnType.INOUT; + default -> ProcedureColumn.ColumnType.UNKNOWN; + }; + } + + + private static FunctionColumn.ColumnType mapFunctionColumnType(String mode) { + if (mode == null) { + return FunctionColumn.ColumnType.UNKNOWN; + } + return switch (mode.toUpperCase()) { + case "IN" -> FunctionColumn.ColumnType.IN; + case "OUT" -> FunctionColumn.ColumnType.OUT; + case "INOUT" -> FunctionColumn.ColumnType.INOUT; + default -> FunctionColumn.ColumnType.UNKNOWN; + }; + } + + + private static JDBCType mapMySqlJdbcType(String dataType) { + if (dataType == null) { + return JDBCType.OTHER; + } + try { + return JDBCType.valueOf(dataType.toUpperCase().replace(" ", "_")); + } catch (IllegalArgumentException e) { + return switch (dataType.toUpperCase()) { + case "INT" -> JDBCType.INTEGER; + case "MEDIUMINT" -> JDBCType.INTEGER; + case "TINYINT" -> JDBCType.TINYINT; + case "BOOL", "BOOLEAN" -> JDBCType.BOOLEAN; + case "TEXT", "MEDIUMTEXT", "LONGTEXT", "TINYTEXT" -> JDBCType.VARCHAR; + case "DATETIME" -> JDBCType.TIMESTAMP; + case "ENUM", "SET", "JSON", "GEOMETRY" -> JDBCType.OTHER; + default -> JDBCType.OTHER; + }; + } + } + + + private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException { + String triggerName = rs.getString("TRIGGER_NAME"); + String tableName = rs.getString("EVENT_OBJECT_TABLE"); + String actionTiming = rs.getString("ACTION_TIMING"); + String eventManipulation = rs.getString("EVENT_MANIPULATION"); + String actionStatement = rs.getString("ACTION_STATEMENT"); + String actionOrientation = rs.getString("ACTION_ORIENTATION"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = mapTriggerTiming(actionTiming); + TriggerEvent event = mapTriggerEvent(eventManipulation); + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(actionStatement), Optional.empty(), Optional.ofNullable(actionOrientation)); + } + + + private ImportedKey readImportedKey(ResultSet rs, String schemaName) throws SQLException { + String fkName = rs.getString("FK_NAME"); + String fkTable = rs.getString("FK_TABLE"); + String fkColumn = rs.getString("FK_COLUMN"); + String pkTable = rs.getString("PK_TABLE"); + String pkColumn = rs.getString("PK_COLUMN"); + int keySeq = rs.getInt("KEY_SEQ"); + String deleteRule = rs.getString("DELETE_RULE"); + String updateRule = rs.getString("UPDATE_RULE"); + + // FK side + Optional fkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side + Optional pkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapReferentialAction(updateRule), + mapReferentialAction(deleteRule), Optional.empty(), ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + // MySQL uses catalog as database name + String catalog = connection.getCatalog(); + if (catalog != null) { + return catalog; + } + return connection.getSchema() != null ? connection.getSchema() : ""; + } + + + private static TriggerTiming mapTriggerTiming(String timing) { + if (timing == null) { + return TriggerTiming.AFTER; + } + return switch (timing.toUpperCase()) { + case "BEFORE" -> TriggerTiming.BEFORE; + default -> TriggerTiming.AFTER; + }; + } + + + private static TriggerEvent mapTriggerEvent(String event) { + if (event == null) { + return TriggerEvent.INSERT; + } + return switch (event.toUpperCase()) { + case "UPDATE" -> TriggerEvent.UPDATE; + case "DELETE" -> TriggerEvent.DELETE; + default -> TriggerEvent.INSERT; + }; + } + + + private static ImportedKey.ReferentialAction mapReferentialAction(String action) { + if (action == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (action.toUpperCase()) { + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; + case "SET DEFAULT" -> ImportedKey.ReferentialAction.SET_DEFAULT; + case "RESTRICT" -> ImportedKey.ReferentialAction.RESTRICT; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + private static IndexInfoItem.IndexType mapMySqlIndexType(String indexType) { + if (indexType == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (indexType.toUpperCase()) { + case "HASH" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + case "BTREE", "FULLTEXT", "SPATIAL" -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProviderFactory.java new file mode 100644 index 0000000..58e88b3 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/MySqlMetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The MySQL (excluding MariaDB, which reports its own product name on the MariaDB driver) {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class MySqlMetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("mysql") && !lower.contains("mariadb"); + } + + @Override + public MetadataProvider createProvider() { + return new MySqlMetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java new file mode 100644 index 0000000..8ec33a2 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProvider.java @@ -0,0 +1,1501 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedTypeReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnPrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.MaterializedViewRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PartitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PseudoColumnRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UserDefinedTypeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The Oracle system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class OracleMetadataProvider implements MetadataProvider { + + + private static final Logger LOGGER = LoggerFactory.getLogger(OracleMetadataProvider.class); + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TRIGGER_BODY + FROM ALL_TRIGGERS WHERE OWNER = ? ORDER BY TABLE_NAME, TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT TRIGGER_NAME, TABLE_NAME, TRIGGER_TYPE, TRIGGERING_EVENT, TRIGGER_BODY + FROM ALL_TRIGGERS WHERE OWNER = ? AND TABLE_NAME = ? + ORDER BY TRIGGER_NAME + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT SEQUENCE_NAME, MIN_VALUE, MAX_VALUE, INCREMENT_BY, CYCLE_FLAG, CACHE_SIZE, LAST_NUMBER + FROM ALL_SEQUENCES WHERE SEQUENCE_OWNER = ? ORDER BY SEQUENCE_NAME + """; + String schemaName = resolveSchema(schema, connection); + List sequences = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("SEQUENCE_NAME"); + long minValue = rs.getLong("MIN_VALUE"); + Optional oMinValue = rs.wasNull() ? Optional.empty() : Optional.of(minValue); + long maxValue = rs.getLong("MAX_VALUE"); + Optional oMaxValue = rs.wasNull() ? Optional.empty() : Optional.of(maxValue); + long incrementBy = rs.getLong("INCREMENT_BY"); + String cycleFlag = rs.getString("CYCLE_FLAG"); + boolean cycle = "Y".equalsIgnoreCase(cycleFlag); + long cacheSize = rs.getLong("CACHE_SIZE"); + Optional oCacheSize = rs.wasNull() ? Optional.empty() : Optional.of(cacheSize); + long lastNumber = rs.getLong("LAST_NUMBER"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + sequences.add(new SequenceRecord(new SequenceReference(oSchema, name), lastNumber, incrementBy, + oMinValue, oMaxValue, cycle, oCacheSize, Optional.empty())); + } + } + } + return List.copyOf(sequences); + } + + + @Override + public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + // Oracle exposes partitioning via ALL_TAB_PARTITIONS / ALL_TAB_SUBPARTITIONS + // plus + // ALL_PART_TABLES (carrying the partitioning_type, sub-partitioning_type, + // partition_count) + // and ALL_PART_KEY_COLUMNS (the partition key columns). + // We emit one Partition per ALL_TAB_PARTITIONS row plus one per + // ALL_TAB_SUBPARTITIONS row. + String partitionSql = """ + SELECT atp.TABLE_NAME, atp.PARTITION_NAME, atp.PARTITION_POSITION, + atp.HIGH_VALUE, atp.NUM_ROWS, + apt.PARTITIONING_TYPE, apt.SUBPARTITIONING_TYPE, + (SELECT LISTAGG(akc.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY akc.COLUMN_POSITION) + FROM ALL_PART_KEY_COLUMNS akc + WHERE akc.OWNER = atp.TABLE_OWNER AND akc.NAME = atp.TABLE_NAME) AS PART_KEY, + (SELECT LISTAGG(askc.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY askc.COLUMN_POSITION) + FROM ALL_SUBPART_KEY_COLUMNS askc + WHERE askc.OWNER = atp.TABLE_OWNER AND askc.NAME = atp.TABLE_NAME) AS SUBPART_KEY + FROM ALL_TAB_PARTITIONS atp + JOIN ALL_PART_TABLES apt + ON apt.OWNER = atp.TABLE_OWNER AND apt.TABLE_NAME = atp.TABLE_NAME + WHERE atp.TABLE_OWNER = ? + ORDER BY atp.TABLE_NAME, atp.PARTITION_POSITION + """; + String subPartitionSql = """ + SELECT atsp.TABLE_NAME, atsp.PARTITION_NAME, atsp.SUBPARTITION_NAME, + atsp.SUBPARTITION_POSITION, atsp.HIGH_VALUE, atsp.NUM_ROWS, + apt.SUBPARTITIONING_TYPE, + (SELECT LISTAGG(askc.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY askc.COLUMN_POSITION) + FROM ALL_SUBPART_KEY_COLUMNS askc + WHERE askc.OWNER = atsp.TABLE_OWNER AND askc.NAME = atsp.TABLE_NAME) AS SUBPART_KEY + FROM ALL_TAB_SUBPARTITIONS atsp + JOIN ALL_PART_TABLES apt + ON apt.OWNER = atsp.TABLE_OWNER AND apt.TABLE_NAME = atsp.TABLE_NAME + WHERE atsp.TABLE_OWNER = ? + ORDER BY atsp.TABLE_NAME, atsp.PARTITION_NAME, atsp.SUBPARTITION_POSITION + """; + String schemaName = resolveSchema(schema, connection); + List partitions = new ArrayList<>(); + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + try (PreparedStatement ps = connection.prepareStatement(partitionSql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String name = rs.getString("PARTITION_NAME"); + int ordinal = rs.getInt("PARTITION_POSITION"); + Optional oOrdinal = rs.wasNull() ? Optional.empty() : Optional.of(ordinal); + String highValue = rs.getString("HIGH_VALUE"); + long rowCount = rs.getLong("NUM_ROWS"); + Optional oRowCount = rs.wasNull() ? Optional.empty() : Optional.of(rowCount); + PartitionMethod method = mapOraclePartitioningType(rs.getString("PARTITIONING_TYPE")); + PartitionMethod subMethod = mapOraclePartitioningType(rs.getString("SUBPARTITIONING_TYPE")); + String partKey = rs.getString("PART_KEY"); + String subPartKey = rs.getString("SUBPART_KEY"); + + TableReference tableRef = new TableReference(oSchema, tableName); + partitions.add(new PartitionRecord(name, tableRef, oOrdinal, method, + partKey == null ? Optional.empty() : Optional.of(partKey), + highValue == null ? Optional.empty() : Optional.of(highValue), oRowCount, Optional.empty(), + subMethod == PartitionMethod.OTHER ? Optional.empty() : Optional.of(subMethod), + subPartKey == null ? Optional.empty() : Optional.of(subPartKey))); + } + } + } catch (SQLException e) { + return List.of(); + } + + try (PreparedStatement ps = connection.prepareStatement(subPartitionSql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String parentName = rs.getString("PARTITION_NAME"); + String subName = rs.getString("SUBPARTITION_NAME"); + int ordinal = rs.getInt("SUBPARTITION_POSITION"); + Optional oOrdinal = rs.wasNull() ? Optional.empty() : Optional.of(ordinal); + String highValue = rs.getString("HIGH_VALUE"); + long rowCount = rs.getLong("NUM_ROWS"); + Optional oRowCount = rs.wasNull() ? Optional.empty() : Optional.of(rowCount); + PartitionMethod method = mapOraclePartitioningType(rs.getString("SUBPARTITIONING_TYPE")); + String subPartKey = rs.getString("SUBPART_KEY"); + + TableReference tableRef = new TableReference(oSchema, tableName); + partitions.add(new PartitionRecord(subName, tableRef, oOrdinal, method, + subPartKey == null ? Optional.empty() : Optional.of(subPartKey), + highValue == null ? Optional.empty() : Optional.of(highValue), oRowCount, + Optional.of(parentName), Optional.empty(), Optional.empty())); + } + } + } catch (SQLException e) { + // Ignore — sub-partitions are optional; the partition list above is still + // useful. + } + + return List.copyOf(partitions); + } + + + private static PartitionMethod mapOraclePartitioningType(String type) { + if (type == null) { + return PartitionMethod.OTHER; + } + return switch (type.toUpperCase(java.util.Locale.ROOT)) { + case "RANGE" -> PartitionMethod.RANGE; + case "LIST" -> PartitionMethod.LIST; + case "HASH" -> PartitionMethod.HASH; + default -> PartitionMethod.OTHER; + }; + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String schemaName = resolveSchema(schema, connection); + return readCheckConstraints(connection, schemaName, null); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + return readCheckConstraints(connection, schemaName, tableName); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, cc.COLUMN_NAME, cc.POSITION + FROM ALL_CONSTRAINTS c + JOIN ALL_CONS_COLUMNS cc ON c.OWNER = cc.OWNER AND c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME + WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'U' + ORDER BY c.TABLE_NAME, c.CONSTRAINT_NAME, cc.POSITION + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, cc.COLUMN_NAME, cc.POSITION + FROM ALL_CONSTRAINTS c + JOIN ALL_CONS_COLUMNS cc ON c.OWNER = cc.OWNER AND c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME + WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'U' AND c.TABLE_NAME = ? + ORDER BY c.CONSTRAINT_NAME, cc.POSITION + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, cc.COLUMN_NAME, cc.POSITION + FROM ALL_CONSTRAINTS c + JOIN ALL_CONS_COLUMNS cc ON c.OWNER = cc.OWNER AND c.CONSTRAINT_NAME = cc.CONSTRAINT_NAME + WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'P' + ORDER BY c.TABLE_NAME, cc.POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT fk.CONSTRAINT_NAME AS FK_NAME, fk.TABLE_NAME AS FK_TABLE, + fk_col.COLUMN_NAME AS FK_COLUMN, pk.TABLE_NAME AS PK_TABLE, + pk_col.COLUMN_NAME AS PK_COLUMN, fk_col.POSITION AS KEY_SEQ, + fk.DELETE_RULE, pk.CONSTRAINT_NAME AS PK_NAME + FROM ALL_CONSTRAINTS fk + JOIN ALL_CONS_COLUMNS fk_col ON fk.OWNER = fk_col.OWNER AND fk.CONSTRAINT_NAME = fk_col.CONSTRAINT_NAME + JOIN ALL_CONSTRAINTS pk ON fk.R_OWNER = pk.OWNER AND fk.R_CONSTRAINT_NAME = pk.CONSTRAINT_NAME + JOIN ALL_CONS_COLUMNS pk_col ON pk.OWNER = pk_col.OWNER AND pk.CONSTRAINT_NAME = pk_col.CONSTRAINT_NAME + AND fk_col.POSITION = pk_col.POSITION + WHERE fk.OWNER = ? AND fk.CONSTRAINT_TYPE = 'R' + ORDER BY fk.TABLE_NAME, fk.CONSTRAINT_NAME, fk_col.POSITION + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: FK entries whose referenced (R_OWNER) side + // is the given schema. + String sql = """ + SELECT fk.CONSTRAINT_NAME AS FK_NAME, fk.TABLE_NAME AS FK_TABLE, + fk_col.COLUMN_NAME AS FK_COLUMN, pk.TABLE_NAME AS PK_TABLE, + pk_col.COLUMN_NAME AS PK_COLUMN, fk_col.POSITION AS KEY_SEQ, + fk.DELETE_RULE, pk.CONSTRAINT_NAME AS PK_NAME + FROM ALL_CONSTRAINTS fk + JOIN ALL_CONS_COLUMNS fk_col ON fk.OWNER = fk_col.OWNER AND fk.CONSTRAINT_NAME = fk_col.CONSTRAINT_NAME + JOIN ALL_CONSTRAINTS pk ON fk.R_OWNER = pk.OWNER AND fk.R_CONSTRAINT_NAME = pk.CONSTRAINT_NAME + JOIN ALL_CONS_COLUMNS pk_col ON pk.OWNER = pk_col.OWNER AND pk.CONSTRAINT_NAME = pk_col.CONSTRAINT_NAME + AND fk_col.POSITION = pk_col.POSITION + WHERE fk.R_OWNER = ? AND fk.CONSTRAINT_TYPE = 'R' + ORDER BY pk.TABLE_NAME, fk.CONSTRAINT_NAME, fk_col.POSITION + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT i.INDEX_NAME, i.INDEX_TYPE, i.TABLE_NAME, i.UNIQUENESS, + ic.COLUMN_NAME, ic.COLUMN_POSITION, ic.DESCEND + FROM ALL_INDEXES i + JOIN ALL_IND_COLUMNS ic ON i.OWNER = ic.INDEX_OWNER AND i.INDEX_NAME = ic.INDEX_NAME + WHERE i.TABLE_OWNER = ? ORDER BY i.TABLE_NAME, i.INDEX_NAME, ic.COLUMN_POSITION + """; + String schemaName = resolveSchema(schema, connection); + Map> tableIndexes = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String indexName = rs.getString("INDEX_NAME"); + String indexType = rs.getString("INDEX_TYPE"); + String uniqueness = rs.getString("UNIQUENESS"); + String columnName = rs.getString("COLUMN_NAME"); + int columnPosition = rs.getInt("COLUMN_POSITION"); + String descend = rs.getString("DESCEND"); + + IndexInfoItem.IndexType mappedType = mapOracleIndexType(indexType); + boolean isUnique = "UNIQUE".equalsIgnoreCase(uniqueness); + Optional ascending = descend != null ? Optional.of("ASC".equalsIgnoreCase(descend)) + : Optional.empty(); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, + columnPosition, ascending, 0L, 0L, Optional.empty(), isUnique); + + tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry : tableIndexes.entrySet()) { + result.add(new IndexInfoRecord(tableRefs.get(entry.getKey()), List.copyOf(entry.getValue()))); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + + // Try TEXT_VC first (available in Oracle 12.2+) + try { + String sql = """ + SELECT VIEW_NAME, TEXT_VC FROM ALL_VIEWS WHERE OWNER = ? ORDER BY VIEW_NAME + """; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String viewName = rs.getString("VIEW_NAME"); + String viewBody = rs.getString("TEXT_VC"); + + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, viewName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewBody), Optional.empty())); + } + } + } + return List.copyOf(views); + } catch (SQLException e) { + LOGGER.debug("TEXT_VC column not available, falling back to TEXT column", e); + } + + // Fall back to TEXT column (LONG type, may have issues) + try { + String sqlFallback = """ + SELECT VIEW_NAME, TEXT FROM ALL_VIEWS WHERE OWNER = ? ORDER BY VIEW_NAME + """; + try (PreparedStatement ps = connection.prepareStatement(sqlFallback)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String viewName = rs.getString("VIEW_NAME"); + String viewBody; + try { + viewBody = rs.getString("TEXT"); + } catch (SQLException ex) { + LOGGER.debug("Could not read TEXT column for view {}", viewName, ex); + viewBody = null; + } + + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, viewName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewBody), Optional.empty())); + } + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read view definitions from ALL_VIEWS", e); + } + return List.copyOf(views); + } + + + @Override + public List getAllMaterializedViews(Connection connection, String catalog, String schema) + throws SQLException { + // ALL_MVIEWS drives the list. Oracle also exposes REFRESH_MODE + // ('DEMAND','COMMIT','NEVER') + // and REFRESH_METHOD ('COMPLETE','FAST','FORCE','NEVER'); we report + // REFRESH_METHOD since + // that is what users normally think of when saying "refresh mode". + String sql = """ + SELECT MVIEW_NAME, QUERY, REFRESH_METHOD, LAST_REFRESH_DATE + FROM ALL_MVIEWS + WHERE OWNER = ? + ORDER BY MVIEW_NAME + """; + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("MVIEW_NAME"); + String body = rs.getString("QUERY"); + String refreshMethod = rs.getString("REFRESH_METHOD"); + Timestamp lastRefresh = rs.getTimestamp("LAST_REFRESH_DATE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, name, "MATERIALIZED VIEW"); + + // DBMS_METADATA.GET_DDL is accurate but slow; skip unless explicitly needed. + Optional fullDef = Optional.empty(); + Optional lastRef = lastRefresh == null ? Optional.empty() + : Optional.of(lastRefresh.toInstant()); + + result.add(new MaterializedViewRecord(viewRef, Optional.ofNullable(body), fullDef, + Optional.ofNullable(refreshMethod), lastRef)); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read materialized views from ALL_MVIEWS: {}", e.getMessage()); + return List.of(); + } + return List.copyOf(result); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadOracleProcedureColumns(connection, schemaName); + Map sourceMap = loadOracleSources(connection, schemaName, "PROCEDURE"); + Map lastDdlMap = loadOracleLastDdlTimes(connection, schemaName, "PROCEDURE"); + + String sql = """ + SELECT OBJECT_NAME, PROCEDURE_NAME + FROM ALL_PROCEDURES + WHERE OWNER = ? AND OBJECT_TYPE = 'PROCEDURE' + ORDER BY OBJECT_NAME + """; + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String objectName = rs.getString("OBJECT_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(objectName, List.of()); + Optional body = Optional.ofNullable(sourceMap.get(objectName)); + Optional fullDef = getMetadataDdl(connection, "PROCEDURE", schemaName, objectName); + Optional lastMod = Optional.ofNullable(lastDdlMap.get(objectName)); + + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, objectName), + Procedure.ProcedureType.NO_RESULT, Optional.empty(), cols, body, fullDef, lastMod)); + } + } + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map> paramMap = loadOracleFunctionColumns(connection, schemaName); + Map sourceMap = loadOracleSources(connection, schemaName, "FUNCTION"); + Map lastDdlMap = loadOracleLastDdlTimes(connection, schemaName, "FUNCTION"); + + String sql = """ + SELECT OBJECT_NAME + FROM ALL_PROCEDURES + WHERE OWNER = ? AND OBJECT_TYPE = 'FUNCTION' + ORDER BY OBJECT_NAME + """; + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String objectName = rs.getString("OBJECT_NAME"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + List cols = paramMap.getOrDefault(objectName, List.of()); + Optional body = Optional.ofNullable(sourceMap.get(objectName)); + Optional fullDef = getMetadataDdl(connection, "FUNCTION", schemaName, objectName); + Optional lastMod = Optional.ofNullable(lastDdlMap.get(objectName)); + + functions.add(new FunctionRecord(new FunctionReference(oSchema, objectName), + Function.FunctionType.NO_TABLE, Optional.empty(), cols, body, fullDef, lastMod)); + } + } + } + return List.copyOf(functions); + } + + + private Map loadOracleLastDdlTimes(Connection connection, String schemaName, String objectType) { + // ALL_OBJECTS.LAST_DDL_TIME is the canonical source for "when was this PL/SQL + // object + // last changed"; a single bulk query covers the whole schema. + String sql = """ + SELECT OBJECT_NAME, LAST_DDL_TIME + FROM ALL_OBJECTS + WHERE OWNER = ? AND OBJECT_TYPE = ? + """; + Map result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, objectType); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("OBJECT_NAME"); + Timestamp ts = rs.getTimestamp("LAST_DDL_TIME"); + if (ts != null) { + result.put(name, ts.toInstant()); + } + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read LAST_DDL_TIME for {} from ALL_OBJECTS: {}", objectType, e.getMessage()); + return Map.of(); + } + return result; + } + + + private Map loadOracleSources(Connection connection, String schemaName, String type) { + // Aggregate source lines per object from ALL_SOURCE. + String sql = """ + SELECT NAME, TEXT + FROM ALL_SOURCE + WHERE OWNER = ? AND TYPE = ? + ORDER BY NAME, LINE + """; + Map byName = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, type); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("NAME"); + String text = rs.getString("TEXT"); + byName.computeIfAbsent(name, k -> new StringBuilder()).append(text == null ? "" : text); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read ALL_SOURCE for type {}: {}", type, e.getMessage()); + return Map.of(); + } + Map result = new LinkedHashMap<>(); + byName.forEach((k, v) -> result.put(k, v.toString())); + return result; + } + + + private Optional getMetadataDdl(Connection connection, String objectType, String schemaName, + String objectName) { + // DBMS_METADATA.GET_DDL returns a CLOB with the full CREATE statement. + String sql = "SELECT DBMS_METADATA.GET_DDL(?, ?, ?) FROM DUAL"; + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, objectType); + ps.setString(2, objectName); + ps.setString(3, schemaName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + String ddl = rs.getString(1); + return Optional.ofNullable(ddl); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not retrieve DDL for {} {}.{}: {}", objectType, schemaName, objectName, e.getMessage()); + } + return Optional.empty(); + } + + + private Map> loadOracleProcedureColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT a.OBJECT_NAME, a.ARGUMENT_NAME, a.IN_OUT, a.DATA_TYPE, + a.POSITION, a.DATA_PRECISION, a.DATA_SCALE + FROM ALL_ARGUMENTS a + JOIN ALL_PROCEDURES p ON a.OBJECT_ID = p.OBJECT_ID AND a.SUBPROGRAM_ID = p.SUBPROGRAM_ID + WHERE a.OWNER = ? AND p.OBJECT_TYPE = 'PROCEDURE' AND a.POSITION > 0 + ORDER BY a.OBJECT_NAME, a.POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String objectName = rs.getString("OBJECT_NAME"); + String argName = rs.getString("ARGUMENT_NAME"); + String inOut = rs.getString("IN_OUT"); + String dataType = rs.getString("DATA_TYPE"); + int position = rs.getInt("POSITION"); + int precision = rs.getInt("DATA_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("DATA_SCALE"); + boolean scaleNull = rs.wasNull(); + + ProcedureColumn.ColumnType colType = mapOracleProcColumnType(inOut); + JDBCType jdbcType = mapOracleJdbcType(dataType); + + ProcedureColumn col = new ProcedureColumnRecord(argName != null ? argName : "", colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + ProcedureColumn.Nullability.UNKNOWN, Optional.empty(), Optional.empty(), position); + + result.computeIfAbsent(objectName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private Map> loadOracleFunctionColumns(Connection connection, String schemaName) + throws SQLException { + String sql = """ + SELECT a.OBJECT_NAME, a.ARGUMENT_NAME, a.IN_OUT, a.DATA_TYPE, + a.POSITION, a.DATA_PRECISION, a.DATA_SCALE + FROM ALL_ARGUMENTS a + JOIN ALL_PROCEDURES p ON a.OBJECT_ID = p.OBJECT_ID AND a.SUBPROGRAM_ID = p.SUBPROGRAM_ID + WHERE a.OWNER = ? AND p.OBJECT_TYPE = 'FUNCTION' + ORDER BY a.OBJECT_NAME, a.POSITION + """; + Map> result = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String objectName = rs.getString("OBJECT_NAME"); + String argName = rs.getString("ARGUMENT_NAME"); + String inOut = rs.getString("IN_OUT"); + String dataType = rs.getString("DATA_TYPE"); + int position = rs.getInt("POSITION"); + int precision = rs.getInt("DATA_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("DATA_SCALE"); + boolean scaleNull = rs.wasNull(); + + FunctionColumn.ColumnType colType = mapOracleFuncColumnType(inOut); + JDBCType jdbcType = mapOracleJdbcType(dataType); + + FunctionColumn col = new FunctionColumnRecord(argName != null ? argName : "", colType, jdbcType, + dataType != null ? dataType : "", + precisionNull ? OptionalInt.empty() : OptionalInt.of(precision), + scaleNull ? OptionalInt.empty() : OptionalInt.of(scale), OptionalInt.of(10), + FunctionColumn.Nullability.UNKNOWN, Optional.empty(), OptionalInt.empty(), position); + + result.computeIfAbsent(objectName, k -> new ArrayList<>()).add(col); + } + } + } + return result; + } + + + private static ProcedureColumn.ColumnType mapOracleProcColumnType(String inOut) { + if (inOut == null) { + return ProcedureColumn.ColumnType.UNKNOWN; + } + return switch (inOut.toUpperCase()) { + case "IN" -> ProcedureColumn.ColumnType.IN; + case "OUT" -> ProcedureColumn.ColumnType.OUT; + case "IN/OUT" -> ProcedureColumn.ColumnType.INOUT; + default -> ProcedureColumn.ColumnType.UNKNOWN; + }; + } + + + private static FunctionColumn.ColumnType mapOracleFuncColumnType(String inOut) { + if (inOut == null) { + return FunctionColumn.ColumnType.UNKNOWN; + } + return switch (inOut.toUpperCase()) { + case "IN" -> FunctionColumn.ColumnType.IN; + case "OUT" -> FunctionColumn.ColumnType.OUT; + case "IN/OUT" -> FunctionColumn.ColumnType.INOUT; + default -> FunctionColumn.ColumnType.UNKNOWN; + }; + } + + + private static JDBCType mapOracleJdbcType(String dataType) { + if (dataType == null) { + return JDBCType.OTHER; + } + return switch (dataType.toUpperCase()) { + case "NUMBER" -> JDBCType.NUMERIC; + case "VARCHAR2", "NVARCHAR2" -> JDBCType.VARCHAR; + case "CHAR", "NCHAR" -> JDBCType.CHAR; + case "DATE" -> JDBCType.DATE; + case "TIMESTAMP", "TIMESTAMP(6)" -> JDBCType.TIMESTAMP; + case "CLOB", "NCLOB" -> JDBCType.CLOB; + case "BLOB" -> JDBCType.BLOB; + case "RAW" -> JDBCType.VARBINARY; + case "LONG RAW" -> JDBCType.LONGVARBINARY; + case "LONG" -> JDBCType.LONGVARCHAR; + case "FLOAT", "BINARY_FLOAT" -> JDBCType.FLOAT; + case "BINARY_DOUBLE" -> JDBCType.DOUBLE; + case "INTEGER", "PLS_INTEGER" -> JDBCType.INTEGER; + case "BOOLEAN" -> JDBCType.BOOLEAN; + case "XMLTYPE" -> JDBCType.SQLXML; + default -> JDBCType.OTHER; + }; + } + + + private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException { + String triggerName = rs.getString("TRIGGER_NAME"); + String tableName = rs.getString("TABLE_NAME"); + String triggerType = rs.getString("TRIGGER_TYPE"); + String triggeringEvent = rs.getString("TRIGGERING_EVENT"); + + // TRIGGER_BODY is LONG type, wrap in try-catch + String triggerBody; + try { + triggerBody = rs.getString("TRIGGER_BODY"); + } catch (SQLException e) { + LOGGER.debug("Could not read TRIGGER_BODY for trigger {}", triggerName, e); + triggerBody = null; + } + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = mapOracleTriggerTiming(triggerType); + TriggerEvent event = mapOracleTriggerEvent(triggeringEvent); + String orientation = parseOracleOrientation(triggerType); + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(triggerBody), Optional.empty(), Optional.ofNullable(orientation)); + } + + + private ImportedKey readImportedKey(ResultSet rs, String schemaName) throws SQLException { + String fkName = rs.getString("FK_NAME"); + String fkTable = rs.getString("FK_TABLE"); + String fkColumn = rs.getString("FK_COLUMN"); + String pkTable = rs.getString("PK_TABLE"); + String pkColumn = rs.getString("PK_COLUMN"); + int keySeq = rs.getInt("KEY_SEQ"); + String deleteRule = rs.getString("DELETE_RULE"); + String pkName = rs.getString("PK_NAME"); + + // FK side + Optional fkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side + Optional pkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, ImportedKey.ReferentialAction.NO_ACTION, // Oracle + // doesn't + // support + // UPDATE + // CASCADE + mapOracleDeleteRule(deleteRule), Optional.ofNullable(pkName), ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readCheckConstraints(Connection connection, String schemaName, String tableName) + throws SQLException { + // Try with SEARCH_CONDITION_VC first (available in Oracle 12.1+) + try { + return readCheckConstraintsWithVc(connection, schemaName, tableName); + } catch (SQLException e) { + LOGGER.debug("SEARCH_CONDITION_VC not available, falling back to SEARCH_CONDITION", e); + } + + // Fall back to SEARCH_CONDITION (LONG type) + return readCheckConstraintsWithLong(connection, schemaName, tableName); + } + + + private List readCheckConstraintsWithVc(Connection connection, String schemaName, String tableName) + throws SQLException { + String sql; + if (tableName != null) { + sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, c.SEARCH_CONDITION_VC + FROM ALL_CONSTRAINTS c WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'C' + AND c.GENERATED != 'GENERATED NAME' AND c.TABLE_NAME = ? + ORDER BY c.CONSTRAINT_NAME + """; + } else { + sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, c.SEARCH_CONDITION_VC + FROM ALL_CONSTRAINTS c WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'C' + AND c.GENERATED != 'GENERATED NAME' + ORDER BY c.TABLE_NAME, c.CONSTRAINT_NAME + """; + } + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String checkClause = rs.getString("SEARCH_CONDITION_VC"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, table); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, + checkClause != null ? checkClause : "")); + } + } + } + return List.copyOf(constraints); + } + + + private List readCheckConstraintsWithLong(Connection connection, String schemaName, + String tableName) throws SQLException { + String sql; + if (tableName != null) { + sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, c.SEARCH_CONDITION + FROM ALL_CONSTRAINTS c WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'C' + AND c.GENERATED != 'GENERATED NAME' AND c.TABLE_NAME = ? + ORDER BY c.CONSTRAINT_NAME + """; + } else { + sql = """ + SELECT c.CONSTRAINT_NAME, c.TABLE_NAME, c.SEARCH_CONDITION + FROM ALL_CONSTRAINTS c WHERE c.OWNER = ? AND c.CONSTRAINT_TYPE = 'C' + AND c.GENERATED != 'GENERATED NAME' + ORDER BY c.TABLE_NAME, c.CONSTRAINT_NAME + """; + } + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String checkClause; + try { + checkClause = rs.getString("SEARCH_CONDITION"); + } catch (SQLException e) { + LOGGER.debug("Could not read SEARCH_CONDITION for constraint {}", constraintName, e); + checkClause = null; + } + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, table); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, + checkClause != null ? checkClause : "")); + } + } + } + return List.copyOf(constraints); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("CONSTRAINT_NAME"); + String table = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + // Oracle uses the user name as the default schema + String connSchema = connection.getSchema(); + if (connSchema != null) { + return connSchema; + } + return connection.getMetaData().getUserName(); + } + + + private static TriggerTiming mapOracleTriggerTiming(String triggerType) { + if (triggerType == null) { + return TriggerTiming.AFTER; + } + String upper = triggerType.toUpperCase(); + if (upper.startsWith("BEFORE")) { + return TriggerTiming.BEFORE; + } else if (upper.startsWith("INSTEAD OF")) { + return TriggerTiming.INSTEAD_OF; + } else { + return TriggerTiming.AFTER; + } + } + + + private static TriggerEvent mapOracleTriggerEvent(String triggeringEvent) { + if (triggeringEvent == null) { + return TriggerEvent.INSERT; + } + // Take the first word: 'INSERT OR UPDATE' -> 'INSERT' + String firstWord = triggeringEvent.trim().split("\\s+")[0].toUpperCase(); + return switch (firstWord) { + case "UPDATE" -> TriggerEvent.UPDATE; + case "DELETE" -> TriggerEvent.DELETE; + default -> TriggerEvent.INSERT; + }; + } + + + private static String parseOracleOrientation(String triggerType) { + if (triggerType == null) { + return null; + } + String upper = triggerType.toUpperCase(); + if (upper.contains("EACH ROW")) { + return "ROW"; + } else if (upper.contains("STATEMENT")) { + return "STATEMENT"; + } + return null; + } + + + private static ImportedKey.ReferentialAction mapOracleDeleteRule(String deleteRule) { + if (deleteRule == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (deleteRule.toUpperCase()) { + case "CASCADE" -> ImportedKey.ReferentialAction.CASCADE; + case "SET NULL" -> ImportedKey.ReferentialAction.SET_NULL; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + private static IndexInfoItem.IndexType mapOracleIndexType(String indexType) { + if (indexType == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (indexType.toUpperCase()) { + case "CLUSTER" -> IndexInfoItem.IndexType.TABLE_INDEX_CLUSTERED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + @Override + public List getAllUserDefinedTypes(Connection connection, String catalog, String schema) + throws SQLException { + String schemaName = resolveSchema(schema, connection); + String sql = """ + SELECT TYPE_NAME, TYPECODE, SUPERTYPE_NAME + FROM ALL_TYPES + WHERE OWNER = ? + ORDER BY TYPE_NAME + """; + List types = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String typeName = rs.getString("TYPE_NAME"); + String typeCode = rs.getString("TYPECODE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + JDBCType baseType = mapOracleUdtBaseType(typeCode); + + types.add(new UserDefinedTypeRecord(new UserDefinedTypeReference(oSchema, typeName), typeCode, + baseType, Optional.empty())); + } + } + } + return List.copyOf(types); + } + + + private static JDBCType mapOracleUdtBaseType(String typeCode) { + if (typeCode == null) { + return JDBCType.OTHER; + } + return switch (typeCode.toUpperCase()) { + case "OBJECT" -> JDBCType.STRUCT; + case "COLLECTION" -> JDBCType.ARRAY; + default -> JDBCType.OTHER; + }; + } + + + @Override + public Optional> getAllTablePrivileges(Connection connection, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + // ALL_TAB_PRIVS sees grants regardless of the current user — PUBLIC, role + // grants, + // and grants made by others are all returned. JDBC's getTablePrivileges only + // returns direct grants to the connected user. + StringBuilder sql = new StringBuilder(""" + SELECT OWNER, TABLE_NAME, GRANTOR, GRANTEE, PRIVILEGE, GRANTABLE + FROM ALL_TAB_PRIVS + WHERE OWNER = ? + """); + boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() + && !"%".equals(tableNamePattern); + if (hasTableFilter) { + sql.append(" AND TABLE_NAME LIKE ?\n"); + } + sql.append("ORDER BY TABLE_NAME, PRIVILEGE, GRANTEE"); + + String schemaName = resolveSchema(schemaPattern, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + if (hasTableFilter) { + ps.setString(2, tableNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String grantor = rs.getString("GRANTOR"); + String grantee = rs.getString("GRANTEE"); + String privilege = rs.getString("PRIVILEGE"); + String grantable = rs.getString("GRANTABLE"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + result.add(new TablePrivilegeRecord(tableRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.ofNullable(grantable))); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read table privileges from ALL_TAB_PRIVS: {}", e.getMessage()); + return Optional.empty(); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getColumnPrivileges(Connection connection, String catalog, String schema, + String tableName, String columnNamePattern) throws SQLException { + // ALL_COL_PRIVS is the column-level equivalent of ALL_TAB_PRIVS. + StringBuilder sql = new StringBuilder(""" + SELECT OWNER, TABLE_NAME, COLUMN_NAME, GRANTOR, GRANTEE, PRIVILEGE, GRANTABLE + FROM ALL_COL_PRIVS + WHERE OWNER = ? AND TABLE_NAME = ? + """); + boolean hasColumnFilter = columnNamePattern != null && !columnNamePattern.isBlank() + && !"%".equals(columnNamePattern); + if (hasColumnFilter) { + sql.append(" AND COLUMN_NAME LIKE ?\n"); + } + sql.append("ORDER BY COLUMN_NAME, PRIVILEGE, GRANTEE"); + + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + if (hasColumnFilter) { + ps.setString(3, columnNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + while (rs.next()) { + String columnName = rs.getString("COLUMN_NAME"); + String grantor = rs.getString("GRANTOR"); + String grantee = rs.getString("GRANTEE"); + String privilege = rs.getString("PRIVILEGE"); + String grantable = rs.getString("GRANTABLE"); + + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + result.add(new ColumnPrivilegeRecord(colRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.ofNullable(grantable))); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read column privileges from ALL_COL_PRIVS: {}", e.getMessage()); + return Optional.empty(); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllPseudoColumns(Connection connection, String catalog, String schemaPattern, + String tableNamePattern, String columnNamePattern) throws SQLException { + // Oracle's JDBC driver returns an empty pseudo-column set for most objects. + // ALL_TAB_COLS with HIDDEN_COLUMN='YES' surfaces Oracle-specific pseudo columns + // (ROWID of index-organized tables, virtual columns, system-generated identity + // tracking columns, etc.) that never show up via getPseudoColumns. + StringBuilder sql = new StringBuilder(""" + SELECT c.OWNER, c.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.DATA_LENGTH, + c.DATA_PRECISION, c.DATA_SCALE, c.NULLABLE + FROM ALL_TAB_COLS c + WHERE c.HIDDEN_COLUMN = 'YES' AND c.OWNER = ? + """); + boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() + && !"%".equals(tableNamePattern); + if (hasTableFilter) { + sql.append(" AND c.TABLE_NAME LIKE ?\n"); + } + boolean hasColumnFilter = columnNamePattern != null && !columnNamePattern.isBlank() + && !"%".equals(columnNamePattern); + if (hasColumnFilter) { + sql.append(" AND c.COLUMN_NAME LIKE ?\n"); + } + sql.append("ORDER BY c.TABLE_NAME, c.COLUMN_NAME"); + + String schemaName = resolveSchema(schemaPattern, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + int idx = 1; + ps.setString(idx++, schemaName); + if (hasTableFilter) { + ps.setString(idx++, tableNamePattern); + } + if (hasColumnFilter) { + ps.setString(idx++, columnNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + while (rs.next()) { + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + String dataType = rs.getString("DATA_TYPE"); + int length = rs.getInt("DATA_LENGTH"); + boolean lengthNull = rs.wasNull(); + int precision = rs.getInt("DATA_PRECISION"); + boolean precisionNull = rs.wasNull(); + int scale = rs.getInt("DATA_SCALE"); + boolean scaleNull = rs.wasNull(); + String nullable = rs.getString("NULLABLE"); + + TableReference tableRef = new TableReference(oSchema, tableName); + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + + JDBCType jdbc = mapOracleJdbcType(dataType); + OptionalInt columnSize = lengthNull ? OptionalInt.empty() + : precisionNull ? OptionalInt.of(length) : OptionalInt.of(precision); + OptionalInt decimalDigits = scaleNull ? OptionalInt.empty() : OptionalInt.of(scale); + + result.add(new PseudoColumnRecord(colRef, jdbc, columnSize, decimalDigits, OptionalInt.of(10), + Optional.empty(), OptionalInt.empty(), + Optional.ofNullable("Y".equals(nullable) ? "YES" : "N".equals(nullable) ? "NO" : null), + "HIDDEN")); + } + } + } catch (SQLException e) { + LOGGER.debug("Could not read pseudo columns from ALL_TAB_COLS: {}", e.getMessage()); + return Optional.empty(); + } + return Optional.of(List.copyOf(result)); + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } + + + @Override + public Optional> getAllColumnDefinitions( + Connection connection, String catalog, String schemaPattern, String tableNamePattern, + String columnNamePattern) throws SQLException { + String sql = """ + SELECT OWNER, TABLE_NAME, COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, + DATA_SCALE, NULLABLE, DATA_DEFAULT, COLUMN_ID + FROM ALL_TAB_COLS + WHERE OWNER = ? AND HIDDEN_COLUMN = 'NO' + ORDER BY OWNER, TABLE_NAME, COLUMN_ID + """; + String schemaName = resolveSchema(schemaPattern, connection); + List out = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String owner = rs.getString("OWNER"); + String tableName = rs.getString("TABLE_NAME"); + String columnName = rs.getString("COLUMN_NAME"); + String dataType = rs.getString("DATA_TYPE"); + long dataLength = rs.getLong("DATA_LENGTH"); + long dataPrecision = rs.getLong("DATA_PRECISION"); + boolean precNull = rs.wasNull(); + long dataScale = rs.getLong("DATA_SCALE"); + boolean scaleNull = rs.wasNull(); + String nullable = rs.getString("NULLABLE"); + String columnDefault = rs.getString("DATA_DEFAULT"); // bufferable here + + java.sql.JDBCType jdbcType = mapOracleType(dataType); + java.util.OptionalInt size; + java.util.OptionalInt scale = java.util.OptionalInt.empty(); + if (isNumericJdbc(jdbcType)) { + size = precNull ? java.util.OptionalInt.empty() : java.util.OptionalInt.of((int) dataPrecision); + if (!scaleNull && dataScale != 0) + scale = java.util.OptionalInt.of((int) dataScale); + } else { + size = dataLength > 0 ? java.util.OptionalInt.of((int) dataLength) + : java.util.OptionalInt.empty(); + } + org.eclipse.daanse.sql.model.schema.ColumnMetaData.Nullability n = "Y".equalsIgnoreCase(nullable) + ? org.eclipse.daanse.sql.model.schema.ColumnMetaData.Nullability.NULLABLE + : org.eclipse.daanse.sql.model.schema.ColumnMetaData.Nullability.NO_NULLS; + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), owner)); + org.eclipse.daanse.sql.model.schema.TableReference tableRef = new TableReference(oSchema, + tableName); + org.eclipse.daanse.sql.model.schema.ColumnReference colRef = new ColumnReference( + Optional.of(tableRef), columnName); + + org.eclipse.daanse.sql.model.schema.ColumnMetaData meta = new org.eclipse.daanse.sql.jdbc.record.schema.ColumnMetaDataRecord( + jdbcType, dataType, size, scale, java.util.OptionalInt.empty(), n, + java.util.OptionalInt.empty(), Optional.empty(), + Optional.ofNullable(columnDefault).map(String::trim).filter(s -> !s.isEmpty()), + org.eclipse.daanse.sql.model.schema.ColumnMetaData.AutoIncrement.UNKNOWN, + org.eclipse.daanse.sql.model.schema.ColumnMetaData.GeneratedColumn.UNKNOWN); + + out.add(new org.eclipse.daanse.sql.jdbc.record.schema.ColumnDefinitionRecord(colRef, meta)); + } + } + } + return Optional.of(List.copyOf(out)); + } + + + private static java.sql.JDBCType mapOracleType(String oracleType) { + if (oracleType == null) + return java.sql.JDBCType.OTHER; + return switch (oracleType.toUpperCase()) { + case "NUMBER" -> java.sql.JDBCType.NUMERIC; + case "INTEGER" -> java.sql.JDBCType.INTEGER; + case "FLOAT", "BINARY_FLOAT" -> java.sql.JDBCType.REAL; + case "BINARY_DOUBLE" -> java.sql.JDBCType.DOUBLE; + case "VARCHAR", "VARCHAR2", "NVARCHAR2" -> java.sql.JDBCType.VARCHAR; + case "CHAR", "NCHAR" -> java.sql.JDBCType.CHAR; + case "CLOB", "NCLOB" -> java.sql.JDBCType.CLOB; + case "BLOB" -> java.sql.JDBCType.BLOB; + case "DATE" -> java.sql.JDBCType.DATE; + case "TIMESTAMP", "TIMESTAMP WITH TIME ZONE", "TIMESTAMP WITH LOCAL TIME ZONE" -> java.sql.JDBCType.TIMESTAMP; + case "RAW", "LONG RAW" -> java.sql.JDBCType.VARBINARY; + default -> java.sql.JDBCType.OTHER; + }; + } + + + private static boolean isNumericJdbc(java.sql.JDBCType t) { + if (t == null) + return false; + return switch (t) { + case TINYINT, SMALLINT, INTEGER, BIGINT, FLOAT, REAL, DOUBLE, DECIMAL, NUMERIC -> true; + default -> false; + }; + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProviderFactory.java new file mode 100644 index 0000000..8fee676 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/OracleMetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The Oracle {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class OracleMetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("oracle"); + } + + @Override + public MetadataProvider createProvider() { + return new OracleMetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java new file mode 100644 index 0000000..14dfefb --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProvider.java @@ -0,0 +1,1014 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.JDBCType; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.TriggerReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedTypeReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.eclipse.daanse.sql.jdbc.record.schema.CheckConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ColumnPrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.FunctionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ImportedKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoItemRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.IndexInfoRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.MaterializedViewRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PartitionRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.PrimaryKeyRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ProcedureRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.SequenceRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TablePrivilegeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.TriggerRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UniqueConstraintRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.UserDefinedTypeRecord; +import org.eclipse.daanse.sql.jdbc.record.schema.ViewDefinitionRecord; + +/** + * The PostgreSql system-catalog/{@code information_schema} reader — the + * {@link MetadataProvider} overrides extracted 1:1 from the SQL dialect (reading only, + * no spelling). + */ +public class PostgreSqlMetadataProvider implements MetadataProvider { + + + @Override + public List getAllTriggers(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT t.tgname AS trigger_name, c.relname AS table_name, n.nspname AS schema_name, + pg_get_triggerdef(t.oid) AS definition, + p.prosrc AS proc_body + FROM pg_trigger t + JOIN pg_class c ON c.oid = t.tgrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_proc p ON p.oid = t.tgfoid + WHERE NOT t.tgisinternal AND n.nspname = ? + ORDER BY c.relname, t.tgname + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getTriggers(Connection connection, String catalog, String schema, String tableName) + throws SQLException { + String sql = """ + SELECT t.tgname AS trigger_name, c.relname AS table_name, n.nspname AS schema_name, + pg_get_triggerdef(t.oid) AS definition, + p.prosrc AS proc_body + FROM pg_trigger t + JOIN pg_class c ON c.oid = t.tgrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + LEFT JOIN pg_proc p ON p.oid = t.tgfoid + WHERE NOT t.tgisinternal AND n.nspname = ? AND c.relname = ? + ORDER BY t.tgname + """; + String schemaName = resolveSchema(schema, connection); + List triggers = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + triggers.add(readTrigger(rs, schemaName)); + } + } + } + return List.copyOf(triggers); + } + + + @Override + public List getAllSequences(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT sequence_name, start_value, increment, minimum_value, maximum_value, + cycle_option, data_type + FROM information_schema.sequences + WHERE sequence_schema = ? + ORDER BY sequence_name + """; + String schemaName = resolveSchema(schema, connection); + List sequences = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("sequence_name"); + long startValue = rs.getLong("start_value"); + long increment = rs.getLong("increment"); + long minValue = rs.getLong("minimum_value"); + Optional oMinValue = rs.wasNull() ? Optional.empty() : Optional.of(minValue); + long maxValue = rs.getLong("maximum_value"); + Optional oMaxValue = rs.wasNull() ? Optional.empty() : Optional.of(maxValue); + String cycleOption = rs.getString("cycle_option"); + boolean cycle = "YES".equalsIgnoreCase(cycleOption); + String dataType = rs.getString("data_type"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + sequences.add(new SequenceRecord(new SequenceReference(oSchema, name), startValue, increment, + oMinValue, oMaxValue, cycle, Optional.empty(), Optional.ofNullable(dataType))); + } + } + } + return List.copyOf(sequences); + } + + + @Override + public List getAllPartitions(Connection connection, String catalog, String schema) throws SQLException { + // PostgreSQL declarative partitioning (10+): pg_inherits links each partition + // to its + // partitioned parent; pg_partitioned_table.partstrat carries the strategy + // ('r' RANGE / 'l' LIST / 'h' HASH); pg_get_expr(relpartbound) returns the + // bound expression + // ("FOR VALUES FROM (...) TO (...)" or "FOR VALUES IN (...)" or "FOR VALUES + // WITH (...)"). + // pg_get_partkeydef returns the partition key like "RANGE (year)". + String sql = """ + SELECT p.relname AS parent_table, + c.relname AS partition_name, + pt.partstrat AS strategy, + pg_get_expr(c.relpartbound, c.oid) AS bound, + c.reltuples::bigint AS row_count, + pg_get_partkeydef(p.oid) AS keydef + FROM pg_inherits i + JOIN pg_class c ON c.oid = i.inhrelid + JOIN pg_class p ON p.oid = i.inhparent + JOIN pg_partitioned_table pt ON pt.partrelid = p.oid + JOIN pg_namespace np ON np.oid = p.relnamespace + WHERE np.nspname = ? + ORDER BY p.relname, c.relname + """; + String schemaName = resolveSchema(schema, connection); + List partitions = new ArrayList<>(); + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String parentTable = rs.getString("parent_table"); + String partitionName = rs.getString("partition_name"); + String strategy = rs.getString("strategy"); + String bound = rs.getString("bound"); + long rowCount = rs.getLong("row_count"); + String keydef = rs.getString("keydef"); + + PartitionMethod method = mapPartitionStrategy(strategy); + String expression = extractPartitionKey(keydef); + + TableReference tableRef = new TableReference(oSchema, parentTable); + partitions.add(new PartitionRecord(partitionName, tableRef, Optional.empty(), // PostgreSQL has no + // native ordinal + // position + method, expression == null ? Optional.empty() : Optional.of(expression), + bound == null ? Optional.empty() : Optional.of(bound), + rs.wasNull() ? Optional.empty() : Optional.of(rowCount), Optional.empty(), Optional.empty(), + Optional.empty())); + } + } + } catch (SQLException e) { + // pg_partitioned_table exists from PostgreSQL 10; older servers return no rows + // but should not error. Catch defensively for ancient or non-PostgreSQL + // impostors. + return List.of(); + } + return List.copyOf(partitions); + } + + + private static PartitionMethod mapPartitionStrategy(String partstrat) { + if (partstrat == null || partstrat.isEmpty()) { + return PartitionMethod.OTHER; + } + return switch (partstrat.charAt(0)) { + case 'r' -> PartitionMethod.RANGE; + case 'l' -> PartitionMethod.LIST; + case 'h' -> PartitionMethod.HASH; + default -> PartitionMethod.OTHER; + }; + } + + + private static String extractPartitionKey(String keydef) { + if (keydef == null) { + return null; + } + int paren = keydef.indexOf('('); + if (paren < 0 || !keydef.endsWith(")")) { + return keydef; + } + return keydef.substring(paren + 1, keydef.length() - 1).trim(); + } + + + @Override + public List getAllCheckConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT con.conname, c.relname, pg_get_constraintdef(con.oid) AS check_clause + FROM pg_constraint con + JOIN pg_class c ON c.oid = con.conrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE con.contype = 'c' AND n.nspname = ? + ORDER BY c.relname, con.conname + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("conname"); + String tableName = rs.getString("relname"); + String checkClause = rs.getString("check_clause"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getCheckConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT con.conname, c.relname, pg_get_constraintdef(con.oid) AS check_clause + FROM pg_constraint con + JOIN pg_class c ON c.oid = con.conrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE con.contype = 'c' AND n.nspname = ? AND c.relname = ? + ORDER BY con.conname + """; + String schemaName = resolveSchema(schema, connection); + List constraints = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("conname"); + String checkClause = rs.getString("check_clause"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + constraints.add(new CheckConstraintRecord(constraintName, tableRef, checkClause)); + } + } + } + return List.copyOf(constraints); + } + + + @Override + public List getAllUniqueConstraints(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT con.conname, c.relname, a.attname, array_position(con.conkey, a.attnum) AS ordinal + FROM pg_constraint con + JOIN pg_class c ON c.oid = con.conrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) + WHERE con.contype = 'u' AND n.nspname = ? + ORDER BY c.relname, con.conname, ordinal + """; + return readUniqueConstraints(connection, sql, schema, null); + } + + + @Override + public List getUniqueConstraints(Connection connection, String catalog, String schema, + String tableName) throws SQLException { + String sql = """ + SELECT con.conname, c.relname, a.attname, array_position(con.conkey, a.attnum) AS ordinal + FROM pg_constraint con + JOIN pg_class c ON c.oid = con.conrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) + WHERE con.contype = 'u' AND n.nspname = ? AND c.relname = ? + ORDER BY con.conname, ordinal + """; + return readUniqueConstraints(connection, sql, schema, tableName); + } + + + @Override + public Optional> getAllPrimaryKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT con.conname, c.relname, a.attname, array_position(con.conkey, a.attnum) AS ordinal + FROM pg_constraint con + JOIN pg_class c ON c.oid = con.conrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = ANY(con.conkey) + WHERE con.contype = 'p' AND n.nspname = ? + ORDER BY c.relname, ordinal + """; + String schemaName = resolveSchema(schema, connection); + Map pkMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("conname"); + String tableName = rs.getString("relname"); + String columnName = rs.getString("attname"); + + String key = tableName + "." + constraintName; + pkMap.computeIfAbsent(key, k -> new PkBuilder(tableName, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (PkBuilder builder : pkMap.values()) { + result.add(builder.build()); + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getAllImportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT con.conname AS fk_name, fk_class.relname AS fk_table, fk_att.attname AS fk_column, + pk_class.relname AS pk_table, pk_att.attname AS pk_column, + cols.ord AS key_seq, con.confdeltype AS delete_rule, con.confupdtype AS update_rule + FROM pg_constraint con + JOIN pg_class fk_class ON fk_class.oid = con.conrelid + JOIN pg_namespace n ON n.oid = fk_class.relnamespace + JOIN pg_class pk_class ON pk_class.oid = con.confrelid + JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(fk_attnum, pk_attnum, ord) ON TRUE + JOIN pg_attribute fk_att ON fk_att.attrelid = con.conrelid AND fk_att.attnum = cols.fk_attnum + JOIN pg_attribute pk_att ON pk_att.attrelid = con.confrelid AND pk_att.attnum = cols.pk_attnum + WHERE con.contype = 'f' AND n.nspname = ? + ORDER BY fk_class.relname, con.conname, cols.ord + """; + String schemaName = resolveSchema(schema, connection); + List importedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + importedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(importedKeys)); + } + + + @Override + public Optional> getAllExportedKeys(Connection connection, String catalog, String schema) + throws SQLException { + // Symmetric to getAllImportedKeys: filter by the namespace of the REFERENCED + // (PK-side) table. + String sql = """ + SELECT con.conname AS fk_name, fk_class.relname AS fk_table, fk_att.attname AS fk_column, + pk_class.relname AS pk_table, pk_att.attname AS pk_column, + cols.ord AS key_seq, con.confdeltype AS delete_rule, con.confupdtype AS update_rule + FROM pg_constraint con + JOIN pg_class fk_class ON fk_class.oid = con.conrelid + JOIN pg_class pk_class ON pk_class.oid = con.confrelid + JOIN pg_namespace pk_n ON pk_n.oid = pk_class.relnamespace + JOIN LATERAL unnest(con.conkey, con.confkey) WITH ORDINALITY AS cols(fk_attnum, pk_attnum, ord) ON TRUE + JOIN pg_attribute fk_att ON fk_att.attrelid = con.conrelid AND fk_att.attnum = cols.fk_attnum + JOIN pg_attribute pk_att ON pk_att.attrelid = con.confrelid AND pk_att.attnum = cols.pk_attnum + WHERE con.contype = 'f' AND pk_n.nspname = ? + ORDER BY pk_class.relname, con.conname, cols.ord + """; + String schemaName = resolveSchema(schema, connection); + List exportedKeys = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + exportedKeys.add(readImportedKey(rs, schemaName)); + } + } + } + return Optional.of(List.copyOf(exportedKeys)); + } + + + @Override + public Optional> getAllIndexInfo(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT c.relname AS table_name, i_class.relname AS index_name, + a.attname AS column_name, array_position(ix.indkey, a.attnum) AS ordinal, + ix.indisunique AS is_unique, + am.amname AS index_type + FROM pg_index ix + JOIN pg_class c ON c.oid = ix.indrelid + JOIN pg_class i_class ON i_class.oid = ix.indexrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_am am ON am.oid = i_class.relam + JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY(ix.indkey) + WHERE n.nspname = ? AND NOT ix.indisprimary + ORDER BY c.relname, i_class.relname, ordinal + """; + String schemaName = resolveSchema(schema, connection); + Map> tableIndexes = new LinkedHashMap<>(); + Map tableRefs = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + String indexName = rs.getString("index_name"); + String columnName = rs.getString("column_name"); + int ordinal = rs.getInt("ordinal"); + boolean isUnique = rs.getBoolean("is_unique"); + String indexType = rs.getString("index_type"); + + IndexInfoItem.IndexType mappedType = mapPgIndexType(indexType); + + TableReference tableRef = tableRefs.computeIfAbsent(tableName, k -> { + Optional oSchema = Optional + .of(new SchemaReference(Optional.empty(), schemaName)); + return new TableReference(oSchema, k); + }); + + Optional colRef = Optional.ofNullable(columnName) + .map(cn -> new ColumnReference(Optional.of(tableRef), cn)); + + IndexInfoItem item = new IndexInfoItemRecord(Optional.ofNullable(indexName), mappedType, colRef, + ordinal, Optional.empty(), 0L, 0L, Optional.empty(), isUnique); + + tableIndexes.computeIfAbsent(tableName, k -> new ArrayList<>()).add(item); + } + } + } + List result = new ArrayList<>(); + for (Map.Entry> entry : tableIndexes.entrySet()) { + result.add(new IndexInfoRecord(tableRefs.get(entry.getKey()), List.copyOf(entry.getValue()))); + } + return Optional.of(List.copyOf(result)); + } + + + private static IndexInfoItem.IndexType mapPgIndexType(String amname) { + if (amname == null) { + return IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + } + return switch (amname.toLowerCase()) { + case "hash" -> IndexInfoItem.IndexType.TABLE_INDEX_HASHED; + default -> IndexInfoItem.IndexType.TABLE_INDEX_OTHER; + }; + } + + + @Override + public List getAllViewDefinitions(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT c.relname, pg_get_viewdef(c.oid, true) AS view_body + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind = 'v' AND n.nspname = ? + ORDER BY c.relname + """; + String schemaName = resolveSchema(schema, connection); + List views = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String viewName = rs.getString("relname"); + String viewBody = rs.getString("view_body"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, viewName, "VIEW"); + + views.add(new ViewDefinitionRecord(viewRef, Optional.ofNullable(viewBody), Optional.empty())); + } + } + } + return List.copyOf(views); + } + + + @Override + public List getAllMaterializedViews(Connection connection, String catalog, String schema) + throws SQLException { + // pg_class.relkind = 'm' identifies materialized views. pg_get_viewdef works on + // matview OIDs just as it does on view OIDs. PostgreSQL has no explicit refresh + // mode (always manual REFRESH MATERIALIZED VIEW) and does not track last + // refresh. + String sql = """ + SELECT c.relname, pg_get_viewdef(c.oid, true) AS view_body + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind = 'm' AND n.nspname = ? + ORDER BY c.relname + """; + String schemaName = resolveSchema(schema, connection); + List mviews = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String name = rs.getString("relname"); + String body = rs.getString("view_body"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference viewRef = new TableReference(oSchema, name, "MATERIALIZED VIEW"); + + mviews.add(new MaterializedViewRecord(viewRef, Optional.ofNullable(body), Optional.empty(), + Optional.empty(), Optional.empty())); + } + } + } + return List.copyOf(mviews); + } + + + @Override + public List getAllProcedures(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT p.proname AS routine_name, p.oid::text AS specific_name, + d.description AS remarks, p.prosrc AS body, + pg_get_functiondef(p.oid) AS full_def + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + LEFT JOIN pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass + WHERE n.nspname = ? AND p.prokind = 'p' + ORDER BY p.proname + """; + String schemaName = resolveSchema(schema, connection); + List procedures = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String specificName = rs.getString("specific_name"); + String remarks = rs.getString("remarks"); + String body = rs.getString("body"); + String fullDef = rs.getString("full_def"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + procedures.add(new ProcedureRecord(new ProcedureReference(oSchema, routineName, specificName), + Procedure.ProcedureType.NO_RESULT, Optional.ofNullable(remarks), List.of(), + Optional.ofNullable(body), Optional.ofNullable(fullDef), Optional.empty())); + } + } + } catch (SQLException e) { + // prokind = 'p' requires PostgreSQL 11+ + if (e.getMessage() != null && e.getMessage().contains("prokind")) { + return List.of(); + } + throw e; + } + return List.copyOf(procedures); + } + + + @Override + public List getAllFunctions(Connection connection, String catalog, String schema) throws SQLException { + String sql = """ + SELECT p.proname AS routine_name, p.oid::text AS specific_name, + d.description AS remarks, p.prosrc AS body, + pg_get_functiondef(p.oid) AS full_def, + CASE WHEN p.proretset THEN 'TABLE' ELSE 'SCALAR' END AS return_type + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + LEFT JOIN pg_description d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass + WHERE n.nspname = ? AND p.prokind = 'f' + ORDER BY p.proname + """; + String schemaName = resolveSchema(schema, connection); + List functions = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String routineName = rs.getString("routine_name"); + String specificName = rs.getString("specific_name"); + String remarks = rs.getString("remarks"); + String body = rs.getString("body"); + String fullDef = rs.getString("full_def"); + String returnType = rs.getString("return_type"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + Function.FunctionType funcType = "TABLE".equals(returnType) ? Function.FunctionType.RETURNS_TABLE + : Function.FunctionType.NO_TABLE; + + functions.add(new FunctionRecord(new FunctionReference(oSchema, routineName, specificName), + funcType, Optional.ofNullable(remarks), List.of(), Optional.ofNullable(body), + Optional.ofNullable(fullDef), Optional.empty())); + } + } + } catch (SQLException e) { + // prokind = 'f' requires PostgreSQL 11+ + if (e.getMessage() != null && e.getMessage().contains("prokind")) { + return List.of(); + } + throw e; + } + return List.copyOf(functions); + } + + + @Override + public List getAllUserDefinedTypes(Connection connection, String catalog, String schema) + throws SQLException { + String sql = """ + SELECT t.typname AS type_name, n.nspname AS schema_name, + t.typtype AS type_type, + CASE t.typtype + WHEN 'c' THEN 'STRUCT' + WHEN 'e' THEN 'ENUM' + WHEN 'd' THEN 'DISTINCT' + ELSE 'OTHER' + END AS class_name, + obj_description(t.oid, 'pg_type') AS remarks + FROM pg_type t + JOIN pg_namespace n ON n.oid = t.typnamespace + WHERE n.nspname = ? + AND t.typtype IN ('c', 'e', 'd') + AND NOT EXISTS (SELECT 1 FROM pg_class c WHERE c.reltype = t.oid AND c.relkind IN ('r', 'v', 'm')) + ORDER BY t.typname + """; + String schemaName = resolveSchema(schema, connection); + List types = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String typeName = rs.getString("type_name"); + String className = rs.getString("class_name"); + String typeType = rs.getString("type_type"); + String remarks = rs.getString("remarks"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + + JDBCType baseType = mapPgUdtBaseType(typeType); + + types.add(new UserDefinedTypeRecord(new UserDefinedTypeReference(oSchema, typeName), className, + baseType, Optional.ofNullable(remarks))); + } + } + } + return List.copyOf(types); + } + + + private static JDBCType mapPgUdtBaseType(String typtype) { + if (typtype == null) { + return JDBCType.OTHER; + } + return switch (typtype) { + case "c" -> JDBCType.STRUCT; + case "e" -> JDBCType.VARCHAR; + case "d" -> JDBCType.DISTINCT; + default -> JDBCType.OTHER; + }; + } + + + private Trigger readTrigger(ResultSet rs, String schemaName) throws SQLException { + String triggerName = rs.getString("trigger_name"); + String tableName = rs.getString("table_name"); + String definition = rs.getString("definition"); + // pg_proc.prosrc is the procedural source of the function the trigger + // calls — this is what callers need to reconstruct the trigger. + String procBody = rs.getString("proc_body"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + TriggerTiming timing = parseTriggerTiming(definition); + TriggerEvent event = parseTriggerEvent(definition); + Optional orientation = parseTriggerOrientation(definition); + + return new TriggerRecord(new TriggerReference(tableRef, triggerName), timing, event, + Optional.ofNullable(procBody), Optional.ofNullable(definition), orientation); + } + + + private static Optional parseTriggerOrientation(String definition) { + if (definition == null) + return Optional.empty(); + String upper = definition.toUpperCase(); + if (upper.contains("FOR EACH ROW")) + return Optional.of("ROW"); + if (upper.contains("FOR EACH STATEMENT")) + return Optional.of("STATEMENT"); + return Optional.empty(); + } + + + private ImportedKey readImportedKey(ResultSet rs, String schemaName) throws SQLException { + String fkName = rs.getString("fk_name"); + String fkTable = rs.getString("fk_table"); + String fkColumn = rs.getString("fk_column"); + String pkTable = rs.getString("pk_table"); + String pkColumn = rs.getString("pk_column"); + int keySeq = rs.getInt("key_seq"); + String deleteRule = rs.getString("delete_rule"); + String updateRule = rs.getString("update_rule"); + + // FK side + Optional fkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference fkTableRef = new TableReference(fkSchemaRef, fkTable); + ColumnReference fkColRef = new ColumnReference(Optional.of(fkTableRef), fkColumn); + + // PK side + Optional pkSchemaRef = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference pkTableRef = new TableReference(pkSchemaRef, pkTable); + ColumnReference pkColRef = new ColumnReference(Optional.of(pkTableRef), pkColumn); + + return new ImportedKeyRecord(pkColRef, fkColRef, fkName, keySeq, mapPgReferentialAction(updateRule), + mapPgReferentialAction(deleteRule), Optional.empty(), ImportedKey.Deferrability.NOT_DEFERRABLE); + } + + + private List readUniqueConstraints(Connection connection, String sql, String schema, + String tableName) throws SQLException { + String schemaName = resolveSchema(schema, connection); + Map ucMap = new LinkedHashMap<>(); + try (PreparedStatement ps = connection.prepareStatement(sql)) { + ps.setString(1, schemaName); + if (tableName != null) { + ps.setString(2, tableName); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String constraintName = rs.getString("conname"); + String table = rs.getString("relname"); + String columnName = rs.getString("attname"); + + String key = table + "." + constraintName; + ucMap.computeIfAbsent(key, k -> new UcBuilder(table, constraintName, schemaName)) + .addColumn(columnName); + } + } + } + List result = new ArrayList<>(); + for (UcBuilder builder : ucMap.values()) { + result.add(builder.build()); + } + return List.copyOf(result); + } + + + private String resolveSchema(String schema, Connection connection) throws SQLException { + if (schema != null) { + return schema; + } + return connection.getSchema() != null ? connection.getSchema() : "public"; + } + + + private static TriggerTiming parseTriggerTiming(String definition) { + if (definition == null) { + return TriggerTiming.AFTER; + } + String upper = definition.toUpperCase(); + if (upper.contains("INSTEAD OF")) { + return TriggerTiming.INSTEAD_OF; + } + if (upper.contains("BEFORE")) { + return TriggerTiming.BEFORE; + } + return TriggerTiming.AFTER; + } + + + private static TriggerEvent parseTriggerEvent(String definition) { + if (definition == null) { + return TriggerEvent.INSERT; + } + String upper = definition.toUpperCase(); + if (upper.contains("DELETE")) { + return TriggerEvent.DELETE; + } + if (upper.contains("UPDATE")) { + return TriggerEvent.UPDATE; + } + return TriggerEvent.INSERT; + } + + + private static ImportedKey.ReferentialAction mapPgReferentialAction(String action) { + if (action == null) { + return ImportedKey.ReferentialAction.NO_ACTION; + } + return switch (action.toLowerCase()) { + case "a" -> ImportedKey.ReferentialAction.NO_ACTION; + case "r" -> ImportedKey.ReferentialAction.RESTRICT; + case "c" -> ImportedKey.ReferentialAction.CASCADE; + case "n" -> ImportedKey.ReferentialAction.SET_NULL; + case "d" -> ImportedKey.ReferentialAction.SET_DEFAULT; + default -> ImportedKey.ReferentialAction.NO_ACTION; + }; + } + + + @Override + public Optional> getAllTablePrivileges(Connection connection, String catalog, + String schemaPattern, String tableNamePattern) throws SQLException { + // information_schema.table_privileges already expands role memberships and + // PUBLIC grants through the pg_catalog views, so it reports more than + // JDBC's getTablePrivileges alone. + StringBuilder sql = new StringBuilder(""" + SELECT table_schema, table_name, grantor, grantee, privilege_type, is_grantable + FROM information_schema.table_privileges + WHERE table_schema = ? + """); + boolean hasTableFilter = tableNamePattern != null && !tableNamePattern.isBlank() + && !"%".equals(tableNamePattern); + if (hasTableFilter) { + sql.append(" AND table_name LIKE ?\n"); + } + sql.append("ORDER BY table_name, privilege_type, grantee"); + + String schemaName = resolveSchema(schemaPattern, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + if (hasTableFilter) { + ps.setString(2, tableNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + String tableName = rs.getString("table_name"); + String grantor = rs.getString("grantor"); + String grantee = rs.getString("grantee"); + String privilege = rs.getString("privilege_type"); + String isGrantable = rs.getString("is_grantable"); + + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + result.add(new TablePrivilegeRecord(tableRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.ofNullable(isGrantable))); + } + } + } + return Optional.of(List.copyOf(result)); + } + + + @Override + public Optional> getColumnPrivileges(Connection connection, String catalog, String schema, + String tableName, String columnNamePattern) throws SQLException { + StringBuilder sql = new StringBuilder(""" + SELECT column_name, grantor, grantee, privilege_type, is_grantable + FROM information_schema.column_privileges + WHERE table_schema = ? AND table_name = ? + """); + boolean hasColumnFilter = columnNamePattern != null && !columnNamePattern.isBlank() + && !"%".equals(columnNamePattern); + if (hasColumnFilter) { + sql.append(" AND column_name LIKE ?\n"); + } + sql.append("ORDER BY column_name, privilege_type, grantee"); + + String schemaName = resolveSchema(schema, connection); + List result = new ArrayList<>(); + try (PreparedStatement ps = connection.prepareStatement(sql.toString())) { + ps.setString(1, schemaName); + ps.setString(2, tableName); + if (hasColumnFilter) { + ps.setString(3, columnNamePattern); + } + try (ResultSet rs = ps.executeQuery()) { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + + while (rs.next()) { + String columnName = rs.getString("column_name"); + String grantor = rs.getString("grantor"); + String grantee = rs.getString("grantee"); + String privilege = rs.getString("privilege_type"); + String isGrantable = rs.getString("is_grantable"); + + ColumnReference colRef = new ColumnReference(Optional.of(tableRef), columnName); + result.add(new ColumnPrivilegeRecord(colRef, Optional.ofNullable(grantor), grantee, privilege, + Optional.ofNullable(isGrantable))); + } + } + } + return Optional.of(List.copyOf(result)); + } + + + // Builder for aggregating composite primary key columns + private static class PkBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + PkBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + PrimaryKey build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new PrimaryKeyRecord(tableRef, colRefs, Optional.of(constraintName)); + } + } + + + // Builder for aggregating composite unique constraint columns + private static class UcBuilder { + private final String tableName; + private final String constraintName; + private final String schemaName; + private final List columns = new ArrayList<>(); + + UcBuilder(String tableName, String constraintName, String schemaName) { + this.tableName = tableName; + this.constraintName = constraintName; + this.schemaName = schemaName; + } + + void addColumn(String columnName) { + columns.add(columnName); + } + + UniqueConstraint build() { + Optional oSchema = Optional.of(new SchemaReference(Optional.empty(), schemaName)); + TableReference tableRef = new TableReference(oSchema, tableName); + List colRefs = columns.stream() + .map(col -> (ColumnReference) new ColumnReference(Optional.of(tableRef), col)).toList(); + return new UniqueConstraintRecord(constraintName, tableRef, colRefs); + } + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProviderFactory.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProviderFactory.java new file mode 100644 index 0000000..25ed44f --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/PostgreSqlMetadataProviderFactory.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.util.Locale; + +import org.eclipse.daanse.sql.jdbc.api.MetadataProvider; +import org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ServiceScope; + +/** The PostgreSQL {@link MetadataProviderFactory} (product-name substring match). */ +@Component(service = MetadataProviderFactory.class, scope = ServiceScope.SINGLETON) +public class PostgreSqlMetadataProviderFactory implements MetadataProviderFactory { + + @Override + public boolean supports(String databaseProductName) { + if (databaseProductName == null) { + return false; + } + String lower = databaseProductName.toLowerCase(Locale.ROOT); + return lower.contains("postgresql"); + } + + @Override + public MetadataProvider createProvider() { + return new PostgreSqlMetadataProvider(); + } +} diff --git a/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/package-info.java b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/package-info.java new file mode 100644 index 0000000..bbc4720 --- /dev/null +++ b/jdbc/metadata/src/main/java/org/eclipse/daanse/sql/jdbc/metadata/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +/** + * Engine-specific {@link org.eclipse.daanse.sql.jdbc.api.MetadataProvider} implementations — + * the {@code information_schema}/system-catalog readers formerly embedded in the SQL dialects — + * plus their {@link org.eclipse.daanse.sql.jdbc.api.MetadataProviderFactory} OSGi services and + * the static {@code MetadataProviders} resolver. Reading only: no SQL spelling, no dialect + * dependency. + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.jdbc.metadata; diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderTest.java new file mode 100644 index 0000000..aae7a8a --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/H2MetadataProviderTest.java @@ -0,0 +1,649 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +class H2MetadataProviderTest { + + private static Connection connection; + private static H2MetadataProvider provider; + private static final String SCHEMA = "PUBLIC"; + + @BeforeAll + static void setUp() throws Exception { + connection = DriverManager.getConnection("jdbc:h2:mem:h2MetadataTest;DB_CLOSE_DELAY=-1", "sa", ""); + try (Statement stmt = connection.createStatement()) { + // Sequences + stmt.execute(""" + CREATE SEQUENCE SEQ_ORDER_ID START WITH 1000 INCREMENT BY 1 + """); + stmt.execute(""" + CREATE SEQUENCE SEQ_AUDIT_ID START WITH 1 INCREMENT BY 5 + MINVALUE 1 MAXVALUE 999999 CYCLE CACHE 10 + """); + + // Tables + stmt.execute(""" + CREATE TABLE DEPARTMENTS ( + DEPT_ID INT NOT NULL, + DEPT_NAME VARCHAR(100) NOT NULL, + LOCATION VARCHAR(200), + CONSTRAINT PK_DEPARTMENTS PRIMARY KEY (DEPT_ID), + CONSTRAINT UQ_DEPT_NAME UNIQUE (DEPT_NAME), + CONSTRAINT CK_DEPT_NAME_LEN CHECK (LENGTH(DEPT_NAME) >= 2) + ) + """); + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL, + FIRST_NAME VARCHAR(50) NOT NULL, + LAST_NAME VARCHAR(50) NOT NULL, + EMAIL VARCHAR(100) NOT NULL, + SALARY DECIMAL(10,2), + DEPT_ID INT, + CONSTRAINT PK_EMPLOYEES PRIMARY KEY (EMP_ID), + CONSTRAINT UQ_EMP_EMAIL UNIQUE (EMAIL), + CONSTRAINT CK_SALARY_POSITIVE CHECK (SALARY > 0), + CONSTRAINT FK_EMP_DEPT FOREIGN KEY (DEPT_ID) + REFERENCES DEPARTMENTS(DEPT_ID) ON DELETE SET NULL ON UPDATE CASCADE + ) + """); + stmt.execute(""" + CREATE TABLE ORDERS ( + ORDER_ID INT NOT NULL, + EMP_ID INT NOT NULL, + ORDER_DATE DATE NOT NULL, + STATUS VARCHAR(20) DEFAULT 'PENDING', + CONSTRAINT PK_ORDERS PRIMARY KEY (ORDER_ID), + CONSTRAINT CK_STATUS CHECK (STATUS IN ('PENDING', 'PROCESSING', 'COMPLETED', 'CANCELLED')), + CONSTRAINT FK_ORD_EMP FOREIGN KEY (EMP_ID) REFERENCES EMPLOYEES(EMP_ID) + ) + """); + stmt.execute(""" + CREATE TABLE ORDER_ITEMS ( + ORDER_ID INT NOT NULL, + ITEM_SEQ INT NOT NULL, + PRODUCT_NAME VARCHAR(100) NOT NULL, + QUANTITY INT NOT NULL, + AMOUNT DECIMAL(10,2) NOT NULL, + CONSTRAINT PK_ORDER_ITEMS PRIMARY KEY (ORDER_ID, ITEM_SEQ), + CONSTRAINT CK_QUANTITY CHECK (QUANTITY > 0), + CONSTRAINT CK_AMOUNT_POSITIVE CHECK (AMOUNT > 0), + CONSTRAINT FK_OI_ORDER FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID) + ) + """); + stmt.execute(""" + CREATE TABLE AUDIT_LOG ( + LOG_ID INT NOT NULL, + TABLE_NAME VARCHAR(100) NOT NULL, + ACTION_TYPE VARCHAR(20) NOT NULL, + ACTION_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + USER_NAME VARCHAR(100), + CONSTRAINT PK_AUDIT_LOG PRIMARY KEY (LOG_ID), + CONSTRAINT UQ_AUDIT_UNIQUE UNIQUE (TABLE_NAME, ACTION_TYPE, ACTION_TIMESTAMP) + ) + """); + + // Indexes + stmt.execute("CREATE INDEX IDX_EMP_LAST_NAME ON EMPLOYEES(LAST_NAME)"); + stmt.execute("CREATE INDEX IDX_EMP_DEPT ON EMPLOYEES(DEPT_ID)"); + stmt.execute("CREATE INDEX IDX_ORD_DATE ON ORDERS(ORDER_DATE)"); + stmt.execute("CREATE UNIQUE INDEX IDX_ORD_EMP_DATE ON ORDERS(EMP_ID, ORDER_DATE)"); + + // Views + stmt.execute(""" + CREATE VIEW V_EMP_DEPT AS + SELECT E.EMP_ID, E.FIRST_NAME, E.LAST_NAME, E.EMAIL, E.SALARY, + D.DEPT_NAME, D.LOCATION + FROM EMPLOYEES E LEFT JOIN DEPARTMENTS D ON E.DEPT_ID = D.DEPT_ID + """); + stmt.execute(""" + CREATE VIEW V_ORDER_SUMMARY AS + SELECT O.ORDER_ID, O.ORDER_DATE, O.STATUS, + E.FIRST_NAME || ' ' || E.LAST_NAME AS EMP_NAME + FROM ORDERS O JOIN EMPLOYEES E ON O.EMP_ID = E.EMP_ID + """); + + // Triggers (H2 uses CALL "class.name") + stmt.execute(""" + CREATE TRIGGER TRG_EMP_AUDIT AFTER INSERT ON EMPLOYEES + FOR EACH ROW CALL "org.eclipse.daanse.sql.jdbc.metadata.TestAuditTrigger" + """); + stmt.execute(""" + CREATE TRIGGER TRG_ORD_AUDIT BEFORE UPDATE ON ORDERS + FOR EACH ROW CALL "org.eclipse.daanse.sql.jdbc.metadata.TestAuditTrigger" + """); + + // Functions (H2 uses CREATE ALIAS for Java-based functions) + stmt.execute(""" + CREATE ALIAS TO_UPPER AS $$ + String toUpper(String s) { + return s == null ? null : s.toUpperCase(); + } + $$ + """); + stmt.execute(""" + CREATE ALIAS ADD_NUMBERS AS $$ + int addNumbers(int a, int b) { + return a + b; + } + $$ + """); + } + provider = new H2MetadataProvider(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + try (Statement stmt = connection.createStatement()) { + stmt.execute("DROP ALL OBJECTS"); + } + connection.close(); + } + } + + @Test + void getAllTriggers_returnsTwoTriggers() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, SCHEMA); + assertThat(triggers).hasSize(2); + } + + @Test + void getAllTriggers_correctTimingAndEvent() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, SCHEMA); + + Trigger empTrigger = findTrigger(triggers, "TRG_EMP_AUDIT"); + assertThat(empTrigger.timing()).isEqualTo(TriggerTiming.AFTER); + assertThat(empTrigger.event()).isEqualTo(TriggerEvent.INSERT); + + Trigger ordTrigger = findTrigger(triggers, "TRG_ORD_AUDIT"); + assertThat(ordTrigger.timing()).isEqualTo(TriggerTiming.BEFORE); + assertThat(ordTrigger.event()).isEqualTo(TriggerEvent.UPDATE); + } + + @Test + void getAllTriggers_correctTable() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, SCHEMA); + + assertThat(findTrigger(triggers, "TRG_EMP_AUDIT").reference().table().name()).isEqualTo("EMPLOYEES"); + assertThat(findTrigger(triggers, "TRG_ORD_AUDIT").reference().table().name()).isEqualTo("ORDERS"); + } + + @Test + void getAllTriggers_bodyContainsClassName() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, SCHEMA); + + for (Trigger trigger : triggers) { + assertThat(trigger.body()).isPresent(); + assertThat(trigger.body().get()).containsIgnoringCase("TestAuditTrigger"); + } + } + + @Test + void getAllTriggers_orientationIsRow() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, SCHEMA); + + for (Trigger trigger : triggers) { + assertThat(trigger.orientation()).isPresent(); + assertThat(trigger.orientation().get()).isEqualToIgnoringCase("ROW"); + } + } + + @Test + void getTriggers_filtersByTable() throws SQLException { + List empTriggers = provider.getTriggers(connection, null, SCHEMA, "EMPLOYEES"); + assertThat(empTriggers).hasSize(1); + assertThat(empTriggers.get(0).name()).isEqualTo("TRG_EMP_AUDIT"); + + List ordTriggers = provider.getTriggers(connection, null, SCHEMA, "ORDERS"); + assertThat(ordTriggers).hasSize(1); + assertThat(ordTriggers.get(0).name()).isEqualTo("TRG_ORD_AUDIT"); + } + + @Test + void getTriggers_emptyForTableWithNoTriggers() throws SQLException { + List triggers = provider.getTriggers(connection, null, SCHEMA, "DEPARTMENTS"); + assertThat(triggers).isEmpty(); + } + + @Test + void getAllSequences_returnsAtLeastTwo() throws SQLException { + List sequences = provider.getAllSequences(connection, null, SCHEMA); + assertThat(sequences).hasSizeGreaterThanOrEqualTo(2); + } + + @Test + void getAllSequences_seqOrderIdProperties() throws SQLException { + List sequences = provider.getAllSequences(connection, null, SCHEMA); + Sequence seq = findSequence(sequences, "SEQ_ORDER_ID"); + + assertThat(seq.startValue()).isEqualTo(1000L); + assertThat(seq.incrementBy()).isEqualTo(1L); + assertThat(seq.cycle()).isFalse(); + } + + @Test + void getAllSequences_seqAuditIdProperties() throws SQLException { + List sequences = provider.getAllSequences(connection, null, SCHEMA); + Sequence seq = findSequence(sequences, "SEQ_AUDIT_ID"); + + assertThat(seq.startValue()).isEqualTo(1L); + assertThat(seq.incrementBy()).isEqualTo(5L); + assertThat(seq.minValue()).isPresent().hasValue(1L); + assertThat(seq.maxValue()).isPresent().hasValue(999999L); + assertThat(seq.cycle()).isTrue(); + assertThat(seq.cacheSize()).isPresent().hasValue(10L); + } + + @Test + void getAllSequences_schemaIsPublic() throws SQLException { + List sequences = provider.getAllSequences(connection, null, SCHEMA); + + for (Sequence seq : sequences) { + assertThat(seq.schema()).isPresent(); + assertThat(seq.schema().get().name()).isEqualTo("PUBLIC"); + } + } + + @Test + void getAllCheckConstraints_returnsNamedConstraints() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, SCHEMA); + + List namedConstraints = constraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + + assertThat(namedConstraints).containsExactlyInAnyOrder("CK_DEPT_NAME_LEN", "CK_SALARY_POSITIVE", "CK_STATUS", + "CK_QUANTITY", "CK_AMOUNT_POSITIVE"); + } + + @Test + void getAllCheckConstraints_checkClauseContent() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, SCHEMA); + + CheckConstraint salaryCheck = constraints.stream().filter(c -> "CK_SALARY_POSITIVE".equals(c.name())) + .findFirst().orElseThrow(); + assertThat(salaryCheck.checkClause()).containsIgnoringCase("SALARY"); + + CheckConstraint statusCheck = constraints.stream().filter(c -> "CK_STATUS".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(statusCheck.checkClause()).containsIgnoringCase("STATUS"); + } + + @Test + void getAllCheckConstraints_tableAssociation() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, SCHEMA); + + CheckConstraint salaryCheck = constraints.stream().filter(c -> "CK_SALARY_POSITIVE".equals(c.name())) + .findFirst().orElseThrow(); + assertThat(salaryCheck.table().name()).isEqualTo("EMPLOYEES"); + + CheckConstraint statusCheck = constraints.stream().filter(c -> "CK_STATUS".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(statusCheck.table().name()).isEqualTo("ORDERS"); + } + + @Test + void getCheckConstraints_filtersByTable() throws SQLException { + List empConstraints = provider.getCheckConstraints(connection, null, SCHEMA, "EMPLOYEES"); + + List namedEmp = empConstraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + assertThat(namedEmp).containsExactly("CK_SALARY_POSITIVE"); + + List orderItemConstraints = provider.getCheckConstraints(connection, null, SCHEMA, + "ORDER_ITEMS"); + + List namedOI = orderItemConstraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + assertThat(namedOI).containsExactlyInAnyOrder("CK_QUANTITY", "CK_AMOUNT_POSITIVE"); + } + + @Test + void getAllUniqueConstraints_returnsAtLeastThree() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, SCHEMA); + assertThat(constraints).hasSizeGreaterThanOrEqualTo(3); + } + + @Test + void getAllUniqueConstraints_singleColumn() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, SCHEMA); + + UniqueConstraint uqDeptName = constraints.stream().filter(c -> "UQ_DEPT_NAME".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(uqDeptName.table().name()).isEqualTo("DEPARTMENTS"); + assertThat(uqDeptName.columns()).hasSize(1); + assertThat(uqDeptName.columns().get(0).name()).isEqualTo("DEPT_NAME"); + } + + @Test + void getAllUniqueConstraints_multiColumn() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, SCHEMA); + + UniqueConstraint uqAudit = constraints.stream().filter(c -> "UQ_AUDIT_UNIQUE".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(uqAudit.table().name()).isEqualTo("AUDIT_LOG"); + assertThat(uqAudit.columns()).hasSize(3); + + List columnNames = uqAudit.columns().stream().map(ColumnReference::name).toList(); + assertThat(columnNames).containsExactly("TABLE_NAME", "ACTION_TYPE", "ACTION_TIMESTAMP"); + } + + @Test + void getUniqueConstraints_filtersByTable() throws SQLException { + List deptConstraints = provider.getUniqueConstraints(connection, null, SCHEMA, "DEPARTMENTS"); + assertThat(deptConstraints).hasSize(1); + assertThat(deptConstraints.get(0).name()).isEqualTo("UQ_DEPT_NAME"); + + List orderConstraints = provider.getUniqueConstraints(connection, null, SCHEMA, "ORDERS"); + assertThat(orderConstraints).isEmpty(); + } + + @Test + void getAllPrimaryKeys_isPresent() throws SQLException { + Optional> result = provider.getAllPrimaryKeys(connection, null, SCHEMA); + assertThat(result).isPresent(); + } + + @Test + void getAllPrimaryKeys_returnsFive() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, SCHEMA).orElseThrow(); + assertThat(primaryKeys).hasSize(5); + } + + @Test + void getAllPrimaryKeys_simplePrimaryKey() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, SCHEMA).orElseThrow(); + + PrimaryKey deptPk = primaryKeys.stream().filter(pk -> "DEPARTMENTS".equals(pk.table().name())).findFirst() + .orElseThrow(); + assertThat(deptPk.constraintName()).isPresent().hasValue("PK_DEPARTMENTS"); + assertThat(deptPk.columns()).hasSize(1); + assertThat(deptPk.columns().get(0).name()).isEqualTo("DEPT_ID"); + } + + @Test + void getAllPrimaryKeys_compositePrimaryKey() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, SCHEMA).orElseThrow(); + + PrimaryKey oiPk = primaryKeys.stream().filter(pk -> "ORDER_ITEMS".equals(pk.table().name())).findFirst() + .orElseThrow(); + assertThat(oiPk.constraintName()).isPresent().hasValue("PK_ORDER_ITEMS"); + assertThat(oiPk.columns()).hasSize(2); + + List columnNames = oiPk.columns().stream().map(ColumnReference::name).toList(); + assertThat(columnNames).containsExactly("ORDER_ID", "ITEM_SEQ"); + } + + @Test + void getAllImportedKeys_isPresent() throws SQLException { + Optional> result = provider.getAllImportedKeys(connection, null, SCHEMA); + assertThat(result).isPresent(); + } + + @Test + void getAllImportedKeys_returnsThree() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, SCHEMA).orElseThrow(); + assertThat(importedKeys).hasSize(3); + } + + @Test + void getAllImportedKeys_fkEmpDeptDetails() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, SCHEMA).orElseThrow(); + + ImportedKey fkEmpDept = importedKeys.stream().filter(fk -> "FK_EMP_DEPT".equals(fk.name())).findFirst() + .orElseThrow(); + + assertThat(fkEmpDept.foreignKeyColumn().name()).isEqualTo("DEPT_ID"); + assertThat(fkEmpDept.foreignKeyColumn().table()).isPresent(); + assertThat(fkEmpDept.foreignKeyColumn().table().get().name()).isEqualTo("EMPLOYEES"); + + assertThat(fkEmpDept.primaryKeyColumn().name()).isEqualTo("DEPT_ID"); + assertThat(fkEmpDept.primaryKeyColumn().table()).isPresent(); + assertThat(fkEmpDept.primaryKeyColumn().table().get().name()).isEqualTo("DEPARTMENTS"); + + assertThat(fkEmpDept.deleteRule()).isEqualTo(ImportedKey.ReferentialAction.SET_NULL); + assertThat(fkEmpDept.updateRule()).isEqualTo(ImportedKey.ReferentialAction.CASCADE); + } + + @Test + void getAllImportedKeys_keySequence() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, SCHEMA).orElseThrow(); + + for (ImportedKey fk : importedKeys) { + assertThat(fk.keySequence()).isGreaterThanOrEqualTo(1); + } + } + + @Test + void getAllIndexInfo_isPresent() throws SQLException { + Optional> result = provider.getAllIndexInfo(connection, null, SCHEMA); + assertThat(result).isPresent(); + } + + @Test + void getAllIndexInfo_containsUserIndexes() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, SCHEMA).orElseThrow(); + + List allIndexNames = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .map(IndexInfoItem::indexName).filter(Optional::isPresent).map(Optional::get).toList(); + + assertThat(allIndexNames).contains("IDX_EMP_LAST_NAME", "IDX_EMP_DEPT", "IDX_ORD_DATE", "IDX_ORD_EMP_DATE"); + } + + @Test + void getAllIndexInfo_nonUniqueIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, SCHEMA).orElseThrow(); + + IndexInfoItem lastNameIdx = findIndexItem(indexInfos, "IDX_EMP_LAST_NAME"); + assertThat(lastNameIdx.unique()).isFalse(); + assertThat(lastNameIdx.column()).isPresent(); + assertThat(lastNameIdx.column().get().name()).isEqualTo("LAST_NAME"); + } + + @Test + void getAllIndexInfo_uniqueIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, SCHEMA).orElseThrow(); + + List empDateItems = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && "IDX_ORD_EMP_DATE".equals(item.indexName().get())) + .toList(); + + assertThat(empDateItems).isNotEmpty(); + assertThat(empDateItems.get(0).unique()).isTrue(); + } + + @Test + void getAllIndexInfo_multiColumnIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, SCHEMA).orElseThrow(); + + List empDateItems = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && "IDX_ORD_EMP_DATE".equals(item.indexName().get())) + .toList(); + + assertThat(empDateItems).hasSize(2); + + List columnNames = empDateItems.stream().map(item -> item.column().map(c -> c.name()).orElse("")) + .toList(); + assertThat(columnNames).containsExactly("EMP_ID", "ORDER_DATE"); + } + + @Test + void getAllIndexInfo_groupedByTable() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, SCHEMA).orElseThrow(); + + List tableNames = indexInfos.stream().map(ii -> ii.tableReference().name()).toList(); + + // All 5 user tables should have indexes (at least PK indexes) + assertThat(tableNames).contains("DEPARTMENTS", "EMPLOYEES", "ORDERS", "ORDER_ITEMS", "AUDIT_LOG"); + } + + @Test + void getAllViewDefinitions_returnsTwo() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, SCHEMA); + assertThat(views).hasSize(2); + } + + @Test + void getAllViewDefinitions_bodyIsPresent() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, SCHEMA); + + for (ViewDefinition view : views) { + assertThat(view.viewBody()).isPresent(); + assertThat(view.viewBody().get()).isNotBlank(); + } + } + + @Test + void getAllViewDefinitions_bodyContainsSelect() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, SCHEMA); + + for (ViewDefinition view : views) { + assertThat(view.viewBody().get()).containsIgnoringCase("SELECT"); + } + } + + @Test + void getAllViewDefinitions_viewType() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, SCHEMA); + + List viewNames = views.stream().map(v -> v.view().name()).toList(); + assertThat(viewNames).containsExactlyInAnyOrder("V_EMP_DEPT", "V_ORDER_SUMMARY"); + + for (ViewDefinition view : views) { + assertThat(view.view().type()).isEqualTo("VIEW"); + } + } + + @Test + void getAllProcedures_returnsEmpty() throws SQLException { + // H2 does not have stored procedures in INFORMATION_SCHEMA.ROUTINES by default + // (CREATE ALIAS creates functions, not procedures) + List procedures = provider.getAllProcedures(connection, null, SCHEMA); + assertThat(procedures).isNotNull(); + } + + @Test + void getAllFunctions_returnsAliases() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + assertThat(functions).isNotEmpty(); + } + + @Test + void getAllFunctions_containsToUpper() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + assertThat(functions).anyMatch(f -> "TO_UPPER".equals(f.reference().name())); + } + + @Test + void getAllFunctions_containsAddNumbers() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + assertThat(functions).anyMatch(f -> "ADD_NUMBERS".equals(f.reference().name())); + } + + @Test + void getAllFunctions_addNumbersHasParameters() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + Function addNumbers = functions.stream().filter(f -> "ADD_NUMBERS".equals(f.reference().name())).findFirst() + .orElseThrow(); + assertThat(addNumbers.columns()).hasSizeGreaterThanOrEqualTo(2); + } + + @Test + void getAllFunctions_schemaIsPublic() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + for (Function func : functions) { + assertThat(func.reference().schema()).isPresent(); + assertThat(func.reference().schema().get().name()).isEqualTo("PUBLIC"); + } + } + + @Test + void getAllFunctions_bodyIsPresent() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + // H2 stores the Java source of CREATE ALIAS functions in ROUTINE_DEFINITION. + Function toUpper = functions.stream().filter(f -> "TO_UPPER".equals(f.reference().name())).findFirst() + .orElseThrow(); + assertThat(toUpper.body()).isPresent(); + assertThat(toUpper.body().get()).containsIgnoringCase("toUpperCase"); + } + + @Test + void getAllFunctions_addNumbersBodyContainsReturn() throws SQLException { + List functions = provider.getAllFunctions(connection, null, SCHEMA); + Function addNumbers = functions.stream().filter(f -> "ADD_NUMBERS".equals(f.reference().name())).findFirst() + .orElseThrow(); + assertThat(addNumbers.body()).isPresent(); + assertThat(addNumbers.body().get()).contains("return"); + } + + @Test + void getAllFunctions_fullDefinitionEmpty_forH2() throws SQLException { + // H2's INFORMATION_SCHEMA.ROUTINES has no full CREATE DDL; the dialect reports + // empty. + List functions = provider.getAllFunctions(connection, null, SCHEMA); + for (Function func : functions) { + assertThat(func.fullDefinition()).isNotPresent(); + } + } + + @Test + void getAllUserDefinedTypes_returnsEmpty() throws SQLException { + List udts = provider.getAllUserDefinedTypes(connection, null, SCHEMA); + assertThat(udts).isEmpty(); + } + + private static Trigger findTrigger(List triggers, String name) { + return triggers.stream().filter(t -> name.equals(t.name())).findFirst() + .orElseThrow(() -> new AssertionError("Trigger not found: " + name)); + } + + private static Sequence findSequence(List sequences, String name) { + return sequences.stream().filter(s -> name.equals(s.name())).findFirst() + .orElseThrow(() -> new AssertionError("Sequence not found: " + name)); + } + + private static IndexInfoItem findIndexItem(List indexInfos, String indexName) { + return indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && indexName.equals(item.indexName().get())).findFirst() + .orElseThrow(() -> new AssertionError("Index not found: " + indexName)); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDBMetadataProviderTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDBMetadataProviderTest.java new file mode 100644 index 0000000..ea57300 --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MariaDBMetadataProviderTest.java @@ -0,0 +1,712 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +class MariaDBMetadataProviderTest { + + private static final String DATABASE = "testdb"; + private static final String USER = "root"; + private static final String PASSWORD = "test"; + + @SuppressWarnings("resource") + private static final GenericContainer MARIADB = new GenericContainer<>("mariadb:10.11") + .withEnv("MARIADB_ROOT_PASSWORD", PASSWORD).withEnv("MARIADB_DATABASE", DATABASE).withExposedPorts(3306) + .waitingFor( + Wait.forLogMessage(".*ready for connections.*\\n", 2).withStartupTimeout(Duration.ofMinutes(2))); + + private static Connection connection; + private static MariaDbMetadataProvider provider; + + @BeforeAll + static void setUp() throws Exception { + MARIADB.start(); + + String host = MARIADB.getHost(); + int port = MARIADB.getMappedPort(3306); + String jdbcUrl = "jdbc:mariadb://" + host + ":" + port + "/" + DATABASE; + + Class.forName("org.mariadb.jdbc.Driver"); + connection = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); + + try (Statement stmt = connection.createStatement()) { + // Sequences (MariaDB 10.3+) + stmt.execute("CREATE SEQUENCE SEQ_ORDER_ID START WITH 1000 INCREMENT BY 1"); + stmt.execute(""" + CREATE SEQUENCE SEQ_AUDIT_ID START WITH 1 INCREMENT BY 5 + MINVALUE 1 MAXVALUE 999999 CYCLE CACHE 10 + """); + + // Tables + stmt.execute(""" + CREATE TABLE DEPARTMENTS ( + DEPT_ID INT NOT NULL, + DEPT_NAME VARCHAR(100) NOT NULL, + LOCATION VARCHAR(200), + CONSTRAINT PK_DEPARTMENTS PRIMARY KEY (DEPT_ID), + CONSTRAINT UQ_DEPT_NAME UNIQUE (DEPT_NAME), + CONSTRAINT CK_DEPT_NAME_LEN CHECK (CHAR_LENGTH(DEPT_NAME) >= 2) + ) + """); + stmt.execute(""" + CREATE TABLE EMPLOYEES ( + EMP_ID INT NOT NULL, + FIRST_NAME VARCHAR(50) NOT NULL, + LAST_NAME VARCHAR(50) NOT NULL, + EMAIL VARCHAR(100) NOT NULL, + SALARY DECIMAL(10,2), + DEPT_ID INT, + CONSTRAINT PK_EMPLOYEES PRIMARY KEY (EMP_ID), + CONSTRAINT UQ_EMP_EMAIL UNIQUE (EMAIL), + CONSTRAINT CK_SALARY_POSITIVE CHECK (SALARY > 0), + CONSTRAINT FK_EMP_DEPT FOREIGN KEY (DEPT_ID) + REFERENCES DEPARTMENTS(DEPT_ID) ON DELETE SET NULL ON UPDATE CASCADE + ) + """); + stmt.execute(""" + CREATE TABLE ORDERS ( + ORDER_ID INT NOT NULL, + EMP_ID INT NOT NULL, + ORDER_DATE DATE NOT NULL, + STATUS VARCHAR(20) DEFAULT 'PENDING', + CONSTRAINT PK_ORDERS PRIMARY KEY (ORDER_ID), + CONSTRAINT CK_STATUS CHECK (STATUS IN ('PENDING', 'PROCESSING', 'COMPLETED', 'CANCELLED')), + CONSTRAINT FK_ORD_EMP FOREIGN KEY (EMP_ID) REFERENCES EMPLOYEES(EMP_ID) + ) + """); + stmt.execute(""" + CREATE TABLE ORDER_ITEMS ( + ORDER_ID INT NOT NULL, + ITEM_SEQ INT NOT NULL, + PRODUCT_NAME VARCHAR(100) NOT NULL, + QUANTITY INT NOT NULL, + AMOUNT DECIMAL(10,2) NOT NULL, + CONSTRAINT PK_ORDER_ITEMS PRIMARY KEY (ORDER_ID, ITEM_SEQ), + CONSTRAINT CK_QUANTITY CHECK (QUANTITY > 0), + CONSTRAINT CK_AMOUNT_POSITIVE CHECK (AMOUNT > 0), + CONSTRAINT FK_OI_ORDER FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID) + ) + """); + stmt.execute(""" + CREATE TABLE AUDIT_LOG ( + LOG_ID INT NOT NULL, + TABLE_NAME VARCHAR(100) NOT NULL, + ACTION_TYPE VARCHAR(20) NOT NULL, + ACTION_TIMESTAMP TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + USER_NAME VARCHAR(100), + CONSTRAINT PK_AUDIT_LOG PRIMARY KEY (LOG_ID), + CONSTRAINT UQ_AUDIT_UNIQUE UNIQUE (TABLE_NAME, ACTION_TYPE, ACTION_TIMESTAMP) + ) + """); + + // Indexes + stmt.execute("CREATE INDEX IDX_EMP_LAST_NAME ON EMPLOYEES(LAST_NAME)"); + stmt.execute("CREATE INDEX IDX_EMP_DEPT ON EMPLOYEES(DEPT_ID)"); + stmt.execute("CREATE INDEX IDX_ORD_DATE ON ORDERS(ORDER_DATE)"); + stmt.execute("CREATE UNIQUE INDEX IDX_ORD_EMP_DATE ON ORDERS(EMP_ID, ORDER_DATE)"); + + // Views + stmt.execute(""" + CREATE VIEW V_EMP_DEPT AS + SELECT E.EMP_ID, E.FIRST_NAME, E.LAST_NAME, E.EMAIL, E.SALARY, + D.DEPT_NAME, D.LOCATION + FROM EMPLOYEES E LEFT JOIN DEPARTMENTS D ON E.DEPT_ID = D.DEPT_ID + """); + stmt.execute(""" + CREATE VIEW V_ORDER_SUMMARY AS + SELECT O.ORDER_ID, O.ORDER_DATE, O.STATUS, + CONCAT(E.FIRST_NAME, ' ', E.LAST_NAME) AS EMP_NAME + FROM ORDERS O JOIN EMPLOYEES E ON O.EMP_ID = E.EMP_ID + """); + + // Triggers (MariaDB syntax) + stmt.execute(""" + CREATE TRIGGER TRG_EMP_AUDIT AFTER INSERT ON EMPLOYEES + FOR EACH ROW + INSERT INTO AUDIT_LOG (LOG_ID, TABLE_NAME, ACTION_TYPE, USER_NAME) + VALUES (NEW.EMP_ID, 'EMPLOYEES', 'INSERT', CURRENT_USER()) + """); + stmt.execute(""" + CREATE TRIGGER TRG_ORD_AUDIT BEFORE UPDATE ON ORDERS + FOR EACH ROW + INSERT INTO AUDIT_LOG (LOG_ID, TABLE_NAME, ACTION_TYPE, USER_NAME) + VALUES (NEW.ORDER_ID, 'ORDERS', 'UPDATE', CURRENT_USER()) + """); + + // Functions + stmt.execute(""" + CREATE FUNCTION TO_UPPER(s VARCHAR(255)) + RETURNS VARCHAR(255) DETERMINISTIC + RETURN UPPER(s) + """); + stmt.execute(""" + CREATE FUNCTION ADD_NUMBERS(a INT, b INT) + RETURNS INT DETERMINISTIC + RETURN a + b + """); + + // Procedures + stmt.execute(""" + CREATE PROCEDURE INSERT_DEPT(IN p_id INT, IN p_name VARCHAR(100)) + INSERT INTO DEPARTMENTS(DEPT_ID, DEPT_NAME) VALUES (p_id, p_name) + """); + + // Partitioned table — RANGE partitioning by year-from-date + stmt.execute(""" + CREATE TABLE SALES_BY_YEAR ( + SALE_ID INT NOT NULL, + SALE_DATE DATE NOT NULL, + AMOUNT DECIMAL(10,2) NOT NULL, + PRIMARY KEY (SALE_ID, SALE_DATE) + ) + PARTITION BY RANGE (YEAR(SALE_DATE)) ( + PARTITION p2022 VALUES LESS THAN (2023), + PARTITION p2023 VALUES LESS THAN (2024), + PARTITION p2024 VALUES LESS THAN (2025), + PARTITION pmax VALUES LESS THAN MAXVALUE + ) + """); + } + provider = new MariaDbMetadataProvider(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + MARIADB.stop(); + } + + @Test + void getAllTriggers_returnsTwoTriggers() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, DATABASE); + assertThat(triggers).hasSize(2); + } + + @Test + void getAllTriggers_correctTimingAndEvent() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, DATABASE); + + Trigger empTrigger = findTrigger(triggers, "TRG_EMP_AUDIT"); + assertThat(empTrigger.timing()).isEqualTo(TriggerTiming.AFTER); + assertThat(empTrigger.event()).isEqualTo(TriggerEvent.INSERT); + + Trigger ordTrigger = findTrigger(triggers, "TRG_ORD_AUDIT"); + assertThat(ordTrigger.timing()).isEqualTo(TriggerTiming.BEFORE); + assertThat(ordTrigger.event()).isEqualTo(TriggerEvent.UPDATE); + } + + @Test + void getAllTriggers_correctTable() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, DATABASE); + + assertThat(findTrigger(triggers, "TRG_EMP_AUDIT").table().name()).isEqualToIgnoringCase("EMPLOYEES"); + assertThat(findTrigger(triggers, "TRG_ORD_AUDIT").table().name()).isEqualToIgnoringCase("ORDERS"); + } + + @Test + void getAllTriggers_bodyContainsAction() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, DATABASE); + + for (Trigger trigger : triggers) { + assertThat(trigger.body()).isPresent(); + assertThat(trigger.body().get()).containsIgnoringCase("AUDIT_LOG"); + } + } + + @Test + void getAllTriggers_orientationIsRow() throws SQLException { + List triggers = provider.getAllTriggers(connection, null, DATABASE); + + for (Trigger trigger : triggers) { + assertThat(trigger.orientation()).isPresent(); + assertThat(trigger.orientation().get()).isEqualToIgnoringCase("ROW"); + } + } + + @Test + void getTriggers_filtersByTable() throws SQLException { + List empTriggers = provider.getTriggers(connection, null, DATABASE, "EMPLOYEES"); + assertThat(empTriggers).hasSize(1); + assertThat(empTriggers.get(0).name()).isEqualToIgnoringCase("TRG_EMP_AUDIT"); + + List ordTriggers = provider.getTriggers(connection, null, DATABASE, "ORDERS"); + assertThat(ordTriggers).hasSize(1); + assertThat(ordTriggers.get(0).name()).isEqualToIgnoringCase("TRG_ORD_AUDIT"); + } + + @Test + void getTriggers_emptyForTableWithNoTriggers() throws SQLException { + List triggers = provider.getTriggers(connection, null, DATABASE, "DEPARTMENTS"); + assertThat(triggers).isEmpty(); + } + + @Test + void getAllSequences_returnsAtLeastTwo() throws SQLException { + List sequences = provider.getAllSequences(connection, null, DATABASE); + assertThat(sequences).hasSizeGreaterThanOrEqualTo(2); + } + + @Test + void getAllSequences_seqOrderIdProperties() throws SQLException { + List sequences = provider.getAllSequences(connection, null, DATABASE); + Sequence seq = findSequence(sequences, "SEQ_ORDER_ID"); + + assertThat(seq.startValue()).isEqualTo(1000L); + assertThat(seq.incrementBy()).isEqualTo(1L); + assertThat(seq.cycle()).isFalse(); + } + + @Test + void getAllSequences_seqAuditIdProperties() throws SQLException { + List sequences = provider.getAllSequences(connection, null, DATABASE); + Sequence seq = findSequence(sequences, "SEQ_AUDIT_ID"); + + assertThat(seq.startValue()).isEqualTo(1L); + assertThat(seq.incrementBy()).isEqualTo(5L); + assertThat(seq.minValue()).isPresent().hasValue(1L); + assertThat(seq.maxValue()).isPresent().hasValue(999999L); + assertThat(seq.cycle()).isTrue(); + assertThat(seq.cacheSize()).isPresent().hasValue(10L); + } + + @Test + void getAllSequences_schemaIsDatabase() throws SQLException { + List sequences = provider.getAllSequences(connection, null, DATABASE); + + for (Sequence seq : sequences) { + assertThat(seq.schema()).isPresent(); + assertThat(seq.schema().get().name()).isEqualTo(DATABASE); + } + } + + @Test + void getAllCheckConstraints_returnsNamedConstraints() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, DATABASE); + + List namedConstraints = constraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + + assertThat(namedConstraints).containsExactlyInAnyOrder("CK_DEPT_NAME_LEN", "CK_SALARY_POSITIVE", "CK_STATUS", + "CK_QUANTITY", "CK_AMOUNT_POSITIVE"); + } + + @Test + void getAllCheckConstraints_checkClauseContent() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, DATABASE); + + CheckConstraint salaryCheck = constraints.stream().filter(c -> "CK_SALARY_POSITIVE".equals(c.name())) + .findFirst().orElseThrow(); + assertThat(salaryCheck.checkClause()).containsIgnoringCase("SALARY"); + + CheckConstraint statusCheck = constraints.stream().filter(c -> "CK_STATUS".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(statusCheck.checkClause()).containsIgnoringCase("STATUS"); + } + + @Test + void getAllCheckConstraints_tableAssociation() throws SQLException { + List constraints = provider.getAllCheckConstraints(connection, null, DATABASE); + + CheckConstraint salaryCheck = constraints.stream().filter(c -> "CK_SALARY_POSITIVE".equals(c.name())) + .findFirst().orElseThrow(); + assertThat(salaryCheck.table().name()).isEqualToIgnoringCase("EMPLOYEES"); + + CheckConstraint statusCheck = constraints.stream().filter(c -> "CK_STATUS".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(statusCheck.table().name()).isEqualToIgnoringCase("ORDERS"); + } + + @Test + void getCheckConstraints_filtersByTable() throws SQLException { + List empConstraints = provider.getCheckConstraints(connection, null, DATABASE, "EMPLOYEES"); + + List namedEmp = empConstraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + assertThat(namedEmp).containsExactly("CK_SALARY_POSITIVE"); + + List orderItemConstraints = provider.getCheckConstraints(connection, null, DATABASE, + "ORDER_ITEMS"); + + List namedOI = orderItemConstraints.stream().map(CheckConstraint::name) + .filter(name -> name.startsWith("CK_")).toList(); + assertThat(namedOI).containsExactlyInAnyOrder("CK_QUANTITY", "CK_AMOUNT_POSITIVE"); + } + + @Test + void getAllUniqueConstraints_returnsAtLeastThree() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, DATABASE); + assertThat(constraints).hasSizeGreaterThanOrEqualTo(3); + } + + @Test + void getAllUniqueConstraints_singleColumn() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, DATABASE); + + UniqueConstraint uqDeptName = constraints.stream().filter(c -> "UQ_DEPT_NAME".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(uqDeptName.table().name()).isEqualToIgnoringCase("DEPARTMENTS"); + assertThat(uqDeptName.columns()).hasSize(1); + assertThat(uqDeptName.columns().get(0).name()).isEqualToIgnoringCase("DEPT_NAME"); + } + + @Test + void getAllUniqueConstraints_multiColumn() throws SQLException { + List constraints = provider.getAllUniqueConstraints(connection, null, DATABASE); + + UniqueConstraint uqAudit = constraints.stream().filter(c -> "UQ_AUDIT_UNIQUE".equals(c.name())).findFirst() + .orElseThrow(); + assertThat(uqAudit.table().name()).isEqualToIgnoringCase("AUDIT_LOG"); + assertThat(uqAudit.columns()).hasSize(3); + + List columnNames = uqAudit.columns().stream().map(ColumnReference::name).map(String::toUpperCase) + .toList(); + assertThat(columnNames).containsExactly("TABLE_NAME", "ACTION_TYPE", "ACTION_TIMESTAMP"); + } + + @Test + void getUniqueConstraints_filtersByTable() throws SQLException { + List deptConstraints = provider.getUniqueConstraints(connection, null, DATABASE, + "DEPARTMENTS"); + assertThat(deptConstraints).hasSize(1); + assertThat(deptConstraints.get(0).name()).isEqualTo("UQ_DEPT_NAME"); + + // In MariaDB, a UNIQUE INDEX is also represented as a unique constraint in + // information_schema.TABLE_CONSTRAINTS, so ORDERS carries the IDX_ORD_EMP_DATE + // unique index as a single unique constraint. + List orderConstraints = provider.getUniqueConstraints(connection, null, DATABASE, "ORDERS"); + assertThat(orderConstraints).hasSize(1); + assertThat(orderConstraints.get(0).name()).isEqualTo("IDX_ORD_EMP_DATE"); + } + + @Test + void getAllPrimaryKeys_isPresent() throws SQLException { + Optional> result = provider.getAllPrimaryKeys(connection, null, DATABASE); + assertThat(result).isPresent(); + } + + @Test + void getAllPrimaryKeys_returnsSix() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, DATABASE).orElseThrow(); + assertThat(primaryKeys).hasSize(6); + } + + @Test + void getAllPrimaryKeys_simplePrimaryKey() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, DATABASE).orElseThrow(); + + PrimaryKey deptPk = primaryKeys.stream().filter(pk -> "DEPARTMENTS".equalsIgnoreCase(pk.table().name())) + .findFirst().orElseThrow(); + assertThat(deptPk.columns()).hasSize(1); + assertThat(deptPk.columns().get(0).name()).isEqualToIgnoringCase("DEPT_ID"); + } + + @Test + void getAllPrimaryKeys_compositePrimaryKey() throws SQLException { + List primaryKeys = provider.getAllPrimaryKeys(connection, null, DATABASE).orElseThrow(); + + PrimaryKey oiPk = primaryKeys.stream().filter(pk -> "ORDER_ITEMS".equalsIgnoreCase(pk.table().name())) + .findFirst().orElseThrow(); + assertThat(oiPk.columns()).hasSize(2); + + List columnNames = oiPk.columns().stream().map(ColumnReference::name).map(String::toUpperCase).toList(); + assertThat(columnNames).containsExactly("ORDER_ID", "ITEM_SEQ"); + } + + @Test + void getAllImportedKeys_isPresent() throws SQLException { + Optional> result = provider.getAllImportedKeys(connection, null, DATABASE); + assertThat(result).isPresent(); + } + + @Test + void getAllImportedKeys_returnsThree() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, DATABASE).orElseThrow(); + assertThat(importedKeys).hasSize(3); + } + + @Test + void getAllImportedKeys_fkEmpDeptDetails() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, DATABASE).orElseThrow(); + + ImportedKey fkEmpDept = importedKeys.stream().filter(fk -> "FK_EMP_DEPT".equals(fk.name())).findFirst() + .orElseThrow(); + + assertThat(fkEmpDept.foreignKeyColumn().name()).isEqualToIgnoringCase("DEPT_ID"); + assertThat(fkEmpDept.foreignKeyColumn().table()).isPresent(); + assertThat(fkEmpDept.foreignKeyColumn().table().get().name()).isEqualToIgnoringCase("EMPLOYEES"); + + assertThat(fkEmpDept.primaryKeyColumn().name()).isEqualToIgnoringCase("DEPT_ID"); + assertThat(fkEmpDept.primaryKeyColumn().table()).isPresent(); + assertThat(fkEmpDept.primaryKeyColumn().table().get().name()).isEqualToIgnoringCase("DEPARTMENTS"); + + assertThat(fkEmpDept.deleteRule()).isEqualTo(ImportedKey.ReferentialAction.SET_NULL); + assertThat(fkEmpDept.updateRule()).isEqualTo(ImportedKey.ReferentialAction.CASCADE); + } + + @Test + void getAllImportedKeys_keySequence() throws SQLException { + List importedKeys = provider.getAllImportedKeys(connection, null, DATABASE).orElseThrow(); + + for (ImportedKey fk : importedKeys) { + assertThat(fk.keySequence()).isGreaterThanOrEqualTo(1); + } + } + + @Test + void getAllIndexInfo_isPresent() throws SQLException { + Optional> result = provider.getAllIndexInfo(connection, null, DATABASE); + assertThat(result).isPresent(); + } + + @Test + void getAllIndexInfo_containsUserIndexes() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, DATABASE).orElseThrow(); + + List allIndexNames = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .map(IndexInfoItem::indexName).filter(Optional::isPresent).map(Optional::get).toList(); + + assertThat(allIndexNames).contains("IDX_EMP_LAST_NAME", "IDX_EMP_DEPT", "IDX_ORD_DATE", "IDX_ORD_EMP_DATE"); + } + + @Test + void getAllIndexInfo_nonUniqueIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, DATABASE).orElseThrow(); + + IndexInfoItem lastNameIdx = findIndexItem(indexInfos, "IDX_EMP_LAST_NAME"); + assertThat(lastNameIdx.unique()).isFalse(); + assertThat(lastNameIdx.column()).isPresent(); + assertThat(lastNameIdx.column().get().name()).isEqualToIgnoringCase("LAST_NAME"); + } + + @Test + void getAllIndexInfo_uniqueIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, DATABASE).orElseThrow(); + + List empDateItems = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && "IDX_ORD_EMP_DATE".equals(item.indexName().get())) + .toList(); + + assertThat(empDateItems).isNotEmpty(); + assertThat(empDateItems.get(0).unique()).isTrue(); + } + + @Test + void getAllIndexInfo_multiColumnIndex() throws SQLException { + List indexInfos = provider.getAllIndexInfo(connection, null, DATABASE).orElseThrow(); + + List empDateItems = indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && "IDX_ORD_EMP_DATE".equals(item.indexName().get())) + .toList(); + + assertThat(empDateItems).hasSize(2); + + List columnNames = empDateItems.stream().map(item -> item.column().map(c -> c.name()).orElse("")) + .map(String::toUpperCase).toList(); + assertThat(columnNames).containsExactly("EMP_ID", "ORDER_DATE"); + } + + @Test + void getAllViewDefinitions_returnsTwo() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, DATABASE); + assertThat(views).hasSize(2); + } + + @Test + void getAllViewDefinitions_bodyIsPresent() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, DATABASE); + + for (ViewDefinition view : views) { + assertThat(view.viewBody()).isPresent(); + assertThat(view.viewBody().get()).isNotBlank(); + } + } + + @Test + void getAllViewDefinitions_bodyContainsSelect() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, DATABASE); + + for (ViewDefinition view : views) { + assertThat(view.viewBody().get()).containsIgnoringCase("SELECT"); + } + } + + @Test + void getAllViewDefinitions_viewType() throws SQLException { + List views = provider.getAllViewDefinitions(connection, null, DATABASE); + + List viewNames = views.stream().map(v -> v.view().name().toUpperCase()).toList(); + assertThat(viewNames).containsExactlyInAnyOrder("V_EMP_DEPT", "V_ORDER_SUMMARY"); + + for (ViewDefinition view : views) { + assertThat(view.view().type()).isEqualTo("VIEW"); + } + } + + @Test + void getAllProcedures_returnsInsertDept() throws SQLException { + List procedures = provider.getAllProcedures(connection, null, DATABASE); + assertThat(procedures).anyMatch(p -> "INSERT_DEPT".equalsIgnoreCase(p.reference().name())); + } + + @Test + void getAllProcedures_bodyIsPresent() throws SQLException { + List procedures = provider.getAllProcedures(connection, null, DATABASE); + Procedure insertDept = procedures.stream().filter(p -> "INSERT_DEPT".equalsIgnoreCase(p.reference().name())) + .findFirst().orElseThrow(); + assertThat(insertDept.body()).isPresent(); + assertThat(insertDept.body().get()).containsIgnoringCase("DEPARTMENTS"); + } + + @Test + void getAllProcedures_fullDefinitionIsPresent() throws SQLException { + List procedures = provider.getAllProcedures(connection, null, DATABASE); + Procedure insertDept = procedures.stream().filter(p -> "INSERT_DEPT".equalsIgnoreCase(p.reference().name())) + .findFirst().orElseThrow(); + assertThat(insertDept.fullDefinition()).isPresent(); + assertThat(insertDept.fullDefinition().get()).containsIgnoringCase("CREATE"); + } + + @Test + void getAllFunctions_containsDefinedFunctions() throws SQLException { + List functions = provider.getAllFunctions(connection, null, DATABASE); + List names = functions.stream().map(f -> f.reference().name().toUpperCase()).toList(); + assertThat(names).contains("TO_UPPER", "ADD_NUMBERS"); + } + + @Test + void getAllFunctions_addNumbersHasParameters() throws SQLException { + List functions = provider.getAllFunctions(connection, null, DATABASE); + Function addNumbers = functions.stream().filter(f -> "ADD_NUMBERS".equalsIgnoreCase(f.reference().name())) + .findFirst().orElseThrow(); + assertThat(addNumbers.columns()).hasSizeGreaterThanOrEqualTo(2); + } + + @Test + void getAllFunctions_bodyIsPresent() throws SQLException { + List functions = provider.getAllFunctions(connection, null, DATABASE); + Function addNumbers = functions.stream().filter(f -> "ADD_NUMBERS".equalsIgnoreCase(f.reference().name())) + .findFirst().orElseThrow(); + assertThat(addNumbers.body()).isPresent(); + assertThat(addNumbers.body().get()).containsIgnoringCase("RETURN"); + } + + @Test + void getAllUserDefinedTypes_returnsEmpty() throws SQLException { + List udts = provider.getAllUserDefinedTypes(connection, null, DATABASE); + assertThat(udts).isEmpty(); + } + + @Test + void getAllPartitions_returnsFourPartitions() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, DATABASE); + assertThat(partitions).hasSize(4); + assertThat(partitions).allMatch(p -> "SALES_BY_YEAR".equalsIgnoreCase(p.table().name())); + } + + @Test + void getAllPartitions_methodIsRange() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, DATABASE); + assertThat(partitions).allMatch(p -> p.method() == PartitionMethod.RANGE); + } + + @Test + void getAllPartitions_orderingByOrdinalPosition() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, DATABASE); + assertThat(partitions).extracting(Partition::name).containsExactly("p2022", "p2023", "p2024", "pmax"); + } + + @Test + void getAllPartitions_descriptionsCarryBoundaries() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, DATABASE); + // MariaDB reports boundaries as "2023", "2024", "2025", "MAXVALUE" respectively + assertThat(findPartition(partitions, "p2022").description()).contains("2023"); + assertThat(findPartition(partitions, "pmax").description()).contains("MAXVALUE"); + } + + @Test + void getAllPartitions_expressionIsPartitionKey() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, DATABASE); + // every partition reports the same partitioning expression + assertThat(partitions).allMatch(p -> p.expression().isPresent() + && p.expression().get().toUpperCase(java.util.Locale.ROOT).contains("YEAR")); + } + + @Test + void getPartitions_perTableMatchesAll() throws SQLException { + List all = provider.getAllPartitions(connection, null, DATABASE); + List sales = provider.getPartitions(connection, null, DATABASE, "SALES_BY_YEAR"); + assertThat(sales).hasSameSizeAs(all); + } + + @Test + void getPartitions_returnsEmptyForUnpartitionedTable() throws SQLException { + List employees = provider.getPartitions(connection, null, DATABASE, "EMPLOYEES"); + assertThat(employees).isEmpty(); + } + + private static Partition findPartition(List partitions, String name) { + return partitions.stream().filter(p -> name.equalsIgnoreCase(p.name())).findFirst() + .orElseThrow(() -> new AssertionError("Partition not found: " + name)); + } + + private static Trigger findTrigger(List triggers, String name) { + return triggers.stream().filter(t -> name.equalsIgnoreCase(t.name())).findFirst() + .orElseThrow(() -> new AssertionError("Trigger not found: " + name)); + } + + private static Sequence findSequence(List sequences, String name) { + return sequences.stream().filter(s -> name.equalsIgnoreCase(s.name())).findFirst() + .orElseThrow(() -> new AssertionError("Sequence not found: " + name)); + } + + private static IndexInfoItem findIndexItem(List indexInfos, String indexName) { + return indexInfos.stream().flatMap(ii -> ii.indexInfoItems().stream()) + .filter(item -> item.indexName().isPresent() && indexName.equalsIgnoreCase(item.indexName().get())) + .findFirst().orElseThrow(() -> new AssertionError("Index not found: " + indexName)); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlPartitionsTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlPartitionsTest.java new file mode 100644 index 0000000..025e6df --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/MsSqlPartitionsTest.java @@ -0,0 +1,101 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +*/ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.MSSQLServerContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +@TestInstance(Lifecycle.PER_CLASS) +class MsSqlPartitionsTest { + + @Container + @SuppressWarnings("resource") + static final MSSQLServerContainer CONTAINER = new MSSQLServerContainer<>( + "mcr.microsoft.com/mssql/server:2022-latest").acceptLicense(); + + private static final String SCHEMA = "dbo"; + + private static Connection connection; + private static MicrosoftSqlServerMetadataProvider provider; + + @BeforeAll + void setUp() throws Exception { + Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); + connection = java.sql.DriverManager.getConnection(CONTAINER.getJdbcUrl(), CONTAINER.getUsername(), + CONTAINER.getPassword()); + try (Statement stmt = connection.createStatement()) { + // Partition function over INT (year) + stmt.execute("CREATE PARTITION FUNCTION pf_sales_year (INT) AS RANGE RIGHT FOR VALUES (2023, 2024, 2025)"); + stmt.execute("CREATE PARTITION SCHEME ps_sales_year AS PARTITION pf_sales_year ALL TO ([PRIMARY])"); + stmt.execute(""" + CREATE TABLE sales_by_year ( + sale_id INT NOT NULL, + sale_year INT NOT NULL, + amount DECIMAL(10,2) NOT NULL, + CONSTRAINT pk_sales_by_year PRIMARY KEY CLUSTERED (sale_year, sale_id) + ) ON ps_sales_year(sale_year) + """); + } + provider = new MicrosoftSqlServerMetadataProvider(); + } + + @AfterAll + void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + } + + @Test + void getAllPartitions_returnsFourPartitions() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + // RANGE RIGHT FOR VALUES (2023, 2024, 2025) → 4 partitions + assertThat(partitions).hasSize(4); + assertThat(partitions).allMatch(p -> p.method() == PartitionMethod.RANGE); + } + + @Test + void getAllPartitions_partitionColumnIsSaleYear() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + assertThat(partitions) + .allMatch(p -> p.expression().isPresent() && "sale_year".equalsIgnoreCase(p.expression().get())); + } + + @Test + void getAllPartitions_ordinalPositionsAreSequential() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + assertThat(partitions).extracting(p -> p.ordinalPosition().orElseThrow()).containsExactly(1, 2, 3, 4); + } + + @Test + void getPartitions_perTableMatchesAll() throws SQLException { + List all = provider.getAllPartitions(connection, null, SCHEMA); + List sales = provider.getPartitions(connection, null, SCHEMA, "sales_by_year"); + assertThat(sales).hasSameSizeAs(all); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/PgPartitionsTest.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/PgPartitionsTest.java new file mode 100644 index 0000000..fa0854e --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/PgPartitionsTest.java @@ -0,0 +1,135 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +*/ +package org.eclipse.daanse.sql.jdbc.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.time.Duration; +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +@EnabledIfSystemProperty(named = "integration.docker", matches = "true") +class PgPartitionsTest { + + private static final String DATABASE = "test"; + private static final String USER = "postgres"; + private static final String PASSWORD = "secret"; + private static final String SCHEMA = "public"; + + @SuppressWarnings("resource") + private static final GenericContainer POSTGRES = new GenericContainer<>("postgres:15") + .withEnv("POSTGRES_PASSWORD", PASSWORD).withEnv("POSTGRES_DB", DATABASE).withExposedPorts(5432) + .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*\\n", 2) + .withStartupTimeout(Duration.ofMinutes(2))); + + private static Connection connection; + private static PostgreSqlMetadataProvider provider; + + @BeforeAll + static void setUp() throws Exception { + POSTGRES.start(); + String jdbcUrl = "jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getMappedPort(5432) + "/" + + DATABASE; + Class.forName("org.postgresql.Driver"); + connection = DriverManager.getConnection(jdbcUrl, USER, PASSWORD); + try (Statement stmt = connection.createStatement()) { + stmt.execute(""" + CREATE TABLE sales_by_year ( + sale_id BIGINT NOT NULL, + sale_date DATE NOT NULL, + amount NUMERIC(10,2) NOT NULL + ) PARTITION BY RANGE (sale_date) + """); + stmt.execute("CREATE TABLE sales_2022 PARTITION OF sales_by_year " + + "FOR VALUES FROM ('2022-01-01') TO ('2023-01-01')"); + stmt.execute("CREATE TABLE sales_2023 PARTITION OF sales_by_year " + + "FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')"); + stmt.execute("CREATE TABLE sales_default PARTITION OF sales_by_year DEFAULT"); + + stmt.execute(""" + CREATE TABLE customers_by_region ( + cust_id BIGINT NOT NULL, + region TEXT NOT NULL + ) PARTITION BY LIST (region) + """); + stmt.execute("CREATE TABLE customers_eu PARTITION OF customers_by_region FOR VALUES IN ('EU')"); + stmt.execute("CREATE TABLE customers_us PARTITION OF customers_by_region FOR VALUES IN ('US')"); + } + provider = new PostgreSqlMetadataProvider(); + } + + @AfterAll + static void tearDown() throws Exception { + if (connection != null && !connection.isClosed()) { + connection.close(); + } + POSTGRES.stop(); + } + + @Test + void getAllPartitions_findsBothPartitionedTables() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + // 3 sales partitions + 2 customers partitions + assertThat(partitions).hasSize(5); + } + + @Test + void getAllPartitions_rangeAndListMethodsBothReported() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + assertThat(partitions).filteredOn(p -> "sales_by_year".equals(p.table().name())) + .allMatch(p -> p.method() == PartitionMethod.RANGE); + assertThat(partitions).filteredOn(p -> "customers_by_region".equals(p.table().name())) + .allMatch(p -> p.method() == PartitionMethod.LIST); + } + + @Test + void getAllPartitions_boundsCarryFromTo() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + Partition sales2022 = partitions.stream().filter(p -> "sales_2022".equals(p.name())).findFirst().orElseThrow(); + assertThat(sales2022.description()).isPresent(); + assertThat(sales2022.description().get()).contains("2022-01-01").contains("2023-01-01"); + } + + @Test + void getAllPartitions_listPartitionDescription() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + Partition eu = partitions.stream().filter(p -> "customers_eu".equals(p.name())).findFirst().orElseThrow(); + assertThat(eu.description()).isPresent(); + assertThat(eu.description().get()).contains("EU"); + } + + @Test + void getAllPartitions_expressionExtractsPartitionKey() throws SQLException { + List partitions = provider.getAllPartitions(connection, null, SCHEMA); + Partition any = partitions.stream().filter(p -> "sales_by_year".equals(p.table().name())).findFirst() + .orElseThrow(); + assertThat(any.expression()).contains("sale_date"); + } + + @Test + void getPartitions_perTableFilters() throws SQLException { + List sales = provider.getPartitions(connection, null, SCHEMA, "sales_by_year"); + assertThat(sales).hasSize(3); + List cust = provider.getPartitions(connection, null, SCHEMA, "customers_by_region"); + assertThat(cust).hasSize(2); + } +} diff --git a/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/TestAuditTrigger.java b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/TestAuditTrigger.java new file mode 100644 index 0000000..3b2c8fd --- /dev/null +++ b/jdbc/metadata/src/test/java/org/eclipse/daanse/sql/jdbc/metadata/TestAuditTrigger.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.jdbc.metadata; + +import java.sql.Connection; +import java.sql.SQLException; + +import org.h2.api.Trigger; + +public class TestAuditTrigger implements Trigger { + + @Override + public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) + throws SQLException { + // no-op + } + + @Override + public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException { + // no-op + } + + @Override + public void close() throws SQLException { + // no-op + } + + @Override + public void remove() throws SQLException { + // no-op + } +} diff --git a/jdbc/pom.xml b/jdbc/pom.xml new file mode 100644 index 0000000..0b633fe --- /dev/null +++ b/jdbc/pom.xml @@ -0,0 +1,68 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.pom.parent + 0.0.7 + + + org.eclipse.daanse.sql.jdbc + ${revision} + pom + Eclipse Daanse SQL JDBC + + + 0.0.1-SNAPSHOT + 2.0.9 + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + compile + + + + + + + ossrh + Sonatype Nexus Snapshots + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + JDBC binding for the Daanse SQL stack. Reads live database + metadata through java.sql.DatabaseMetaData into the org.eclipse.daanse.sql.model + vocabulary, provides per-vendor metadata providers, record implementations of the + schema types, and a CSV import utility. + + api + record + metadata + impl + importer + + diff --git a/jdbc/record/pom.xml b/jdbc/record/pom.xml new file mode 100644 index 0000000..3a42901 --- /dev/null +++ b/jdbc/record/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc + ${revision} + + org.eclipse.daanse.sql.jdbc.record + Eclipse Daanse SQL JDBC Record + Record-based implementation of JDBC database operations for + Daanse. Provides immutable Java record classes for representing database + entities and query results with efficient memory usage and type safety. + + + + org.eclipse.daanse + org.eclipse.daanse.sql.model + ${project.version} + + + org.eclipse.daanse + org.eclipse.daanse.sql.jdbc.api + ${project.version} + + + diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/DatabaseInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/DatabaseInfoRecord.java new file mode 100644 index 0000000..4df7d0d --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/DatabaseInfoRecord.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.meta; + +import org.eclipse.daanse.sql.jdbc.api.meta.DatabaseInfo; + +public record DatabaseInfoRecord(// + String databaseProductName, // + String databaseProductVersion, // + int databaseMajorVersion, // + int databaseMinorVersion // +) implements DatabaseInfo { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/IdentifierInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/IdentifierInfoRecord.java new file mode 100644 index 0000000..dc1dd6e --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/IdentifierInfoRecord.java @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.meta; + +import java.util.List; +import java.util.Set; + +import org.eclipse.daanse.sql.jdbc.api.meta.IdentifierInfo; + +public record IdentifierInfoRecord(String quoteString, int maxColumnNameLength, boolean readOnly, Set> supportedResultSetStyles) implements IdentifierInfo { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/MetaInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/MetaInfoRecord.java new file mode 100644 index 0000000..1fb7caa --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/MetaInfoRecord.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.meta; + +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.meta.DatabaseInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IdentifierInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.MetaInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; + +public record MetaInfoRecord(DatabaseInfo databaseInfo,StructureInfo structureInfo , IdentifierInfo identifierInfo, List typeInfos, List indexInfos) + implements MetaInfo { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java new file mode 100644 index 0000000..fae111c --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/StructureInfoRecord.java @@ -0,0 +1,53 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.meta; + +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.meta.StructureInfo; +import org.eclipse.daanse.sql.model.schema.CatalogReference; +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; + +public record StructureInfoRecord( + List catalogs, + List schemas, + List tables, + List columns, + List importedKeys, + List primaryKeys, + List triggers, + List sequences, + List checkConstraints, + List uniqueConstraints, + List userDefinedTypes, + List viewDefinitions, + List procedures, + List functions, + List materializedViews, + List partitions) implements StructureInfo { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/TypeInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/TypeInfoRecord.java new file mode 100644 index 0000000..c295e40 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/TypeInfoRecord.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.meta; + +import java.sql.JDBCType; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.meta.TypeInfo; + +public record TypeInfoRecord(String typeName, JDBCType dataType, int precision, Optional literalPrefix, + Optional literalSuffix, Optional createParams, Nullable nullable, boolean caseSensitive, + Searchable searchable, boolean unsignedAttribute, boolean fixedPrecScale, boolean autoIncrement, + Optional localTypeName, short minimumScale, short maximumScale, int numPrecRadix) implements TypeInfo { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/package-info.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/package-info.java new file mode 100644 index 0000000..d7a7a92 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/meta/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.record.meta; diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/BestRowIdentifierRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/BestRowIdentifierRecord.java new file mode 100644 index 0000000..0d188f0 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/BestRowIdentifierRecord.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.jdbc.api.schema.BestRowIdentifier; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +public record BestRowIdentifierRecord( + ColumnReference column, + Scope scope, + JDBCType dataType, + String typeName, + OptionalInt columnSize, + OptionalInt decimalDigits, + PseudoColumnKind pseudoColumn) implements BestRowIdentifier { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/CheckConstraintRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/CheckConstraintRecord.java new file mode 100644 index 0000000..d344232 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/CheckConstraintRecord.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import org.eclipse.daanse.sql.jdbc.api.schema.CheckConstraint; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record CheckConstraintRecord( + String name, + TableReference table, + String checkClause) implements CheckConstraint { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnDefinitionRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnDefinitionRecord.java new file mode 100644 index 0000000..96f1c3e --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnDefinitionRecord.java @@ -0,0 +1,22 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import org.eclipse.daanse.sql.model.schema.ColumnDefinition; +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +public record ColumnDefinitionRecord(ColumnReference column, ColumnMetaData columnMetaData) implements ColumnDefinition { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnMetaDataRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnMetaDataRecord.java new file mode 100644 index 0000000..6887453 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnMetaDataRecord.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnMetaData; + +public record ColumnMetaDataRecord( + JDBCType dataType, + String typeName, + OptionalInt columnSize, + OptionalInt decimalDigits, + OptionalInt numPrecRadix, + Nullability nullability, + OptionalInt charOctetLength, + Optional remarks, + Optional columnDefault, + AutoIncrement autoIncrement, + GeneratedColumn generatedColumn) implements ColumnMetaData { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnPrivilegeRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnPrivilegeRecord.java new file mode 100644 index 0000000..bf97060 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ColumnPrivilegeRecord.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.ColumnPrivilege; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +public record ColumnPrivilegeRecord( + ColumnReference column, + Optional grantor, + String grantee, + String privilege, + Optional isGrantable) implements ColumnPrivilege { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/DropImportedKeyRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/DropImportedKeyRecord.java new file mode 100644 index 0000000..b7690c6 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/DropImportedKeyRecord.java @@ -0,0 +1,21 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import org.eclipse.daanse.sql.jdbc.api.schema.DropImportedKey; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record DropImportedKeyRecord(TableReference table, String name) implements DropImportedKey { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionColumnRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionColumnRecord.java new file mode 100644 index 0000000..5ac90d0 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionColumnRecord.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; + +public record FunctionColumnRecord( + String name, + ColumnType columnType, + JDBCType dataType, + String typeName, + OptionalInt precision, + OptionalInt scale, + OptionalInt radix, + Nullability nullable, + Optional remarks, + OptionalInt charOctetLength, + int ordinalPosition) implements FunctionColumn { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionRecord.java new file mode 100644 index 0000000..b3ca418 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/FunctionRecord.java @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Function; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.FunctionReference; + +public record FunctionRecord( + FunctionReference reference, + FunctionType functionType, + Optional remarks, + List columns, + Optional body, + Optional fullDefinition, + Optional lastModified) implements Function { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ImportedKeyRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ImportedKeyRecord.java new file mode 100644 index 0000000..aeea869 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ImportedKeyRecord.java @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ImportedKey; + +public record ImportedKeyRecord( + ColumnReference primaryKeyColumn, + ColumnReference foreignKeyColumn, + String name, + int keySequence, + ReferentialAction updateRule, + ReferentialAction deleteRule, + Optional primaryKeyName, + Deferrability deferrability) implements ImportedKey { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoItemRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoItemRecord.java new file mode 100644 index 0000000..4b987db --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoItemRecord.java @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.model.schema.ColumnReference; + +public record IndexInfoItemRecord( + Optional indexName, + IndexType type, + Optional column, + int ordinalPosition, + Optional ascending, + long cardinality, + long pages, + Optional filterCondition, + boolean unique) implements IndexInfoItem { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoRecord.java new file mode 100644 index 0000000..c987819 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/IndexInfoRecord.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.List; + +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfo; +import org.eclipse.daanse.sql.jdbc.api.meta.IndexInfoItem; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record IndexInfoRecord(TableReference tableReference, List indexInfoItems) implements IndexInfo { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/MaterializedViewRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/MaterializedViewRecord.java new file mode 100644 index 0000000..0fab4a2 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/MaterializedViewRecord.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.time.Instant; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.MaterializedView; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record MaterializedViewRecord( + TableReference view, + Optional viewBody, + Optional fullDefinition, + Optional refreshMode, + Optional lastRefresh) implements MaterializedView { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PartitionRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PartitionRecord.java new file mode 100644 index 0000000..a9235e2 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PartitionRecord.java @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2026 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Partition; +import org.eclipse.daanse.sql.jdbc.api.schema.PartitionMethod; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record PartitionRecord( + String name, + TableReference table, + Optional ordinalPosition, + PartitionMethod method, + Optional expression, + Optional description, + Optional rowCount, + Optional parentPartitionName, + Optional subPartitionMethod, + Optional subPartitionExpression) implements Partition { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PrimaryKeyRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PrimaryKeyRecord.java new file mode 100644 index 0000000..e18b75d --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PrimaryKeyRecord.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.PrimaryKey; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record PrimaryKeyRecord( + TableReference table, + List columns, + Optional constraintName) implements PrimaryKey { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureColumnRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureColumnRecord.java new file mode 100644 index 0000000..c8118e8 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureColumnRecord.java @@ -0,0 +1,35 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; + +public record ProcedureColumnRecord( + String name, + ColumnType columnType, + JDBCType dataType, + String typeName, + OptionalInt precision, + OptionalInt scale, + OptionalInt radix, + Nullability nullable, + Optional remarks, + Optional columnDefault, + int ordinalPosition) implements ProcedureColumn { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureRecord.java new file mode 100644 index 0000000..57d4e9d --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ProcedureRecord.java @@ -0,0 +1,32 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Procedure; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureColumn; +import org.eclipse.daanse.sql.jdbc.api.schema.ProcedureReference; + +public record ProcedureRecord( + ProcedureReference reference, + ProcedureType procedureType, + Optional remarks, + List columns, + Optional body, + Optional fullDefinition, + Optional lastModified) implements Procedure { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PseudoColumnRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PseudoColumnRecord.java new file mode 100644 index 0000000..e1dae23 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/PseudoColumnRecord.java @@ -0,0 +1,33 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.PseudoColumn; + +public record PseudoColumnRecord( + ColumnReference column, + JDBCType dataType, + OptionalInt columnSize, + OptionalInt decimalDigits, + OptionalInt numPrecRadix, + Optional remarks, + OptionalInt charOctetLength, + Optional isNullable, + String columnUsage) implements PseudoColumn { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SequenceRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SequenceRecord.java new file mode 100644 index 0000000..f5fad59 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SequenceRecord.java @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.Sequence; +import org.eclipse.daanse.sql.jdbc.api.schema.SequenceReference; + +public record SequenceRecord( + SequenceReference reference, + long startValue, + long incrementBy, + Optional minValue, + Optional maxValue, + boolean cycle, + Optional cacheSize, + Optional dataType) implements Sequence { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTableRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTableRecord.java new file mode 100644 index 0000000..ab8699d --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTableRecord.java @@ -0,0 +1,22 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import org.eclipse.daanse.sql.jdbc.api.schema.SuperTable; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record SuperTableRecord( + TableReference table, + String superTableName) implements SuperTable { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTypeRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTypeRecord.java new file mode 100644 index 0000000..50dccc9 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/SuperTypeRecord.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.jdbc.api.schema.SuperType; + +public record SuperTypeRecord( + String typeName, + Optional typeSchema, + String superTypeName, + Optional superTypeSchema) implements SuperType { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableDefinitionRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableDefinitionRecord.java new file mode 100644 index 0000000..d0de736 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableDefinitionRecord.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +package org.eclipse.daanse.sql.jdbc.record.schema; + +import org.eclipse.daanse.sql.jdbc.api.schema.TableDefinition; +import org.eclipse.daanse.sql.jdbc.api.schema.TableMetaData; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record TableDefinitionRecord(TableReference table, TableMetaData tableMetaData) implements TableDefinition { + + public TableDefinitionRecord(TableReference table) { + this(table, new TableMetaDataRecord()); + } + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableMetaDataRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableMetaDataRecord.java new file mode 100644 index 0000000..9aa1134 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TableMetaDataRecord.java @@ -0,0 +1,30 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.TableMetaData; + +public record TableMetaDataRecord(Optional remarks, Optional typeCatalog, Optional typeSchema, + Optional typeName, Optional selfReferencingColumnName, Optional refGeneration) + implements TableMetaData { + + public TableMetaDataRecord() { + this(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), + Optional.empty()); + } + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TablePrivilegeRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TablePrivilegeRecord.java new file mode 100644 index 0000000..b95f070 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TablePrivilegeRecord.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.TablePrivilege; +import org.eclipse.daanse.sql.model.schema.TableReference; + +public record TablePrivilegeRecord( + TableReference table, + Optional grantor, + String grantee, + String privilege, + Optional isGrantable) implements TablePrivilege { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java new file mode 100644 index 0000000..58d4fcb --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/TriggerRecord.java @@ -0,0 +1,31 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.Trigger; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerEvent; +import org.eclipse.daanse.sql.model.schema.Trigger.TriggerTiming; +import org.eclipse.daanse.sql.model.schema.TriggerReference; + +public record TriggerRecord( + TriggerReference reference, + TriggerTiming timing, + TriggerEvent event, + Optional body, + Optional fullDefinition, + Optional orientation) implements Trigger { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UniqueConstraintRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UniqueConstraintRecord.java new file mode 100644 index 0000000..eb143eb --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UniqueConstraintRecord.java @@ -0,0 +1,27 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.List; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.api.schema.UniqueConstraint; + +public record UniqueConstraintRecord( + String name, + TableReference table, + List columns) implements UniqueConstraint { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UserDefinedTypeRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UserDefinedTypeRecord.java new file mode 100644 index 0000000..bd207fb --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/UserDefinedTypeRecord.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.Optional; + +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedType; +import org.eclipse.daanse.sql.jdbc.api.schema.UserDefinedTypeReference; + +public record UserDefinedTypeRecord( + UserDefinedTypeReference reference, + String className, + JDBCType baseType, + Optional remarks) implements UserDefinedType { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/VersionColumnRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/VersionColumnRecord.java new file mode 100644 index 0000000..cee2d3c --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/VersionColumnRecord.java @@ -0,0 +1,29 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.sql.JDBCType; +import java.util.OptionalInt; + +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.jdbc.api.schema.VersionColumn; + +public record VersionColumnRecord( + ColumnReference column, + JDBCType dataType, + String typeName, + OptionalInt columnSize, + OptionalInt decimalDigits, + PseudoColumnKind pseudoColumn) implements VersionColumn { +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ViewDefinitionRecord.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ViewDefinitionRecord.java new file mode 100644 index 0000000..6a8a869 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/ViewDefinitionRecord.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.jdbc.record.schema; + +import java.util.Optional; + +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.jdbc.api.schema.ViewDefinition; + +public record ViewDefinitionRecord( + TableReference view, + Optional viewBody, + Optional fullDefinition) implements ViewDefinition { + +} diff --git a/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/package-info.java b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/package-info.java new file mode 100644 index 0000000..fdb5c34 --- /dev/null +++ b/jdbc/record/src/main/java/org/eclipse/daanse/sql/jdbc/record/schema/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") + +package org.eclipse.daanse.sql.jdbc.record.schema; diff --git a/model/pom.xml b/model/pom.xml new file mode 100644 index 0000000..f046ade --- /dev/null +++ b/model/pom.xml @@ -0,0 +1,50 @@ + + + + 4.0.0 + + org.eclipse.daanse + org.eclipse.daanse.pom.parent + 0.0.7 + + + org.eclipse.daanse.sql.model + ${revision} + Eclipse Daanse SQL Model + The neutral, JDK-only SQL vocabulary shared by the statement model, the + dialects and the JDBC introspection layer: the SQL type system (Datatype, + BestFitColumnType), ordering/aggregation types (OrderedColumn, SortDirection, + NullsOrder, BitOperation) and the naming references plus column/constraint/trigger + descriptors (TableReference, ColumnDefinition, PrimaryKey, Trigger, ...). + No dependencies; the future base module of the org.eclipse.daanse.sql repository. + + + 0.0.1-SNAPSHOT + + + + + ossrh + Sonatype Nexus Snapshots + https://central.sonatype.com/repository/maven-snapshots + + false + + + true + + + + diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/CatalogReference.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/CatalogReference.java new file mode 100644 index 0000000..dfd659e --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/CatalogReference.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +public record CatalogReference(String name) implements Named { +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnDefinition.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnDefinition.java new file mode 100644 index 0000000..475b634 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnDefinition.java @@ -0,0 +1,25 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +public interface ColumnDefinition { + + ColumnReference column(); + + ColumnMetaData columnMetaData(); + + default ColumnReference reference() { + return column(); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnMetaData.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnMetaData.java new file mode 100644 index 0000000..5ba5878 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnMetaData.java @@ -0,0 +1,113 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.Stream; + +public interface ColumnMetaData { + + JDBCType dataType(); + + String typeName(); + + OptionalInt columnSize(); + + OptionalInt decimalDigits(); + + OptionalInt numPrecRadix(); + + Nullability nullability(); + + OptionalInt charOctetLength(); + + Optional remarks(); + + Optional columnDefault(); + + AutoIncrement autoIncrement(); + + GeneratedColumn generatedColumn(); + + enum Nullability { + NO_NULLS(DatabaseMetaData.columnNoNulls), NULLABLE(DatabaseMetaData.columnNullable), + UNKNOWN(DatabaseMetaData.columnNullableUnknown); + + private final int value; + + Nullability(int value) { + this.value = value; + } + + /** + * Raw JDBC integer constant for this enum value (per + * {@link java.sql.DatabaseMetaData}). + */ + public int getValue() { + return value; + } + + /** Look up the enum constant matching the JDBC int code. Throws if no match. */ + public static Nullability of(int value) { + return Stream.of(Nullability.values()).filter(n -> n.value == value).findFirst().orElse(UNKNOWN); + } + + /** + * Look up the enum constant matching the JDBC string code. Throws if no match. + */ + public static Nullability ofString(String value) { + if ("YES".equalsIgnoreCase(value)) { + return NULLABLE; + } else if ("NO".equalsIgnoreCase(value)) { + return NO_NULLS; + } + return UNKNOWN; + } + } + + enum AutoIncrement { + YES, NO, UNKNOWN; + + /** + * Look up the enum constant matching the JDBC string code. Throws if no match. + */ + public static AutoIncrement ofString(String value) { + if ("YES".equalsIgnoreCase(value)) { + return YES; + } else if ("NO".equalsIgnoreCase(value)) { + return NO; + } + return UNKNOWN; + } + } + + enum GeneratedColumn { + YES, NO, UNKNOWN; + + /** + * Look up the enum constant matching the JDBC string code. Throws if no match. + */ + public static GeneratedColumn ofString(String value) { + if ("YES".equalsIgnoreCase(value)) { + return YES; + } else if ("NO".equalsIgnoreCase(value)) { + return NO; + } + return UNKNOWN; + } + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnReference.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnReference.java new file mode 100644 index 0000000..efe3ad2 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/ColumnReference.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.util.Optional; + +public record ColumnReference(Optional table, String name) implements Named { + + /** Convenience: an unqualified column (no parent table). */ + public ColumnReference(String name) { + this(Optional.empty(), name); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/Named.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Named.java new file mode 100644 index 0000000..6644d54 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Named.java @@ -0,0 +1,19 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +public interface Named { + + String name(); +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/PrimaryKey.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/PrimaryKey.java new file mode 100644 index 0000000..4b8e6c0 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/PrimaryKey.java @@ -0,0 +1,28 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.util.List; +import java.util.Optional; + +public interface PrimaryKey { + + TableReference table(); + + /** @return ordered list of column references */ + List columns(); + + /** @return the constraint name, or empty if unnamed */ + Optional constraintName(); +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/SchemaReference.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/SchemaReference.java new file mode 100644 index 0000000..afa1d13 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/SchemaReference.java @@ -0,0 +1,24 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.util.Optional; + +public record SchemaReference(Optional catalog, String name) implements Named { + + /** Convenience: a schema with no catalog. */ + public SchemaReference(String name) { + this(Optional.empty(), name); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/TableReference.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/TableReference.java new file mode 100644 index 0000000..09f7480 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/TableReference.java @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.util.Optional; + +public record TableReference(Optional schema, String name, String type) implements Named { + + public static final String TYPE_TABLE = "TABLE"; + public static final String TYPE_VIEW = "VIEW"; + public static final String TYPE_SYSTEM_TABLE = "SYSTEM TABLE"; + public static final String TYPE_GLOBAL_TEMPORARY = "GLOBAL TEMPORARY"; + public static final String TYPE_LOCAL_TEMPORARY = "LOCAL TEMPORARY"; + public static final String TYPE_ALIAS = "ALIAS"; + public static final String TYPE_SYNONYM = "SYNONYM"; + + /** Convenience: an unqualified table (no schema), {@link #TYPE_TABLE}. */ + public TableReference(String name) { + this(Optional.empty(), name, TYPE_TABLE); + } + + /** Convenience: an unqualified table (no schema) of the given JDBC type. */ + public TableReference(String name, String type) { + this(Optional.empty(), name, type); + } + + /** Convenience: a schema-qualified {@link #TYPE_TABLE}. */ + public TableReference(Optional schema, String name) { + this(schema, name, TYPE_TABLE); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java new file mode 100644 index 0000000..a8b11cc --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/Trigger.java @@ -0,0 +1,80 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +import java.util.Optional; + +public interface Trigger extends Named { + + TriggerReference reference(); + + @Override + default String name() { + return reference().name(); + } + + /** Convenience: the table this trigger fires on. */ + default TableReference table() { + return reference().table(); + } + + TriggerTiming timing(); + + TriggerEvent event(); + + /** @return the trigger body, or empty if not available */ + Optional body(); + + /** @return the full CREATE TRIGGER definition, or empty if not available */ + Optional fullDefinition(); + + /** @return "ROW" or "STATEMENT", or empty if not applicable */ + Optional orientation(); + + /** When the trigger body fires relative to the triggering DML. */ + enum TriggerTiming { + /** Fires before the DML applies to the table. */ + BEFORE, + /** Fires after the DML applies to the table. */ + AFTER, + /** Fires in place of the DML; commonly used for views. */ + INSTEAD_OF + } + + /** Which DML statement kind activates the trigger. */ + enum TriggerEvent { + /** Fires on {@code INSERT}. */ + INSERT, + /** Fires on {@code UPDATE}. */ + UPDATE, + /** Fires on {@code DELETE}. */ + DELETE + } + + /** Whether the trigger fires once per statement or once per affected row. */ + enum TriggerScope { + /** Fires exactly once for the whole statement. */ + STATEMENT, + /** Fires once for every row the statement touches. */ + ROW; + + /** + * @return SQL keyword form ({@code "FOR EACH ROW"} or + * {@code "FOR EACH STATEMENT"}) + */ + public String forEachClause() { + return this == ROW ? "FOR EACH ROW" : "FOR EACH STATEMENT"; + } + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/TriggerReference.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/TriggerReference.java new file mode 100644 index 0000000..3a0eff0 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/TriggerReference.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2024 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ +package org.eclipse.daanse.sql.model.schema; + +public record TriggerReference(TableReference table, String name) implements Named { +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/schema/package-info.java b/model/src/main/java/org/eclipse/daanse/sql/model/schema/package-info.java new file mode 100644 index 0000000..701e8ab --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/schema/package-info.java @@ -0,0 +1,12 @@ +/* + * 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 + */ +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.model.schema; diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/sql/BitOperation.java b/model/src/main/java/org/eclipse/daanse/sql/model/sql/BitOperation.java new file mode 100644 index 0000000..8f41268 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/sql/BitOperation.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.model.sql; + +/** + * Bitwise operation kind that the dialect's {@code FunctionGenerator} can emit. + */ +public enum BitOperation { + + /** Bitwise AND. */ + AND, + + /** Bitwise OR. */ + OR, + + /** Bitwise XOR (exclusive OR). */ + XOR, + + /** Bitwise NAND (negated AND). */ + NAND, + + /** Bitwise NOR (negated OR). */ + NOR, + + /** Bitwise NXOR (negated XOR; equivalence). */ + NXOR +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/sql/NullsOrder.java b/model/src/main/java/org/eclipse/daanse/sql/model/sql/NullsOrder.java new file mode 100644 index 0000000..162b4fa --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/sql/NullsOrder.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.model.sql; + +/** Where {@code NULL} values appear within an {@code ORDER BY} result. */ +public enum NullsOrder { + + /** {@code NULL} values appear at the beginning of the result. */ + FIRST, + + /** {@code NULL} values appear at the end of the result. */ + LAST +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/sql/OrderedColumn.java b/model/src/main/java/org/eclipse/daanse/sql/model/sql/OrderedColumn.java new file mode 100644 index 0000000..d14e7eb --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/sql/OrderedColumn.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.model.sql; + +import java.util.Optional; + + +/** + * @param tableName the name of the table (may be null for unqualified + * columns) + * @param sortDirection empty = use database default, otherwise ASC or DESC + * @param nullsOrder empty = use database default, otherwise FIRST or LAST + */ +public record OrderedColumn(String columnName, String tableName, Optional sortDirection, + Optional nullsOrder) { + + /** + * @param tableName the name of the table (may be null for unqualified columns) + */ + public OrderedColumn(String columnName, String tableName, SortDirection sortDirection) { + this(columnName, tableName, Optional.of(sortDirection), Optional.empty()); + } + + /** + * @param tableName the name of the table (may be null for unqualified columns) + */ + public OrderedColumn(String columnName, String tableName) { + this(columnName, tableName, Optional.empty(), Optional.empty()); + } + + // Factory methods for convenience + + public static OrderedColumn asc(String columnName) { + return new OrderedColumn(columnName, null, SortDirection.ASC); + } + + public static OrderedColumn asc(String tableName, String columnName) { + return new OrderedColumn(columnName, tableName, SortDirection.ASC); + } + + public static OrderedColumn desc(String columnName) { + return new OrderedColumn(columnName, null, SortDirection.DESC); + } + + public static OrderedColumn desc(String tableName, String columnName) { + return new OrderedColumn(columnName, tableName, SortDirection.DESC); + } + + public static OrderedColumn of(String columnName, SortDirection direction, NullsOrder nullsOrder) { + return new OrderedColumn(columnName, null, Optional.ofNullable(direction), Optional.ofNullable(nullsOrder)); + } + + public static OrderedColumn of(String tableName, String columnName, SortDirection direction, + NullsOrder nullsOrder) { + return new OrderedColumn(columnName, tableName, Optional.ofNullable(direction), + Optional.ofNullable(nullsOrder)); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/sql/SortDirection.java b/model/src/main/java/org/eclipse/daanse/sql/model/sql/SortDirection.java new file mode 100644 index 0000000..ee9f0e8 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/sql/SortDirection.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * SmartCity Jena - initial + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.model.sql; + +/** Sort direction for an {@code ORDER BY} key. */ +public enum SortDirection { + + /** Ascending order — smallest first ({@code ORDER BY ... ASC}). */ + ASC, + + /** Descending order — largest first ({@code ORDER BY ... DESC}). */ + DESC +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/sql/package-info.java b/model/src/main/java/org/eclipse/daanse/sql/model/sql/package-info.java new file mode 100644 index 0000000..133e642 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/sql/package-info.java @@ -0,0 +1,17 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.model.sql; diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/type/BestFitColumnType.java b/model/src/main/java/org/eclipse/daanse/sql/model/type/BestFitColumnType.java new file mode 100644 index 0000000..1935e45 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/type/BestFitColumnType.java @@ -0,0 +1,39 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ +package org.eclipse.daanse.sql.model.type; + +/** Java result-set value type that best represents a JDBC column. */ +public enum BestFitColumnType { + + /** Generic {@link Object} — fallback for unrecognized SQL types. */ + OBJECT, + + /** {@code double} — for {@code DOUBLE}, {@code FLOAT}, {@code REAL}. */ + DOUBLE, + + /** {@code int} — for {@code INTEGER} and smaller integer types. */ + INT, + + /** {@code long} — for {@code BIGINT} and large integer types. */ + LONG, + + /** + * {@link String} — for {@code CHAR}/{@code VARCHAR}/{@code TEXT}-like types. + */ + STRING, + + /** {@link java.math.BigDecimal} — for {@code DECIMAL}/{@code NUMERIC}. */ + DECIMAL +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/type/Datatype.java b/model/src/main/java/org/eclipse/daanse/sql/model/type/Datatype.java new file mode 100644 index 0000000..bd89582 --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/type/Datatype.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2002-2017 Hitachi Vantara.. All rights reserved. + * + * For more information please visit the Project: Hitachi Vantara - Mondrian + * + * ---- All changes after Fork in 2023 ------------------------ + * + * Project: Eclipse daanse + * + * Copyright (c) 2023 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors after Fork in 2023: + * SmartCity Jena - initial adapt parts of Syntax.class + * Stefan Bischof (bipolis.org) - initial + */ +package org.eclipse.daanse.sql.model.type; + +import java.util.stream.Stream; + + +/** + * Engine-neutral SQL data type tag used by literal-quoting and DDL emission. + */ +public enum Datatype { + + /** Variable-length character string ({@code VARCHAR}). */ + VARCHAR("Varchar"), + /** Generic exact-numeric type ({@code NUMERIC}). */ + NUMERIC("Numeric"), + /** 32-bit integer ({@code INTEGER}). */ + INTEGER("Integer"), + /** Exact numeric with explicit precision/scale ({@code DECIMAL}). */ + DECIMAL("Decimal"), + /** Approximate single-precision floating-point ({@code FLOAT}). */ + FLOAT("Float"), + /** Approximate single-precision floating-point ({@code REAL}). */ + REAL("Real"), + /** 64-bit integer ({@code BIGINT}). */ + BIGINT("BigInt"), + /** 16-bit integer ({@code SMALLINT}). */ + SMALLINT("SmallInt"), + /** Approximate double-precision floating-point ({@code DOUBLE}). */ + DOUBLE("Double"), + /** Boolean truth value ({@code BOOLEAN}). */ + BOOLEAN("Boolean"), + /** Calendar date without time-of-day ({@code DATE}). */ + DATE("Date"), + /** Time-of-day without a date ({@code TIME}). */ + TIME("Time"), + /** Date and time-of-day ({@code TIMESTAMP}). */ + TIMESTAMP("Timestamp"), + /** Binary / variable-length octet string (BLOB, VARBINARY, BYTEA). */ + BINARY("Binary"), + /** + * UUID / GUID. PostgreSQL native, MSSQL {@code uniqueidentifier}, MySQL stored + * as {@code CHAR(36)}. + */ + UUID("Uuid"), + /** JSON / JSONB document. */ + JSON("Json"), + /** XML document. */ + XML("Xml"), + /** Day–time or year–month interval. */ + INTERVAL("Interval"), + /** SQL ARRAY collection of one of the scalar types above. */ + ARRAY("Array"), + /** Composite / row / struct / object type. */ + STRUCT("Struct"); + + private final String value; + + Datatype(String value) { + this.value = value; + } + + /** @return the canonical mixed-case display name (e.g. {@code BigInt}) */ + public String getValue() { + return value; + } + + /** @return true if this datatype represents a numeric value */ + public boolean isNumeric() { + return switch (this) { + case NUMERIC, INTEGER, DECIMAL, FLOAT, REAL, BIGINT, SMALLINT, DOUBLE -> true; + case VARCHAR, BOOLEAN, DATE, TIME, TIMESTAMP, BINARY, UUID, JSON, XML, INTERVAL, ARRAY, STRUCT -> false; + }; + } + + /** + * @return {@code true} for character or character-like types ({@link #VARCHAR}, + * {@link #JSON}, {@link #XML}, {@link #UUID}) + */ + public boolean isText() { + return switch (this) { + case VARCHAR, JSON, XML, UUID -> true; + default -> false; + }; + } + + /** @return {@code true} for date, time, timestamp, or interval types */ + public boolean isTemporal() { + return switch (this) { + case DATE, TIME, TIMESTAMP, INTERVAL -> true; + default -> false; + }; + } + + /** True for binary octet-stream types ({@link #BINARY}). */ + public boolean isBinary() { + return this == BINARY; + } + + /** True for collection/composite types ({@link #ARRAY}, {@link #STRUCT}). */ + public boolean isComposite() { + return this == ARRAY || this == STRUCT; + } + + /** + * @param v SQL type name or canonical mixed-case display name; tolerates + * whitespace and case differences + * @return the matching {@link Datatype}, or {@link #NUMERIC} when {@code v} is + * {@code null} or unrecognized + */ + public static Datatype fromValue(String v) { + if (v == null) + return NUMERIC; + String key = v.trim().toUpperCase().replaceAll("\\s+", " "); + Datatype byAlias = SQL_NAME_ALIASES.get(key); + if (byAlias != null) + return byAlias; + return Stream.of(Datatype.values()).filter(e -> e.getValue().equalsIgnoreCase(v)).findFirst().orElse(NUMERIC); + } + + private static final java.util.Map SQL_NAME_ALIASES = sqlNameAliases(); + + private static java.util.Map sqlNameAliases() { + java.util.Map m = new java.util.HashMap<>(); + // Character types — every flavour maps to VARCHAR. + m.put("VARCHAR", VARCHAR); + m.put("CHAR", VARCHAR); + m.put("CHARACTER", VARCHAR); + m.put("CHARACTER VARYING", VARCHAR); + m.put("CHARACTER LARGE OBJECT", VARCHAR); + m.put("LONGVARCHAR", VARCHAR); + m.put("NVARCHAR", VARCHAR); + m.put("NCHAR", VARCHAR); + m.put("NATIONAL CHARACTER", VARCHAR); + m.put("NATIONAL CHARACTER VARYING", VARCHAR); + m.put("CLOB", VARCHAR); + m.put("NCLOB", VARCHAR); + // Integral. + m.put("INT", INTEGER); + m.put("INTEGER", INTEGER); + m.put("BIGINT", BIGINT); + m.put("SMALLINT", SMALLINT); + m.put("TINYINT", SMALLINT); + // Numeric (non-integral). + m.put("NUMERIC", NUMERIC); + m.put("DECIMAL", DECIMAL); + m.put("FLOAT", FLOAT); + m.put("REAL", REAL); + m.put("DOUBLE", DOUBLE); + m.put("DOUBLE PRECISION", DOUBLE); + // Boolean / bit. + m.put("BOOLEAN", BOOLEAN); + m.put("BIT", BOOLEAN); + // Temporal. + m.put("DATE", DATE); + m.put("TIME", TIME); + m.put("TIME WITH TIMEZONE", TIME); + m.put("TIMESTAMP", TIMESTAMP); + m.put("TIMESTAMP WITH TIMEZONE", TIMESTAMP); + m.put("INTERVAL", INTERVAL); + // Binary. + m.put("BINARY", BINARY); + m.put("VARBINARY", BINARY); + m.put("LONGVARBINARY", BINARY); + m.put("BLOB", BINARY); + m.put("BYTEA", BINARY); + m.put("RAW", BINARY); + m.put("BINARY LARGE OBJECT", BINARY); + // Modern scalars. + m.put("UUID", UUID); + m.put("UNIQUEIDENTIFIER", UUID); + m.put("JSON", JSON); + m.put("JSONB", JSON); + m.put("XML", XML); + // Collections. + m.put("ARRAY", ARRAY); + m.put("STRUCT", STRUCT); + m.put("ROW", STRUCT); + return m; + } + + /** + * @param v the string value to parse + * @return the corresponding Datatype, or {@code null} if not found + */ + public static Datatype fromValueOrNull(String v) { + if (v == null) + return null; + String key = v.trim().toUpperCase().replaceAll("\\s+", " "); + Datatype byAlias = SQL_NAME_ALIASES.get(key); + if (byAlias != null) + return byAlias; + return Stream.of(Datatype.values()).filter(e -> e.getValue().equalsIgnoreCase(v)).findFirst().orElse(null); + } +} diff --git a/model/src/main/java/org/eclipse/daanse/sql/model/type/package-info.java b/model/src/main/java/org/eclipse/daanse/sql/model/type/package-info.java new file mode 100644 index 0000000..4843e2e --- /dev/null +++ b/model/src/main/java/org/eclipse/daanse/sql/model/type/package-info.java @@ -0,0 +1,18 @@ +/* +* Copyright (c) 2022 Contributors to the Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +* +* Contributors: +* SmartCity Jena - initial +* Stefan Bischof (bipolis.org) - initial +* Sergei Semenkov - initial +*/ + +@org.osgi.annotation.bundle.Export +@org.osgi.annotation.versioning.Version("0.0.1") +package org.eclipse.daanse.sql.model.type; diff --git a/pom.xml b/pom.xml index 1fb2acc..1a486d9 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,9 @@ + model + dialect + jdbc guard deparser statement diff --git a/statement/api/pom.xml b/statement/api/pom.xml index c54c330..eafabfb 100644 --- a/statement/api/pom.xml +++ b/statement/api/pom.xml @@ -30,7 +30,7 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.api + org.eclipse.daanse.sql.dialect.api 0.0.1-SNAPSHOT diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java index 0ee40b0..0e971a5 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/DeleteStatementBuilder.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Objects; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.model.DeleteStatement; diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java index a9c937a..9efb4ce 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/Expressions.java @@ -16,8 +16,8 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.dialect.api.generator.KnownFunction; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.model.type.Datatype; import org.eclipse.daanse.sql.statement.api.expression.ArithmeticOperator; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; @@ -44,21 +44,21 @@ public static SqlExpression column(TableAlias table, String name) { } /** - * A column from a jdbc.db - * {@link org.eclipse.daanse.jdbc.db.api.schema.ColumnReference}, qualified by + * A column from a sql.model + * {@link org.eclipse.daanse.sql.model.schema.ColumnReference}, qualified by * the given query alias. Only the column's {@code name()} is used; the * reference's own table is metadata and is not the in-query qualifier. */ - public static SqlExpression column(TableAlias table, org.eclipse.daanse.jdbc.db.api.schema.ColumnReference column) { + public static SqlExpression column(TableAlias table, org.eclipse.daanse.sql.model.schema.ColumnReference column) { return new SqlExpression.Column(Optional.of(table.name()), column.name()); } /** - * An unqualified column from a jdbc.db - * {@link org.eclipse.daanse.jdbc.db.api.schema.ColumnReference} (uses + * An unqualified column from a sql.model + * {@link org.eclipse.daanse.sql.model.schema.ColumnReference} (uses * {@code name()} only). */ - public static SqlExpression column(org.eclipse.daanse.jdbc.db.api.schema.ColumnReference column) { + public static SqlExpression column(org.eclipse.daanse.sql.model.schema.ColumnReference column) { return new SqlExpression.Column(Optional.empty(), column.name()); } diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java index 71d2530..1f10497 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/From.java @@ -16,8 +16,8 @@ import java.util.Map; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.model.FromClause; import org.eclipse.daanse.sql.statement.api.model.SelectStatement; diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java index 77716e4..aa97e4b 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/InsertStatementBuilder.java @@ -18,7 +18,7 @@ import java.util.Objects; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; import org.eclipse.daanse.sql.statement.api.model.InsertStatement; import org.eclipse.daanse.sql.statement.api.model.Statement; diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java index 2404485..3ba8193 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/SelectStatementBuilder.java @@ -18,8 +18,8 @@ import java.util.Objects; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.dialect.api.generator.StatementHint; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; import org.eclipse.daanse.sql.statement.api.model.ColumnAlias; diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java index d7300ad..aa81b60 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/UpdateStatementBuilder.java @@ -17,7 +17,7 @@ import java.util.List; import java.util.Objects; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; import org.eclipse.daanse.sql.statement.api.model.UpdateStatement; diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/expression/SqlExpression.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/expression/SqlExpression.java index e2358f9..854cd0f 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/expression/SqlExpression.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/expression/SqlExpression.java @@ -16,9 +16,9 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.sql.BitOperation; -import org.eclipse.daanse.jdbc.db.api.sql.OrderedColumn; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.sql.OrderedColumn; +import org.eclipse.daanse.sql.model.type.Datatype; import org.eclipse.daanse.sql.statement.api.model.SelectStatement; /** @@ -102,7 +102,7 @@ record Aggregate(String name, boolean distinct, List arguments) i /** * A portable well-known function call, identified by - * {@link org.eclipse.daanse.jdbc.db.dialect.api.generator.KnownFunction KnownFunction} + * {@link org.eclipse.daanse.sql.dialect.api.generator.KnownFunction KnownFunction} * intent rather than a verbatim function name — the dialect's * {@code FunctionGenerator} chooses the spelling at render time (e.g. * {@code LENGTH} renders as {@code CHAR_LENGTH(x)} on ANSI but {@code LEN(x)} @@ -117,7 +117,7 @@ record Aggregate(String name, boolean distinct, List arguments) i * renderer's dialect generator; the {@code Expressions} * factories validate it eagerly) */ - record KnownCall(org.eclipse.daanse.jdbc.db.dialect.api.generator.KnownFunction function, + record KnownCall(org.eclipse.daanse.sql.dialect.api.generator.KnownFunction function, List arguments) implements SqlExpression { public KnownCall { arguments = List.copyOf(arguments); diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/DeleteStatement.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/DeleteStatement.java index 19d2a4d..ff741e7 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/DeleteStatement.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/DeleteStatement.java @@ -16,13 +16,13 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; /** * A {@code DELETE FROM table [WHERE ...]} statement. * - * @param table the target table (shared jdbc.db identifier) + * @param table the target table (shared sql.model identifier) * @param filters {@code WHERE} predicates, combined with {@code AND} * (empty = delete all) * @param footerComment an optional trailing explanatory comment, appended on diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/FromClause.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/FromClause.java index 3d43d21..99efc72 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/FromClause.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/FromClause.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; /** @@ -45,7 +45,7 @@ sealed interface Aliased extends FromClause /** * A base table reference. * - * @param table the (optionally schema-qualified) table, as the shared jdbc.db + * @param table the (optionally schema-qualified) table, as the shared sql.model * identifier * @param alias the query-local table alias * @param filter an optional per-table filter to add to {@code WHERE} diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/InsertStatement.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/InsertStatement.java index 44c0b88..89538b5 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/InsertStatement.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/InsertStatement.java @@ -16,14 +16,14 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; /** * An {@code INSERT INTO table (cols...)} statement, sourcing rows either from * inline {@code VALUES} or from a sub-query ({@code INSERT ... SELECT}). * - * @param table the target table (shared jdbc.db identifier) + * @param table the target table (shared sql.model identifier) * @param columns target column names (unquoted); empty means positional * insert * @param rows inline value rows (each row's size should match diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/Projection.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/Projection.java index af4236e..0f410c0 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/Projection.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/Projection.java @@ -15,7 +15,7 @@ import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; /** diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SelectStatement.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SelectStatement.java index cf75ac6..921d69a 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SelectStatement.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SelectStatement.java @@ -17,7 +17,7 @@ import java.util.Map; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; import org.eclipse.daanse.sql.statement.api.expression.Predicate; /** diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SortSpec.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SortSpec.java index 7f515ad..dd296a7 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SortSpec.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/SortSpec.java @@ -39,12 +39,12 @@ * {@code "de_DE"}). */ public record SortSpec(SortDirection direction, boolean nullable, NullOrder nullOrder, boolean prepend, - String nullSortValue, org.eclipse.daanse.jdbc.db.api.type.Datatype nullSortDatatype, + String nullSortValue, org.eclipse.daanse.sql.model.type.Datatype nullSortDatatype, Optional collation) { /** Compatibility constructor without a collation. */ public SortSpec(SortDirection direction, boolean nullable, NullOrder nullOrder, boolean prepend, - String nullSortValue, org.eclipse.daanse.jdbc.db.api.type.Datatype nullSortDatatype) { + String nullSortValue, org.eclipse.daanse.sql.model.type.Datatype nullSortDatatype) { this(direction, nullable, nullOrder, prepend, nullSortValue, nullSortDatatype, Optional.empty()); } @@ -75,7 +75,7 @@ public SortSpec prepended() { * @return a copy ordering nulls as if they held {@code value} (of type * {@code datatype}), via the dialect's order-value generator. */ - public SortSpec withNullSortValue(String value, org.eclipse.daanse.jdbc.db.api.type.Datatype datatype) { + public SortSpec withNullSortValue(String value, org.eclipse.daanse.sql.model.type.Datatype datatype) { return new SortSpec(direction, nullable, nullOrder, prepend, value, datatype, collation); } diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/UpdateStatement.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/UpdateStatement.java index add4f69..61f94ec 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/UpdateStatement.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/model/UpdateStatement.java @@ -16,14 +16,14 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; +import org.eclipse.daanse.sql.model.schema.TableReference; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; /** * An {@code UPDATE table SET col = expr, ... [WHERE ...]} statement. * - * @param table the target table (shared jdbc.db identifier) + * @param table the target table (shared sql.model identifier) * @param assignments the {@code SET} assignments (order preserved) * @param filters {@code WHERE} predicates, combined with {@code AND} * (empty = unfiltered) diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/BoundParameter.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/BoundParameter.java index 257ea7d..17bab83 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/BoundParameter.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/BoundParameter.java @@ -13,7 +13,7 @@ */ package org.eclipse.daanse.sql.statement.api.render; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; +import org.eclipse.daanse.sql.model.type.Datatype; /** * One bind parameter of a rendered statement, in placeholder order. diff --git a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/RenderedSql.java b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/RenderedSql.java index 9d1edf2..3942ddf 100644 --- a/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/RenderedSql.java +++ b/statement/api/src/main/java/org/eclipse/daanse/sql/statement/api/render/RenderedSql.java @@ -15,7 +15,7 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; /** * The result of rendering a statement: the SQL text, the column types to read diff --git a/statement/demo/pom.xml b/statement/demo/pom.xml index 9b70361..b090050 100644 --- a/statement/demo/pom.xml +++ b/statement/demo/pom.xml @@ -47,18 +47,18 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.common + org.eclipse.daanse.sql.dialect.db.common 0.0.1-SNAPSHOT org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.mssqlserver + org.eclipse.daanse.sql.dialect.db.mssqlserver 0.0.1-SNAPSHOT test org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.mysql + org.eclipse.daanse.sql.dialect.db.mysql 0.0.1-SNAPSHOT test diff --git a/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/ResultReaderDemo.java b/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/ResultReaderDemo.java index 0b62b2a..f1b86e6 100644 --- a/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/ResultReaderDemo.java +++ b/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/ResultReaderDemo.java @@ -19,10 +19,10 @@ import java.util.List; import java.util.Map; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemo.java b/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemo.java index e512b79..b28dbb3 100644 --- a/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemo.java +++ b/statement/demo/src/main/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemo.java @@ -15,9 +15,9 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/AggregateTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/AggregateTest.java index df45d85..f833124 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/AggregateTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/AggregateTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CollationOrderByTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CollationOrderByTest.java index aa789f4..64ddabf 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CollationOrderByTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CollationOrderByTest.java @@ -16,8 +16,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ConstantPredicateTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ConstantPredicateTest.java index 24deaa4..6d28440 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ConstantPredicateTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ConstantPredicateTest.java @@ -16,9 +16,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CteTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CteTest.java index 6b15169..1e96a37 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CteTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/CteTest.java @@ -20,9 +20,9 @@ import java.sql.Statement; import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DataSourceExecutorTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DataSourceExecutorTest.java index 79a955c..57e4a2f 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DataSourceExecutorTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DataSourceExecutorTest.java @@ -18,9 +18,9 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DmlTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DmlTest.java index b5348a1..fd84459 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DmlTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/DmlTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.DeleteStatementBuilder; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExistsPredicateTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExistsPredicateTest.java index 1cdbb22..5d11a15 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExistsPredicateTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExistsPredicateTest.java @@ -17,9 +17,9 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExtraAggregateTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExtraAggregateTest.java index 6080991..bfb30f2 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExtraAggregateTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ExtraAggregateTest.java @@ -17,10 +17,10 @@ import java.util.Optional; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.sql.BitOperation; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.sql.BitOperation; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FooterCommentTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FooterCommentTest.java index 858ff69..11a77e3 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FooterCommentTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FooterCommentTest.java @@ -20,9 +20,9 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromInlineTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromInlineTest.java index c32d237..ba138b1 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromInlineTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromInlineTest.java @@ -17,10 +17,10 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromSetTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromSetTest.java index faacd15..7b0d235 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromSetTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromSetTest.java @@ -18,10 +18,10 @@ import java.util.List; import java.util.Optional; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromVariantTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromVariantTest.java index d8396b5..b2e6d2f 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromVariantTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/FromVariantTest.java @@ -17,10 +17,10 @@ import java.util.Map; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/InlineCommentHardeningTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/InlineCommentHardeningTest.java index fbe0fd4..7021451 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/InlineCommentHardeningTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/InlineCommentHardeningTest.java @@ -17,9 +17,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/KnownCallTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/KnownCallTest.java index c1ef1ee..4b59e16 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/KnownCallTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/KnownCallTest.java @@ -18,10 +18,10 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.generator.KnownFunction; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mssqlserver.MicrosoftSqlServerDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.generator.KnownFunction; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; import org.eclipse.daanse.sql.statement.render.DialectSqlRenderer; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OrdinalOrderByTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OrdinalOrderByTest.java index 1e3814b..2519412 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OrdinalOrderByTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OrdinalOrderByTest.java @@ -17,8 +17,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OuterJoinRenderingTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OuterJoinRenderingTest.java index c4834cd..65c6145 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OuterJoinRenderingTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/OuterJoinRenderingTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ParameterAndBatchTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ParameterAndBatchTest.java index 44e87d3..d5b8e9e 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ParameterAndBatchTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ParameterAndBatchTest.java @@ -21,9 +21,9 @@ import java.sql.Statement; import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RecursiveCteTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RecursiveCteTest.java index 188c413..8940419 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RecursiveCteTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RecursiveCteTest.java @@ -20,10 +20,10 @@ import java.sql.DriverManager; import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.InsertStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RendererHardeningTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RendererHardeningTest.java index 3e20476..a19cca6 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RendererHardeningTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/RendererHardeningTest.java @@ -16,12 +16,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mssqlserver.MicrosoftSqlServerDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ScalarSubqueryTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ScalarSubqueryTest.java index 4bfa360..847fc66 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ScalarSubqueryTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/ScalarSubqueryTest.java @@ -17,9 +17,9 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/SqlCommentRenderingTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/SqlCommentRenderingTest.java index 1d2ef35..e7962fb 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/SqlCommentRenderingTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/SqlCommentRenderingTest.java @@ -17,9 +17,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StarProjectionTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StarProjectionTest.java index 4882539..0caf015 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StarProjectionTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StarProjectionTest.java @@ -15,8 +15,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemoTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemoTest.java index c21cf6c..8fba2f1 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemoTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementBuilderDemoTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; import org.eclipse.daanse.sql.statement.render.DialectSqlRenderer; import org.junit.jupiter.api.Test; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementHintTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementHintTest.java index 02dcc31..0f93d33 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementHintTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/StatementHintTest.java @@ -18,11 +18,11 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.generator.StatementHint; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mssqlserver.MicrosoftSqlServerDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.api.generator.StatementHint; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mssqlserver.MicrosoftSqlServerDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/TableRefTest.java b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/TableRefTest.java index 12d5336..839891e 100644 --- a/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/TableRefTest.java +++ b/statement/demo/src/test/java/org/eclipse/daanse/sql/statement/demo/TableRefTest.java @@ -17,12 +17,12 @@ import java.util.Optional; -import org.eclipse.daanse.jdbc.db.api.schema.ColumnReference; -import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.model.schema.ColumnReference; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.DeleteStatementBuilder; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; @@ -32,7 +32,7 @@ import org.eclipse.daanse.sql.statement.render.DialectSqlRenderer; import org.junit.jupiter.api.Test; -/** Uses the shared jdbc.db identifier types ({@code TableReference}/{@code ColumnReference}). */ +/** Uses the shared sql.model identifier types ({@code TableReference}/{@code ColumnReference}). */ class TableRefTest { private final DialectSqlRenderer renderer = new DialectSqlRenderer(new AnsiDialect()); diff --git a/statement/impl/pom.xml b/statement/impl/pom.xml index b6c26cd..fe3c673 100644 --- a/statement/impl/pom.xml +++ b/statement/impl/pom.xml @@ -34,18 +34,18 @@ org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.api + org.eclipse.daanse.sql.dialect.api 0.0.1-SNAPSHOT org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.common + org.eclipse.daanse.sql.dialect.db.common 0.0.1-SNAPSHOT test org.eclipse.daanse - org.eclipse.daanse.jdbc.db.dialect.db.mysql + org.eclipse.daanse.sql.dialect.db.mysql 0.0.1-SNAPSHOT test diff --git a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/ColumnAccessors.java b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/ColumnAccessors.java index e50ff22..1cd1fe5 100644 --- a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/ColumnAccessors.java +++ b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/ColumnAccessors.java @@ -16,7 +16,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; /** * Reads a single column from a {@link ResultSet} as the right Java type for a diff --git a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/JdbcRow.java b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/JdbcRow.java index dce6e49..08447a2 100644 --- a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/JdbcRow.java +++ b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/exec/JdbcRow.java @@ -17,7 +17,7 @@ import java.sql.SQLException; import java.util.List; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; import org.eclipse.daanse.sql.statement.api.exec.StatementExecutionException; import org.eclipse.daanse.sql.statement.api.result.Row; diff --git a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java index 9b6db68..f98cfe1 100644 --- a/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java +++ b/statement/impl/src/main/java/org/eclipse/daanse/sql/statement/render/DialectSqlRenderer.java @@ -21,10 +21,10 @@ import java.util.Set; import java.util.stream.Collectors; -import org.eclipse.daanse.jdbc.db.api.schema.SchemaReference; -import org.eclipse.daanse.jdbc.db.api.schema.TableReference; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.schema.SchemaReference; +import org.eclipse.daanse.sql.model.schema.TableReference; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; import org.eclipse.daanse.sql.statement.api.expression.ComparisonOperator; import org.eclipse.daanse.sql.statement.api.expression.Predicate; import org.eclipse.daanse.sql.statement.api.expression.SqlExpression; @@ -138,7 +138,7 @@ private RenderedSql renderInternal(Statement statement, RenderOptions options) { private RenderedSql renderWith(WithStatement with) { // CTE bodies render first, so their bind parameters precede the body's, in order. - List ctes = new ArrayList<>(); + List ctes = new ArrayList<>(); for (org.eclipse.daanse.sql.statement.api.model.CommonTableExpression cte : with.ctes()) { String body = renderInternal(cte.query(), RenderOptions.compact()).sql(); // Quote the CTE name (so body references via From.table(name, ...) match the casing); @@ -148,7 +148,7 @@ private RenderedSql renderWith(WithStatement with) { name += "(" + cte.columns().stream().map(dialect::quoteIdentifier).collect(Collectors.joining(", ")) + ")"; } - ctes.add(new org.eclipse.daanse.jdbc.db.dialect.api.generator.CteGenerator.Cte(name, body)); + ctes.add(new org.eclipse.daanse.sql.dialect.api.generator.CteGenerator.Cte(name, body)); } String withClause = dialect.cteGenerator().withClause(ctes, with.recursive()); RenderedSql body = renderInternal(with.body(), RenderOptions.compact()); @@ -882,7 +882,7 @@ public String renderExpression(SqlExpression e) { * (the legacy code returned {@code null} for the same case). */ private String renderExtraAggregate(SqlExpression.ExtraAggregate ea) { - org.eclipse.daanse.jdbc.db.dialect.api.generator.AggregationGenerator g = dialect.aggregationGenerator(); + org.eclipse.daanse.sql.dialect.api.generator.AggregationGenerator g = dialect.aggregationGenerator(); CharSequence operand = ea.operand().map(this::renderExpression).orElse(null); SqlExpression.ExtraAggregate.Spec spec = ea.spec(); java.util.Optional sql; diff --git a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/DistinctCountSubqueryRewriteTest.java b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/DistinctCountSubqueryRewriteTest.java index dc33814..606c009 100644 --- a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/DistinctCountSubqueryRewriteTest.java +++ b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/DistinctCountSubqueryRewriteTest.java @@ -15,9 +15,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/FromRawAliasingTest.java b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/FromRawAliasingTest.java index 82fe77a..8a6bee5 100644 --- a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/FromRawAliasingTest.java +++ b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/FromRawAliasingTest.java @@ -15,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.SelectStatementBuilder; diff --git a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/InTupleRenderingTest.java b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/InTupleRenderingTest.java index b64b1bd..922cfa8 100644 --- a/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/InTupleRenderingTest.java +++ b/statement/impl/src/test/java/org/eclipse/daanse/sql/statement/render/InTupleRenderingTest.java @@ -17,11 +17,11 @@ import java.util.List; -import org.eclipse.daanse.jdbc.db.dialect.api.Dialect; -import org.eclipse.daanse.jdbc.db.api.type.BestFitColumnType; -import org.eclipse.daanse.jdbc.db.api.type.Datatype; -import org.eclipse.daanse.jdbc.db.dialect.db.common.AnsiDialect; -import org.eclipse.daanse.jdbc.db.dialect.db.mysql.MySqlDialect; +import org.eclipse.daanse.sql.dialect.api.Dialect; +import org.eclipse.daanse.sql.model.type.BestFitColumnType; +import org.eclipse.daanse.sql.model.type.Datatype; +import org.eclipse.daanse.sql.dialect.db.common.AnsiDialect; +import org.eclipse.daanse.sql.dialect.db.mysql.MySqlDialect; import org.eclipse.daanse.sql.statement.api.Expressions; import org.eclipse.daanse.sql.statement.api.From; import org.eclipse.daanse.sql.statement.api.Predicates;