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
4 changes: 3 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions .mvn/settings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<settings>
<servers>
<server>
<id>jooq-pro</id>
<username>${env.JOOQ_REPO_USERNAME}</username>
<password>${env.JOOQ_REPO_PASSWORD}</password>
</server>
</servers>
</settings>
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
36 changes: 36 additions & 0 deletions docs/pages/providers/sql.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:

Expand Down
11 changes: 10 additions & 1 deletion nebula-dsl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>oracle-xe</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
Expand All @@ -171,7 +175,12 @@
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<version>23.4.0.24.05</version>
</dependency>
<dependency>
<groupId>org.jooq.pro</groupId>
<artifactId>jooq</artifactId>
<version>3.19.10</version>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,18 @@ class DatabaseExecutor(private val config: DatabaseConfig, loggers: List<LoggerN

val columns = table.data.first().keys
val jooqTable = DSL.table(table.name)
val jooqColumns = columns.map { DSL.field(it) }

val insert = dsl.insertInto(jooqTable)
.columns(columns.map { DSL.field(it) })

// Insert one row at a time rather than as a single multi-row VALUES statement.
// Oracle doesn't support multi-row VALUES and jOOQ's emulation isn't portable here,
// whereas single-row inserts render to valid SQL on every supported database.
var insertedRows = 0
table.data.forEach { row ->
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}" }
}

Expand Down
4 changes: 4 additions & 0 deletions nebula-dsl/src/main/kotlin/com/orbitalhq/nebula/sql/SqlDsl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
})
Loading