https://github.com/polskianonim/octaviusdatabase
SQL-first data access layer for Kotlin & PostgreSQL. An Anti-ORM with fluent query builders, automatic type mapping (ENUM, COMPOSITE, ARRAY), transaction plans with step dependencies, and polymorphic storage
https://github.com/polskianonim/octaviusdatabase
data-mapping kotlin postgresql
Last synced: 21 days ago
JSON representation
SQL-first data access layer for Kotlin & PostgreSQL. An Anti-ORM with fluent query builders, automatic type mapping (ENUM, COMPOSITE, ARRAY), transaction plans with step dependencies, and polymorphic storage
- Host: GitHub
- URL: https://github.com/polskianonim/octaviusdatabase
- Owner: PolskiAnonim
- License: apache-2.0
- Created: 2026-01-16T20:12:11.000Z (27 days ago)
- Default Branch: main
- Last Pushed: 2026-01-18T17:48:25.000Z (25 days ago)
- Last Synced: 2026-01-18T21:50:33.764Z (25 days ago)
- Topics: data-mapping, kotlin, postgresql
- Language: Kotlin
- Homepage:
- Size: 195 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Octavius Database
**An un-opinionated, SQL-first data access layer for Kotlin & PostgreSQL**
[](https://polskianonim.github.io/OctaviusDatabase/api)
[](https://polskianonim.github.io/OctaviusDatabase/core)
*It's not an ORM. It's a ROM (Relational-Object Mapper) — an Anti-ORM.
---
## Philosophy
| Principle | Description |
|-----------|-------------|
| **Query is King** | Your SQL query dictates the shape of data — not the framework |
| **Object is a Vessel** | A `data class` is simply a type-safe container for query results |
| **Explicitness over Magic** | No lazy-loading, no session management, no dirty checking |
## Features
- **Fluent Query Builders** — SELECT, INSERT, UPDATE, DELETE with a clean API
- **Automatic Type Mapping** — PostgreSQL `COMPOSITE`, `ENUM`, `ARRAY` ↔ Kotlin types
- **Transaction Plans** — Multi-step atomic operations with step dependencies
- **Dynamic Filters** — Safe, composable `WHERE` clauses with `QueryFragment`
- **Dynamic Type System** — Polymorphic storage & ad-hoc object mapping with `dynamic_dto`
## Quick Start
```kotlin
// Define your data class — it maps directly to query results
data class Book(val id: Int, val title: String, val author: String)
// Query with named parameters
val books = dataAccess.select("id", "title", "author")
.from("books")
.where("published_year > :year")
.orderBy("title")
.toListOf("year" to 2020)
```
## Query Builders
```kotlin
// SELECT with pagination
val users = dataAccess.select("id", "name", "email")
.from("users")
.where("active = true")
.orderBy("created_at DESC")
.limit(10)
.offset(20)
.toListOf()
// INSERT with RETURNING
val newId = dataAccess.insertInto("users")
.value("name")
.value("email")
.returning("id")
.toField(mapOf("name" to "John", "email" to "john@example.com"))
// UPDATE with expressions
dataAccess.update("products")
.setExpression("stock", "stock - 1")
.where("id = :id")
.execute("id" to productId)
// DELETE
dataAccess.deleteFrom("sessions")
.where("expires_at < NOW()")
.execute()
```
## Safe Dynamic Filters
Build complex `WHERE` clauses without SQL injection risks:
```kotlin
fun buildFilters(name: String?, minPrice: Int?, category: Category?): QueryFragment {
val fragments = mutableListOf()
name?.let { fragments += QueryFragment("name ILIKE :name", mapOf("name" to "%$it%")) }
minPrice?.let { fragments += QueryFragment("price >= :minPrice", mapOf("minPrice" to it)) }
category?.let { fragments += QueryFragment("category = :cat", mapOf("cat" to it)) }
return fragments.join(" AND ")
}
val filter = buildFilters(name = "Pro", minPrice = 100, category = null)
val products = dataAccess.select("*")
.from("products")
.where(filter.sql)
.toListOf(filter.params)
```
## Transaction Plans
Execute multi-step operations atomically with dependencies between steps:
```kotlin
val plan = TransactionPlan()
// Step 1: Create order, get handle to future ID
val orderIdHandle = plan.add(
dataAccess.insertInto("orders")
.values(listOf("user_id", "total"))
.returning("id")
.asStep()
.toField(mapOf("user_id" to userId, "total" to total))
)
// Step 2: Create order items using the handle
for (item in cartItems) {
val orderItem: Map = mapOf(
"order_id" to orderIdHandle.field(), // Reference future value
"product_id" to item.productId,
"quantity" to item.quantity
)
plan.add(
dataAccess.insertInto("order_items")
.values(orderItem)
.asStep()
.execute(orderItem)
)
}
// Execute all steps in single transaction
dataAccess.executeTransactionPlan(plan)
```
## Type Mapping
Automatic conversion between PostgreSQL and Kotlin types.
### Standard Types
| PostgreSQL | Kotlin | Notes |
|---------------------------|-----------------|------------------------------|
| `int2`, `smallserial` | `Short` | |
| `int4`, `serial` | `Int` | |
| `int8`, `bigserial` | `Long` | |
| `float4` | `Float` | |
| `float8` | `Double` | |
| `numeric` | `BigDecimal` | |
| `text`, `varchar`, `char` | `String` | |
| `bool` | `Boolean` | |
| `uuid` | `UUID` | `java.util.UUID` |
| `bytea` | `ByteArray` | |
| `json`, `jsonb` | `JsonElement` | `kotlinx.serialization.json` |
| `date` | `LocalDate` | `kotlinx.datetime` |
| `time` | `LocalTime` | `kotlinx.datetime` |
| `timetz` | `OffsetTime` | `org.octavius.data` |
| `timestamp` | `LocalDateTime` | `kotlinx.datetime` |
| `timestamptz` | `Instant` | `kotlin.time` |
| `interval` | `Duration` | `kotlin.time` |
Arrays of all standard types are supported and map to `List`.
### Custom Types
```kotlin
// PostgreSQL COMPOSITE TYPE → Kotlin data class
@PgComposite
data class Address(val street: String, val city: String, val zipCode: String)
// PostgreSQL ENUM → Kotlin enum
@PgEnum
enum class OrderStatus { Pending, Processing, Shipped, Delivered }
// Works seamlessly in queries
data class Order(val id: Int, val status: OrderStatus, val shippingAddress: Address)
val orders = dataAccess.select("id", "status", "shipping_address")
.from("orders")
.toListOf() // Types converted automatically
```
## Dynamic Type System
Octavius uses `dynamic_dto` — a PostgreSQL composite type combining a type discriminator with JSONB payload — to bridge static SQL and Kotlin's type system.
```sql
-- Created automatically by Octavius
CREATE TYPE dynamic_dto AS (
type_name TEXT,
data_payload JSONB
);
-- Helper function for constructing values
CREATE FUNCTION dynamic_dto(p_type_name TEXT, p_data JSONB)
RETURNS dynamic_dto AS $$
BEGIN
RETURN ROW(p_type_name, p_data)::dynamic_dto;
END;
$$ LANGUAGE plpgsql;
```
### 1. Polymorphic Storage
Store different types in a single column or array. The framework deserializes each element to its correct Kotlin class based on `type_name`.
```kotlin
@DynamicallyMappable(typeName = "text_note")
@Serializable
data class TextNote(val content: String)
@DynamicallyMappable(typeName = "image_note")
@Serializable
data class ImageNote(val url: String, val caption: String?)
// Database: CREATE TABLE notebooks (id INT, notes dynamic_dto[]);
val notes: List = listOf(
TextNote("Hello world"),
ImageNote("https://example.com/img.png", "A photo")
)
dataAccess.insertInto("notebooks")
.values(listOf("notes"))
.execute("notes" to notes)
// Read back — each element deserialized to its correct type
val notebook = dataAccess.select("notes")
.from("notebooks")
.where("id = 1")
.toField>() // Returns [TextNote(...), ImageNote(...)]
```
### 2. Ad-hoc Object Mapping
Construct Kotlin objects directly in SQL using `jsonb_build_object` — no need to define PostgreSQL COMPOSITE types. Perfect for JOINs and projections where you want nested results without schema changes.
```kotlin
@DynamicallyMappable(typeName = "user_profile")
@Serializable
data class UserProfile(val role: String, val permissions: List)
data class UserWithProfile(val id: Int, val name: String, val profile: UserProfile)
val users = dataAccess.rawQuery("""
SELECT
u.id,
u.name,
dynamic_dto(
'user_profile',
jsonb_build_object(
'role', p.role,
'permissions', p.permissions
)
) AS profile
FROM users u
JOIN profiles p ON p.user_id = u.id
""").toListOf()
```
> **Why use this?** Usually, to get a user with their profile in one query, you'd fetch flat columns (`user_id`, `user_name`, `profile_role`...) and manually map them, or create a database VIEW. With ad-hoc mapping, you construct the nested structure directly in SQL. The database does the packaging, Octavius does the unpacking — zero boilerplate.
## Configuration
```kotlin
// From properties file
val dataAccess = OctaviusDatabase.fromConfig(
DatabaseConfig.loadFromFile("database.properties")
)
// Direct configuration
val dataAccess = OctaviusDatabase.fromConfig(
DatabaseConfig(
dbUrl = "jdbc:postgresql://localhost:5432/mydb",
dbUsername = "user",
dbPassword = "pass",
dbSchemas = listOf("public"),
packagesToScan = listOf("com.myapp.domain")
)
)
// From existing DataSource
val dataAccess = OctaviusDatabase.fromDataSource(existingDataSource, ...)
```
## Database Migrations
Octavius Database integrates [Flyway](https://flywaydb.org/) for schema migrations. Migration files are loaded from `src/main/resources/db/migration/` and applied automatically on startup.
To disable automatic migrations, set `flywayEnabled = false` in `DatabaseConfig`.
For existing databases, set `flywayBaselineVersion` to skip migrations up to that version — Flyway will treat the current schema as the baseline and only apply newer migrations.
## Architecture
| Module | Platform | Description |
|--------|---------------|--------------------------------------------------------------|
| `api` | Multiplatform | Public API, interfaces; annotations with no JVM dependencies |
| `core` | JVM | Implementation using Spring JDBC & HikariCP |