diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 64a1b7c..3777ca3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,10 +25,12 @@ jobs:
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-m2
- name: Deploy with Maven
- run: mvn clean deploy
+ run: mvn clean deploy --settings .mvn/settings.xml
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ JOOQ_REPO_USERNAME: ${{ secrets.JOOQ_REPO_USERNAME }}
+ JOOQ_REPO_PASSWORD: ${{ secrets.JOOQ_REPO_PASSWORD }}
- name: Archive build output
uses: actions/upload-artifact@v4
with:
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
new file mode 100644
index 0000000..7f7feb2
--- /dev/null
+++ b/.mvn/settings.xml
@@ -0,0 +1,9 @@
+
+
+
+ jooq-pro
+ ${env.JOOQ_REPO_USERNAME}
+ ${env.JOOQ_REPO_PASSWORD}
+
+
+
diff --git a/CLAUDE.md b/CLAUDE.md
index 4df956e..bb79204 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -55,7 +55,7 @@ npm start # Start production server
- **Project Reactor** for reactive programming
### Infrastructure Components
-The DSL supports: Kafka, HTTP servers, SQL databases (PostgreSQL/MySQL with JOOQ), MongoDB, S3 (via LocalStack), Hazelcast, Avro/Protobuf schemas.
+The DSL supports: Kafka, HTTP servers, SQL databases (PostgreSQL/MySQL/Oracle with JOOQ), MongoDB, S3 (via LocalStack), Hazelcast, Avro/Protobuf schemas.
## Code Patterns
diff --git a/docs/pages/providers/sql.mdx b/docs/pages/providers/sql.mdx
index ea48e64..22f5fb3 100644
--- a/docs/pages/providers/sql.mdx
+++ b/docs/pages/providers/sql.mdx
@@ -3,6 +3,7 @@ There are various SQL providers available, which all share a common DSL.
* `postgres` : Declares an image using the `postgres:13` image by default
* `mysql`: Declares an image using the `mysql:9` image by default
+ * `oracle`: Declares an image using the `gvenzl/oracle-xe:21-slim-faststart` image by default
```kotlin
// use the default image.
@@ -79,6 +80,41 @@ postgres {
}
```
+### Oracle
+Oracle works the same way as the other SQL providers, but a few Oracle-specific conventions apply:
+
+ * Use **uppercase** table and column names. Oracle folds unquoted identifiers to uppercase, and the
+ identifiers in the `data` maps are quoted when the rows are inserted, so they must match. Declaring
+ `CREATE TABLE USERS (...)` and using keys like `"USERNAME"` keeps the two in sync.
+ * Use Oracle-native types: `VARCHAR2` rather than `VARCHAR`/`TEXT`, `NUMBER` rather than `INTEGER`/`DECIMAL`,
+ and `GENERATED ALWAYS AS IDENTITY` rather than `SERIAL`. Oracle (pre-23c) has no `BOOLEAN` type — use
+ `NUMBER(1)` with `0`/`1` values.
+
+```kotlin
+oracle {
+ table(
+ "USERS", """
+ CREATE TABLE USERS (
+ ID VARCHAR2(36) PRIMARY KEY,
+ USERNAME VARCHAR2(100) NOT NULL,
+ IS_ACTIVE NUMBER(1),
+ LOGIN_COUNT NUMBER(10),
+ BALANCE NUMBER(10, 2)
+ )
+ """,
+ data = listOf(
+ mapOf(
+ "ID" to UUID.randomUUID().toString(),
+ "USERNAME" to "john_doe",
+ "IS_ACTIVE" to 1,
+ "LOGIN_COUNT" to 5,
+ "BALANCE" to BigDecimal("100.50")
+ )
+ )
+ )
+}
+```
+
### Returned values
When any database component is declared, the following data is returned:
diff --git a/nebula-dsl/pom.xml b/nebula-dsl/pom.xml
index fbd4762..8377b9f 100644
--- a/nebula-dsl/pom.xml
+++ b/nebula-dsl/pom.xml
@@ -160,6 +160,10 @@
org.testcontainers
mysql
+
+ org.testcontainers
+ oracle-xe
+
org.postgresql
postgresql
@@ -171,7 +175,12 @@
8.0.33
- org.jooq
+ com.oracle.database.jdbc
+ ojdbc11
+ 23.4.0.24.05
+
+
+ org.jooq.pro
jooq
3.19.10
diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/DatabaseExecutor.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/DatabaseExecutor.kt
index 4e401fc..ef88ecc 100644
--- a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/DatabaseExecutor.kt
+++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/DatabaseExecutor.kt
@@ -131,15 +131,18 @@ class DatabaseExecutor(private val config: DatabaseConfig, loggers: List
- insert.values(row.values.map { convertValue(it) })
+ insertedRows += dsl.insertInto(jooqTable)
+ .columns(jooqColumns)
+ .values(columns.map { convertValue(row[it]) })
+ .execute()
}
-
- val insertedRows = insert.execute()
logger.info { "Inserted $insertedRows rows into ${table.name}" }
}
diff --git a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/SqlDsl.kt b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/SqlDsl.kt
index 4fd3cc4..ec8deea 100644
--- a/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/SqlDsl.kt
+++ b/nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/SqlDsl.kt
@@ -8,6 +8,7 @@ import mu.KotlinLogging
import org.jooq.SQLDialect
import org.testcontainers.containers.JdbcDatabaseContainer
import org.testcontainers.containers.MySQLContainer
+import org.testcontainers.containers.OracleContainer
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.utility.DockerImageName
@@ -20,6 +21,9 @@ interface SqlDsl : InfraDsl {
fun mysql(imageName: String = "mysql:9", databaseName: String = "testDb", componentName: ComponentName = "mysql", dsl: DatabaseBuilder.(KLogger) -> Unit): DatabaseExecutor =
database(MySQLContainer(DockerImageName.parse(imageName)), SQLDialect.MYSQL, "mysql", databaseName, componentName, dsl)
+ fun oracle(imageName: String = "gvenzl/oracle-xe:21-slim-faststart", databaseName: String = "testDb", componentName: ComponentName = "oracle", dsl: DatabaseBuilder.(KLogger) -> Unit): DatabaseExecutor =
+ database(OracleContainer(DockerImageName.parse(imageName)), SQLDialect.ORACLE, "oracle", databaseName, componentName, dsl)
+
fun database(container: JdbcDatabaseContainer<*>, dialect: SQLDialect, type: String, databaseName: String, componentName: ComponentName, dsl: DatabaseBuilder.(KLogger) -> Unit): DatabaseExecutor {
val builder = DatabaseBuilder(container, dialect, type, databaseName, componentName)
builder.dsl(logger)
diff --git a/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/sql/SqlExecutorTest.kt b/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/sql/SqlExecutorTest.kt
index cf8d70c..18bc579 100644
--- a/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/sql/SqlExecutorTest.kt
+++ b/nebula-dsl/src/test/kotlin/com/orbitalhq/nebula/sql/SqlExecutorTest.kt
@@ -116,4 +116,92 @@ class SqlExecutorTest : DescribeSpec({
(widget["id"] as Int) shouldBe 1
}
}
+
+ describe("Oracle database") {
+ afterTest {
+ infra.shutDownAll()
+ }
+
+ // Oracle folds unquoted identifiers to uppercase and jOOQ quotes them, so tables and
+ // columns are declared in uppercase here. Oracle types differ from Postgres: no UUID
+ // (stored as VARCHAR2), no BOOLEAN (NUMBER(1)), and identity columns instead of SERIAL.
+ it("should create tables and insert data with various types") {
+ infra = stack {
+ oracle {
+ table(
+ "USERS", """
+ CREATE TABLE USERS (
+ ID VARCHAR2(36) PRIMARY KEY,
+ USERNAME VARCHAR2(100) NOT NULL,
+ IS_ACTIVE NUMBER(1),
+ LOGIN_COUNT NUMBER(10),
+ BALANCE NUMBER(10, 2)
+ )
+ """, data = listOf(
+ mapOf(
+ "ID" to UUID.randomUUID().toString(),
+ "USERNAME" to "john_doe",
+ "IS_ACTIVE" to 1,
+ "LOGIN_COUNT" to 5,
+ "BALANCE" to BigDecimal("100.50")
+ ),
+ mapOf(
+ "ID" to UUID.randomUUID().toString(),
+ "USERNAME" to "jane_smith",
+ "IS_ACTIVE" to 0,
+ "LOGIN_COUNT" to 2,
+ "BALANCE" to BigDecimal("75.25")
+ )
+ )
+ )
+
+ table(
+ "PRODUCTS", """
+ CREATE TABLE PRODUCTS (
+ ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
+ NAME VARCHAR2(100) NOT NULL,
+ DESCRIPTION VARCHAR2(255),
+ PRICE NUMBER(10, 2) NOT NULL
+ )
+ """, data = listOf(
+ mapOf(
+ "NAME" to "Widget",
+ "DESCRIPTION" to "A fantastic widget",
+ "PRICE" to BigDecimal("9.99")
+ ),
+ mapOf(
+ "NAME" to "Gadget",
+ "DESCRIPTION" to "An amazing gadget",
+ "PRICE" to BigDecimal("24.99")
+ )
+ )
+ )
+ }
+ }.start()
+
+ // Set up jOOQ DSL context
+ dsl = infra.database.single().dsl
+
+ // Test USERS table
+ val users = dsl.selectFrom("USERS").fetch()
+ users.size shouldBe 2
+
+ val johnDoe = users.first { it["USERNAME"] == "john_doe" }
+ johnDoe["USERNAME"] shouldBe "john_doe"
+ (johnDoe["IS_ACTIVE"] as Number).toInt() shouldBe 1
+ (johnDoe["LOGIN_COUNT"] as Number).toInt() shouldBe 5
+ (johnDoe["BALANCE"] as BigDecimal).toDouble() shouldBe 100.50
+
+ // Test PRODUCTS table
+ val products = dsl.selectFrom("PRODUCTS").fetch()
+ products.size shouldBe 2
+
+ val widget = products.first { it["NAME"] == "Widget" }
+ widget["DESCRIPTION"] shouldBe "A fantastic widget"
+ (widget["PRICE"] as BigDecimal).toDouble() shouldBe 9.99
+
+ // Test auto-incrementing identity column
+ (widget["ID"] as Number).toInt() shouldBe 1
+ }
+ }
})
\ No newline at end of file