Skip to content

Commit 7229997

Browse files
philipgreatPhilip Z
andauthored
Make JDBC Business ID schema setup portable (#170)
* Make Business ID schema setup portable for SQL Server * Verify Business ID allocation on isolated DM8 * Map Oracle Business ID counters to integral NUMBER --------- Co-authored-by: Philip Z <hello@teaql.io>
1 parent 9ff4101 commit 7229997

8 files changed

Lines changed: 300 additions & 16 deletions

File tree

‎.github/workflows/live-dialects.yml‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,44 @@ jobs:
5050
- name: Verify both live dialects (skips are forbidden)
5151
run: >-
5252
mvn -B -ntp -pl teaql-postgres,teaql-mysql,examples/business-id-runtime -am
53-
-Dtest=PostgresIntegrationTest,MysqlIntegrationTest,JdbcBusinessIdLiveDialectIT
53+
-Dtest=PostgresIntegrationTest,MysqlIntegrationTest,JdbcBusinessIdLiveDialectIT#postgresAllocatesAcrossInstancesAndRestart+mysqlAllocatesAcrossInstancesAndRestart
54+
-Dsurefire.failIfNoSpecifiedTests=false test
55+
56+
business-id-sqlserver:
57+
runs-on: ubuntu-latest
58+
services:
59+
sqlserver:
60+
image: mcr.microsoft.com/mssql/server:2022-latest
61+
env:
62+
ACCEPT_EULA: "Y"
63+
MSSQL_SA_PASSWORD: TeaqlBusinessLiveTest123!
64+
ports:
65+
- 1433:1433
66+
options: >-
67+
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P TeaqlBusinessLiveTest123! -C -Q 'SELECT 1' -b"
68+
--health-interval 10s --health-timeout 5s --health-retries 20
69+
env:
70+
TEAQL_REQUIRE_LIVE_DB: "true"
71+
TEAQL_TEST_MSSQL_URL: "jdbc:sqlserver://127.0.0.1:1433;databaseName=teaql_live_ci;encrypt=false;trustServerCertificate=true"
72+
TEAQL_TEST_MSSQL_USER: sa
73+
TEAQL_TEST_MSSQL_PASSWORD: TeaqlBusinessLiveTest123!
74+
steps:
75+
- uses: actions/checkout@v4
76+
- uses: actions/setup-java@v4
77+
with:
78+
distribution: temurin
79+
java-version: "21"
80+
cache: maven
81+
- name: Create isolated Business ID test database
82+
run: >-
83+
docker exec "${{ job.services.sqlserver.id }}"
84+
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa
85+
-P TeaqlBusinessLiveTest123! -C
86+
-Q 'CREATE DATABASE teaql_live_ci'
87+
- name: Verify SQL Server Business ID schema and allocation (skips are forbidden)
88+
run: >-
89+
mvn -B -ntp -pl examples/business-id-runtime -am
90+
'-Dtest=JdbcBusinessIdLiveDialectIT#sqlServerAllocatesAcrossInstancesAndRestart+sqlServerConcurrentSchemaStartupIsIdempotent'
5491
-Dsurefire.failIfNoSpecifiedTests=false test
5592
5693
sqlserver:

‎examples/business-id-runtime/README.md‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,36 @@ mvn -pl examples/business-id-runtime -am \
3131
-Dtest=BusinessIdRuntimeExampleTest \
3232
-Dsurefire.failIfNoSpecifiedTests=false test
3333
```
34+
35+
The live suite also verifies PostgreSQL, MySQL, and SQL Server in CI. The
36+
SQL Server cases include independent-connection allocation and concurrent
37+
schema startup. DM8 and Oracle are local-only live checks until durable CI
38+
images and dedicated test credentials are available. Run DM8 only against a
39+
disposable instance;
40+
the test refuses to run without `TEAQL_TEST_DM8_ISOLATED=true`:
41+
42+
```bash
43+
TEAQL_REQUIRE_LIVE_DB=true \
44+
TEAQL_TEST_DM8_ISOLATED=true \
45+
TEAQL_TEST_DM8_URL='jdbc:dm://127.0.0.1:5236' \
46+
TEAQL_TEST_DM8_USER='SYSDBA' \
47+
TEAQL_TEST_DM8_PASSWORD='<test-instance-password>' \
48+
mvn -pl examples/business-id-runtime -am \
49+
'-Dtest=JdbcBusinessIdLiveDialectIT#dm8AllocatesAcrossInstancesAndRestart' \
50+
-Dsurefire.failIfNoSpecifiedTests=false test
51+
```
52+
53+
For Oracle, create a dedicated `TEAQL_LIVE_*` user in an isolated test PDB,
54+
then run the Oracle method with that user's credentials. Oracle requires
55+
`new JdbcBusinessIdAllocator(database, table, new OracleDialect())` so the
56+
Business ID counter uses `NUMBER(19,0)` rather than unsupported `BIGINT`:
57+
58+
```bash
59+
TEAQL_REQUIRE_LIVE_DB=true \
60+
TEAQL_TEST_ORACLE_URL='jdbc:oracle:thin:@//127.0.0.1:1521/FREEPDB1' \
61+
TEAQL_TEST_ORACLE_USER='TEAQL_LIVE_BID' \
62+
TEAQL_TEST_ORACLE_PASSWORD='<test-user-password>' \
63+
mvn -pl examples/business-id-runtime -am \
64+
'-Dtest=JdbcBusinessIdLiveDialectIT#oracleAllocatesAcrossInstancesAndRestart' \
65+
-Dsurefire.failIfNoSpecifiedTests=false test
66+
```

‎examples/business-id-runtime/pom.xml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,23 @@
4343
<artifactId>junit</artifactId>
4444
<scope>test</scope>
4545
</dependency>
46+
<dependency>
47+
<groupId>com.microsoft.sqlserver</groupId>
48+
<artifactId>mssql-jdbc</artifactId>
49+
<version>13.4.0.jre11</version>
50+
<scope>test</scope>
51+
</dependency>
52+
<dependency>
53+
<groupId>com.dameng</groupId>
54+
<artifactId>DmJdbcDriver18</artifactId>
55+
<version>8.1.3.140</version>
56+
<scope>test</scope>
57+
</dependency>
58+
<dependency>
59+
<groupId>com.oracle.database.jdbc</groupId>
60+
<artifactId>ojdbc11</artifactId>
61+
<version>23.9.0.25.07</version>
62+
<scope>test</scope>
63+
</dependency>
4664
</dependencies>
4765
</project>

‎examples/business-id-runtime/src/test/java/io/teaql/examples/businessid/BusinessIdRuntimeExampleTest.java‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,27 @@ public void sqliteAllocatorRejectsMalformedSequenceRowWithoutRetryExhaustion()
238238
}
239239
}
240240

241+
@Test
242+
public void sqliteAllocatorRejectsIncompleteExistingSchema() throws Exception {
243+
Path databasePath = Files.createTempFile("teaql-business-id-incomplete-schema", ".db");
244+
try (Connection connection = open(databasePath)) {
245+
try (Statement statement = connection.createStatement()) {
246+
statement.execute("CREATE TABLE teaql_business_id_space (scope_key TEXT PRIMARY KEY)");
247+
}
248+
JdbcBusinessIdAllocator allocator =
249+
new JdbcBusinessIdAllocator(database(connection));
250+
UserContext context = context(LocalDate.of(2026, 9, 20), allocator);
251+
context.putAttribute(SchemaExecutor.class.getName(), noOpSchemaExecutor());
252+
253+
IllegalStateException failure = Assert.assertThrows(
254+
IllegalStateException.class, context::ensureSchema);
255+
Assert.assertTrue(failure.getMessage(),
256+
failure.getMessage().contains("missing required columns"));
257+
} finally {
258+
Files.deleteIfExists(databasePath);
259+
}
260+
}
261+
241262
@Test
242263
public void sqliteAllocatorRejectsExhaustedAndOverflowedSequencesWithoutMutation()
243264
throws Exception {
@@ -419,7 +440,22 @@ public void executeInTransaction(Runnable action) {
419440

420441
@Override
421442
public List<Map<String, Object>> getTableColumns(String tableName) {
422-
throw new UnsupportedOperationException();
443+
try {
444+
List<Map<String, Object>> columns = new ArrayList<>();
445+
java.sql.DatabaseMetaData metadata = connection.getMetaData();
446+
for (String name : List.of(tableName, tableName.toUpperCase(Locale.ROOT))) {
447+
try (ResultSet result = metadata.getColumns(
448+
connection.getCatalog(), connection.getSchema(), name, null)) {
449+
while (result.next()) {
450+
columns.add(Map.of("column_name", result.getString("COLUMN_NAME")));
451+
}
452+
}
453+
if (!columns.isEmpty()) break;
454+
}
455+
return columns;
456+
} catch (SQLException error) {
457+
throw new RuntimeException(error);
458+
}
423459
}
424460

425461
private void bind(PreparedStatement statement, Object[] args) throws SQLException {

‎examples/business-id-runtime/src/test/java/io/teaql/examples/businessid/JdbcBusinessIdLiveDialectIT.java‎

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.teaql.core.businessid.BusinessIdGenerationRequest;
88
import io.teaql.core.businessid.BusinessIdPlan;
99
import io.teaql.runtime.businessid.DailySequenceBusinessIdProfile;
10+
import io.teaql.core.sql.dialect.OracleDialect;
1011
import java.sql.Connection;
1112
import java.sql.DriverManager;
1213
import java.time.LocalDate;
@@ -24,7 +25,7 @@
2425
import org.junit.Assume;
2526
import org.junit.Test;
2627

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

43+
@Test
44+
public void sqlServerAllocatesAcrossInstancesAndRestart() throws Exception {
45+
verify("MSSQL");
46+
}
47+
48+
@Test
49+
public void dm8AllocatesAcrossInstancesAndRestart() throws Exception {
50+
verify("DM8");
51+
}
52+
53+
@Test
54+
public void oracleAllocatesAcrossInstancesAndRestart() throws Exception {
55+
verify("ORACLE");
56+
}
57+
58+
@Test
59+
public void sqlServerConcurrentSchemaStartupIsIdempotent() throws Exception {
60+
String url = System.getenv("TEAQL_TEST_MSSQL_URL");
61+
String user = System.getenv("TEAQL_TEST_MSSQL_USER");
62+
String password = System.getenv("TEAQL_TEST_MSSQL_PASSWORD");
63+
if (url == null || user == null || password == null) {
64+
if (Boolean.parseBoolean(System.getenv("TEAQL_REQUIRE_LIVE_DB"))) {
65+
Assert.fail("MSSQL Business ID live gate requires URL, USER, and PASSWORD");
66+
}
67+
Assume.assumeTrue("MSSQL live database is not configured", false);
68+
}
69+
Assert.assertTrue("Use a dedicated teaql_live_* database for MSSQL",
70+
url.contains("teaql_live_"));
71+
String table = "bid_schema_" + UUID.randomUUID().toString().substring(0, 8);
72+
CountDownLatch start = new CountDownLatch(1);
73+
ExecutorService workers = Executors.newFixedThreadPool(2);
74+
try {
75+
List<Future<?>> futures = new ArrayList<>();
76+
for (int worker = 0; worker < 2; worker++) {
77+
futures.add(workers.submit(() -> {
78+
try (Connection connection = DriverManager.getConnection(url, user, password)) {
79+
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
80+
BusinessIdRuntimeExampleTest.database(connection), table);
81+
UserContext context = BusinessIdRuntimeExampleTest.context(
82+
BUSINESS_DATE, allocator);
83+
context.putAttribute(SchemaExecutor.class.getName(),
84+
BusinessIdRuntimeExampleTest.noOpSchemaExecutor());
85+
Assert.assertTrue(start.await(15, TimeUnit.SECONDS));
86+
context.ensureSchema();
87+
return null;
88+
}
89+
}));
90+
}
91+
start.countDown();
92+
for (Future<?> future : futures) future.get(30, TimeUnit.SECONDS);
93+
} finally {
94+
workers.shutdownNow();
95+
}
96+
try (Connection connection = DriverManager.getConnection(url, user, password)) {
97+
Assert.assertEquals(4, BusinessIdRuntimeExampleTest.database(connection)
98+
.getTableColumns(table).size());
99+
}
100+
}
101+
42102
private void verify(String dialect) throws Exception {
43103
String prefix = "TEAQL_TEST_" + dialect + "_";
44104
String url = System.getenv(prefix + "URL");
@@ -50,8 +110,16 @@ private void verify(String dialect) throws Exception {
50110
}
51111
Assume.assumeTrue(dialect + " live database is not configured", false);
52112
}
53-
Assert.assertTrue("Use a dedicated teaql_live_* database for " + dialect,
54-
url.contains("teaql_live_"));
113+
if ("DM8".equals(dialect)) {
114+
Assert.assertEquals("Use only an isolated disposable DM8 instance",
115+
"true", System.getenv("TEAQL_TEST_DM8_ISOLATED"));
116+
} else if ("ORACLE".equals(dialect)) {
117+
Assert.assertTrue("Use only a dedicated TEAQL_LIVE_* Oracle user",
118+
user.toUpperCase(java.util.Locale.ROOT).startsWith("TEAQL_LIVE_"));
119+
} else {
120+
Assert.assertTrue("Use a dedicated teaql_live_* database for " + dialect,
121+
url.contains("teaql_live_"));
122+
}
55123

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

63131
try (Connection schemaConnection = DriverManager.getConnection(url, user, password)) {
64-
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
65-
BusinessIdRuntimeExampleTest.database(schemaConnection));
132+
JdbcBusinessIdAllocator allocator = allocator(schemaConnection, dialect);
66133
UserContext context = BusinessIdRuntimeExampleTest.context(BUSINESS_DATE, allocator);
67134
context.putAttribute(SchemaExecutor.class.getName(),
68135
BusinessIdRuntimeExampleTest.noOpSchemaExecutor());
69136
context.ensureSchema();
137+
context.ensureSchema();
70138
}
71139

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

105172
try (Connection restarted = DriverManager.getConnection(url, user, password)) {
106-
JdbcBusinessIdAllocator allocator = new JdbcBusinessIdAllocator(
107-
BusinessIdRuntimeExampleTest.database(restarted));
173+
JdbcBusinessIdAllocator allocator = allocator(restarted, dialect);
108174
Assert.assertEquals(2L * ALLOCATIONS_PER_INSTANCE + 1,
109175
allocator.allocate(plan).sequence());
110176
}
111177
}
178+
179+
private static JdbcBusinessIdAllocator allocator(Connection connection, String dialect) {
180+
if ("ORACLE".equals(dialect)) {
181+
return new JdbcBusinessIdAllocator(
182+
BusinessIdRuntimeExampleTest.database(connection),
183+
JdbcBusinessIdAllocator.DEFAULT_TABLE,
184+
new OracleDialect());
185+
}
186+
return new JdbcBusinessIdAllocator(BusinessIdRuntimeExampleTest.database(connection));
187+
}
112188
}

0 commit comments

Comments
 (0)