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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion .github/workflows/live-dialects.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,44 @@ jobs:
- name: Verify both live dialects (skips are forbidden)
run: >-
mvn -B -ntp -pl teaql-postgres,teaql-mysql,examples/business-id-runtime -am
-Dtest=PostgresIntegrationTest,MysqlIntegrationTest,JdbcBusinessIdLiveDialectIT
-Dtest=PostgresIntegrationTest,MysqlIntegrationTest,JdbcBusinessIdLiveDialectIT#postgresAllocatesAcrossInstancesAndRestart+mysqlAllocatesAcrossInstancesAndRestart
-Dsurefire.failIfNoSpecifiedTests=false test

business-id-sqlserver:
runs-on: ubuntu-latest
services:
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
env:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: TeaqlBusinessLiveTest123!
ports:
- 1433:1433
options: >-
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P TeaqlBusinessLiveTest123! -C -Q 'SELECT 1' -b"
--health-interval 10s --health-timeout 5s --health-retries 20
env:
TEAQL_REQUIRE_LIVE_DB: "true"
TEAQL_TEST_MSSQL_URL: "jdbc:sqlserver://127.0.0.1:1433;databaseName=teaql_live_ci;encrypt=false;trustServerCertificate=true"
TEAQL_TEST_MSSQL_USER: sa
TEAQL_TEST_MSSQL_PASSWORD: TeaqlBusinessLiveTest123!
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Create isolated Business ID test database
run: >-
docker exec "${{ job.services.sqlserver.id }}"
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa
-P TeaqlBusinessLiveTest123! -C
-Q 'CREATE DATABASE teaql_live_ci'
- name: Verify SQL Server Business ID schema and allocation (skips are forbidden)
run: >-
mvn -B -ntp -pl examples/business-id-runtime -am
'-Dtest=JdbcBusinessIdLiveDialectIT#sqlServerAllocatesAcrossInstancesAndRestart+sqlServerConcurrentSchemaStartupIsIdempotent'
-Dsurefire.failIfNoSpecifiedTests=false test

sqlserver:
Expand Down
33 changes: 33 additions & 0 deletions examples/business-id-runtime/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,36 @@ mvn -pl examples/business-id-runtime -am \
-Dtest=BusinessIdRuntimeExampleTest \
-Dsurefire.failIfNoSpecifiedTests=false test
```

The live suite also verifies PostgreSQL, MySQL, and SQL Server in CI. The
SQL Server cases include independent-connection allocation and concurrent
schema startup. DM8 and Oracle are local-only live checks until durable CI
images and dedicated test credentials are available. Run DM8 only against a
disposable instance;
the test refuses to run without `TEAQL_TEST_DM8_ISOLATED=true`:

```bash
TEAQL_REQUIRE_LIVE_DB=true \
TEAQL_TEST_DM8_ISOLATED=true \
TEAQL_TEST_DM8_URL='jdbc:dm://127.0.0.1:5236' \
TEAQL_TEST_DM8_USER='SYSDBA' \
TEAQL_TEST_DM8_PASSWORD='<test-instance-password>' \
mvn -pl examples/business-id-runtime -am \
'-Dtest=JdbcBusinessIdLiveDialectIT#dm8AllocatesAcrossInstancesAndRestart' \
-Dsurefire.failIfNoSpecifiedTests=false test
```

For Oracle, create a dedicated `TEAQL_LIVE_*` user in an isolated test PDB,
then run the Oracle method with that user's credentials. Oracle requires
`new JdbcBusinessIdAllocator(database, table, new OracleDialect())` so the
Business ID counter uses `NUMBER(19,0)` rather than unsupported `BIGINT`:

```bash
TEAQL_REQUIRE_LIVE_DB=true \
TEAQL_TEST_ORACLE_URL='jdbc:oracle:thin:@//127.0.0.1:1521/FREEPDB1' \
TEAQL_TEST_ORACLE_USER='TEAQL_LIVE_BID' \
TEAQL_TEST_ORACLE_PASSWORD='<test-user-password>' \
mvn -pl examples/business-id-runtime -am \
'-Dtest=JdbcBusinessIdLiveDialectIT#oracleAllocatesAcrossInstancesAndRestart' \
-Dsurefire.failIfNoSpecifiedTests=false test
```
18 changes: 18 additions & 0 deletions examples/business-id-runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,23 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>13.4.0.jre11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.dameng</groupId>
<artifactId>DmJdbcDriver18</artifactId>
<version>8.1.3.140</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>23.9.0.25.07</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,27 @@ public void sqliteAllocatorRejectsMalformedSequenceRowWithoutRetryExhaustion()
}
}

@Test
public void sqliteAllocatorRejectsIncompleteExistingSchema() throws Exception {
Path databasePath = Files.createTempFile("teaql-business-id-incomplete-schema", ".db");
try (Connection connection = open(databasePath)) {
try (Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE teaql_business_id_space (scope_key TEXT PRIMARY KEY)");
}
JdbcBusinessIdAllocator allocator =
new JdbcBusinessIdAllocator(database(connection));
UserContext context = context(LocalDate.of(2026, 9, 20), allocator);
context.putAttribute(SchemaExecutor.class.getName(), noOpSchemaExecutor());

IllegalStateException failure = Assert.assertThrows(
IllegalStateException.class, context::ensureSchema);
Assert.assertTrue(failure.getMessage(),
failure.getMessage().contains("missing required columns"));
} finally {
Files.deleteIfExists(databasePath);
}
}

@Test
public void sqliteAllocatorRejectsExhaustedAndOverflowedSequencesWithoutMutation()
throws Exception {
Expand Down Expand Up @@ -419,7 +440,22 @@ public void executeInTransaction(Runnable action) {

@Override
public List<Map<String, Object>> getTableColumns(String tableName) {
throw new UnsupportedOperationException();
try {
List<Map<String, Object>> columns = new ArrayList<>();
java.sql.DatabaseMetaData metadata = connection.getMetaData();
for (String name : List.of(tableName, tableName.toUpperCase(Locale.ROOT))) {
try (ResultSet result = metadata.getColumns(
connection.getCatalog(), connection.getSchema(), name, null)) {
while (result.next()) {
columns.add(Map.of("column_name", result.getString("COLUMN_NAME")));
}
}
if (!columns.isEmpty()) break;
}
return columns;
} catch (SQLException error) {
throw new RuntimeException(error);
}
}

private void bind(PreparedStatement statement, Object[] args) throws SQLException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.teaql.core.businessid.BusinessIdGenerationRequest;
import io.teaql.core.businessid.BusinessIdPlan;
import io.teaql.runtime.businessid.DailySequenceBusinessIdProfile;
import io.teaql.core.sql.dialect.OracleDialect;
import java.sql.Connection;
import java.sql.DriverManager;
import java.time.LocalDate;
Expand All @@ -24,7 +25,7 @@
import org.junit.Assume;
import org.junit.Test;

/** Live, independent-connection Business ID allocation across PostgreSQL and MySQL. */
/** Live, independent-connection Business ID allocation across SQL providers. */
public class JdbcBusinessIdLiveDialectIT {
private static final LocalDate BUSINESS_DATE = LocalDate.of(2026, 9, 20);
private static final int ALLOCATIONS_PER_INSTANCE = 40;
Expand All @@ -39,6 +40,65 @@ public void mysqlAllocatesAcrossInstancesAndRestart() throws Exception {
verify("MYSQL");
}

@Test
public void sqlServerAllocatesAcrossInstancesAndRestart() throws Exception {
verify("MSSQL");
}

@Test
public void dm8AllocatesAcrossInstancesAndRestart() throws Exception {
verify("DM8");
}

@Test
public void oracleAllocatesAcrossInstancesAndRestart() throws Exception {
verify("ORACLE");
}

@Test
public void sqlServerConcurrentSchemaStartupIsIdempotent() throws Exception {
String url = System.getenv("TEAQL_TEST_MSSQL_URL");
String user = System.getenv("TEAQL_TEST_MSSQL_USER");
String password = System.getenv("TEAQL_TEST_MSSQL_PASSWORD");
if (url == null || user == null || password == null) {
if (Boolean.parseBoolean(System.getenv("TEAQL_REQUIRE_LIVE_DB"))) {
Assert.fail("MSSQL Business ID live gate requires URL, USER, and PASSWORD");
}
Assume.assumeTrue("MSSQL live database is not configured", false);
}
Assert.assertTrue("Use a dedicated teaql_live_* database for MSSQL",
url.contains("teaql_live_"));
String table = "bid_schema_" + UUID.randomUUID().toString().substring(0, 8);
CountDownLatch start = new CountDownLatch(1);
ExecutorService workers = Executors.newFixedThreadPool(2);
try {
List<Future<?>> futures = new ArrayList<>();
for (int worker = 0; worker < 2; worker++) {
futures.add(workers.submit(() -> {
try (Connection connection = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
BusinessIdRuntimeExampleTest.database(connection), table);
UserContext context = BusinessIdRuntimeExampleTest.context(
BUSINESS_DATE, allocator);
context.putAttribute(SchemaExecutor.class.getName(),
BusinessIdRuntimeExampleTest.noOpSchemaExecutor());
Assert.assertTrue(start.await(15, TimeUnit.SECONDS));
context.ensureSchema();
return null;
}
}));
}
start.countDown();
for (Future<?> future : futures) future.get(30, TimeUnit.SECONDS);
} finally {
workers.shutdownNow();
}
try (Connection connection = DriverManager.getConnection(url, user, password)) {
Assert.assertEquals(4, BusinessIdRuntimeExampleTest.database(connection)
.getTableColumns(table).size());
}
}

private void verify(String dialect) throws Exception {
String prefix = "TEAQL_TEST_" + dialect + "_";
String url = System.getenv(prefix + "URL");
Expand All @@ -50,8 +110,16 @@ private void verify(String dialect) throws Exception {
}
Assume.assumeTrue(dialect + " live database is not configured", false);
}
Assert.assertTrue("Use a dedicated teaql_live_* database for " + dialect,
url.contains("teaql_live_"));
if ("DM8".equals(dialect)) {
Assert.assertEquals("Use only an isolated disposable DM8 instance",
"true", System.getenv("TEAQL_TEST_DM8_ISOLATED"));
} else if ("ORACLE".equals(dialect)) {
Assert.assertTrue("Use only a dedicated TEAQL_LIVE_* Oracle user",
user.toUpperCase(java.util.Locale.ROOT).startsWith("TEAQL_LIVE_"));
} else {
Assert.assertTrue("Use a dedicated teaql_live_* database for " + dialect,
url.contains("teaql_live_"));
}

BusinessIdDefinition definition = BusinessIdDefinition.dailySequence(
"order_number", "CO", "commerce_order");
Expand All @@ -61,12 +129,12 @@ private void verify(String dialect) throws Exception {
"commerce_order", BUSINESS_DATE));

try (Connection schemaConnection = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
BusinessIdRuntimeExampleTest.database(schemaConnection));
JdbcBusinessIdAllocator allocator = allocator(schemaConnection, dialect);
UserContext context = BusinessIdRuntimeExampleTest.context(BUSINESS_DATE, allocator);
context.putAttribute(SchemaExecutor.class.getName(),
BusinessIdRuntimeExampleTest.noOpSchemaExecutor());
context.ensureSchema();
context.ensureSchema();
}

CountDownLatch start = new CountDownLatch(1);
Expand All @@ -76,8 +144,7 @@ private void verify(String dialect) throws Exception {
for (int worker = 0; worker < 2; worker++) {
futures.add(workers.submit(() -> {
try (Connection connection = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
BusinessIdRuntimeExampleTest.database(connection));
JdbcBusinessIdAllocator allocator = allocator(connection, dialect);
Assert.assertTrue("Concurrent start timed out",
start.await(15, TimeUnit.SECONDS));
List<Long> sequences = new ArrayList<>();
Expand All @@ -103,10 +170,19 @@ private void verify(String dialect) throws Exception {
}

try (Connection restarted = DriverManager.getConnection(url, user, password)) {
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
BusinessIdRuntimeExampleTest.database(restarted));
JdbcBusinessIdAllocator allocator = allocator(restarted, dialect);
Assert.assertEquals(2L * ALLOCATIONS_PER_INSTANCE + 1,
allocator.allocate(plan).sequence());
}
}

private static JdbcBusinessIdAllocator allocator(Connection connection, String dialect) {
if ("ORACLE".equals(dialect)) {
return new JdbcBusinessIdAllocator(
BusinessIdRuntimeExampleTest.database(connection),
JdbcBusinessIdAllocator.DEFAULT_TABLE,
new OracleDialect());
}
return new JdbcBusinessIdAllocator(BusinessIdRuntimeExampleTest.database(connection));
}
}
Loading
Loading