{"id":34741798,"url":"https://github.com/arkanovicz/skorm","last_synced_at":"2026-04-19T21:06:04.263Z","repository":{"id":304471106,"uuid":"1018883282","full_name":"arkanovicz/skorm","owner":"arkanovicz","description":"Simple Kotlin Object Relational Mapping","archived":false,"fork":false,"pushed_at":"2026-01-24T11:07:45.000Z","size":1003,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-24T21:58:59.119Z","etag":null,"topics":["data","database","model","orm","sql"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/arkanovicz.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-13T09:01:03.000Z","updated_at":"2026-01-24T11:07:48.000Z","dependencies_parsed_at":"2025-09-12T15:48:06.246Z","dependency_job_id":null,"html_url":"https://github.com/arkanovicz/skorm","commit_stats":null,"previous_names":["arkanovicz/skorm"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/arkanovicz/skorm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkanovicz%2Fskorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkanovicz%2Fskorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkanovicz%2Fskorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkanovicz%2Fskorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/arkanovicz","download_url":"https://codeload.github.com/arkanovicz/skorm/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/arkanovicz%2Fskorm/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32022569,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T20:23:30.271Z","status":"online","status_checked_at":"2026-04-19T02:00:07.110Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data","database","model","orm","sql"],"created_at":"2025-12-25T04:16:23.891Z","updated_at":"2026-04-19T21:06:04.257Z","avatar_url":"https://github.com/arkanovicz.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# skorm\n\n*Simple Kotlin Object Relational Mapping*\n\nThe nicest Kotlin multiplatform ORM around. Fully multiplatform. Coroutines-enabled.\n\n## Concepts\n\n*Five* main concepts:\n\n+ **Database** - Root container for your data model\n+ **Schema** - Logical grouping of entities (see [Configuration](#configuration))\n+ **Entity** - Corresponds to a table or view (defined in [kddl syntax](#kddl-syntax))\n+ **Instance** - A single row in a table with CRUD operations\n+ **Attribute** - Custom queries and mutations (see [ksql syntax](#ksql-syntax)), with five variants:\n\n     + ScalarAttribute, returning Any?\n     + RowAttribute, returning Instance?\n     + RowSetAttribute, returning Sequence\u003cInstance\u003e\n     + MutationAttribute, returning Long (either the number of modified rows, or the generated serial value)\n     + TransactionAttribute, returning List\u003cLong\u003e (number of modified rows for each comprised mutation statement)\n\n*Four* main methods in the lifecycle of database objects instances (along with transaction handling):\n\n+ `Instance.insert()`\n+ `Entity.fetch(primaryKey)`\n+ `Instance.update()`\n+ `Instance.delete()`\n\n*Three* main customization points (see [Configuration](#configuration)):\n\n+ identifiers mapping (snake to camel, prefix/suffix removal, lowercase ...)\n+ fields filtering (hide secret field, mark field as read-only, ...)\n+ values filtering (transform timestamps, etc.)\n\n*Two* model definition formats: kddl for \u003cabbr title=\"Data Definition Language\"\u003eDDL\u003c/abbr\u003e (schema structure), ksql for \u003cabbr title=\"Data Manipulation Language\"\u003eDML\u003c/abbr\u003e (custom queries and mutations). And *two* concrete database connectors, one for \u003cabbr title=\"Java DataBase Connectivity\"\u003eJDBC\u003c/abbr\u003e and one for a service \u003cabbr title=\"Application Programmable Interface\"\u003eAPI\u003c/abbr\u003e. More to come, hopefully.\n\n*One* goal: maximum simplicity without conceeding anyting to extensibility.\n\n*Zero* annotation. Zero \u003cabbr title=\"Set Query Language\"\u003eSQL\u003c/abbr\u003e code fragmentation.\n\n## Quick Start\n\nLet's create a simple todo list application.\n\n### 1. Define your schema (`todo.kddl`)\n\n```\ndatabase todo_app {\n  schema todos {\n    table task {\n      title string(200)\n      completed boolean = false\n    }\n  }\n}\n```\n\n### 2. Add custom queries and mutations (`todo.ksql`, optional)\n\n```\ndatabase todo_app {\n  schema todos {\n    attr pendingCount: Int =\n      SELECT count(*) FROM task WHERE completed = false;\n\n    mut Task.toggle =\n      UPDATE task SET completed = NOT completed WHERE task_id = {task_id};\n  }\n}\n```\n\n### 3. Configure the Gradle plugin (`build.gradle.kts`)\n\n```kotlin\nplugins {\n    kotlin(\"multiplatform\") version \"2.3.0\"\n    id(\"com.republicate.skorm\") version \"0.12\"\n}\n\nskorm {\n    structure.set(File(\"src/commonMain/model/todo.kddl\"))\n    runtimeModel.set(File(\"src/commonMain/model/todo.ksql\"))  // optional\n    destPackage.set(\"com.example.todo\")\n}\n\ndependencies {\n    // For JVM backend\n    implementation(\"com.republicate.skorm:skorm-core\")\n    implementation(\"com.republicate.skorm:skorm-jdbc\")\n\n    // Or for JS/WASM client\n    implementation(\"com.republicate.skorm:skorm-api-client\")\n}\n```\n\n### 4. Use the generated code\n\n```kotlin\n// Initialize database (JVM)\nval database = TodoAppDatabase(CoreProcessor(JdbcConnector()))\ndatabase.configure(mapOf(\n    \"jdbc\" to mapOf(\n        \"url\" to \"jdbc:h2:mem:todo\",\n        \"user\" to \"sa\"\n    )\n))\ndatabase.initialize()\n\n// Create a task\nval task = Task().apply {\n    title = \"Learn skorm\"\n    completed = false\n    insert()\n}\n\n// Fetch and update\nval fetched = Task.fetch(task.taskId)\nfetched?.let {\n    it.completed = true\n    it.update()\n}\n\n// Browse all tasks\nTask.browse().forEach { println(it.title) }\n```\n\nThat's it! The skorm Gradle plugin generates all the necessary Kotlin classes from your `.kddl` file.\n\n### Dynamic Usage (Without Code Generation)\n\nYou can also use skorm without the code generator, accessing entities dynamically:\n\n```kotlin\n// Navigate the model\nval schema = database.schema(\"todos\")\nval taskEntity = schema.entity(\"task\")\n\n// CRUD operations\nval task = taskEntity.new()\ntask[\"title\"] = \"Learn skorm\"\ntask[\"completed\"] = false\ntask.insert()\n\nval fetched = taskEntity.fetch(task[\"taskId\"])\nfetched?.let {\n    it[\"completed\"] = true\n    it.update()\n}\n\ntaskEntity.browse().forEach { println(it[\"title\"]) }\n```\n\nThis is useful for generic tools, migrations, or when the schema is only known at runtime.\n\n## Reference\n\n### Configuration\n\n```\nDatabase *—— Schema *—— Entity *—— Instance\n```\n\n*Five* main verbs to interact with attributes:\n\n+ `eval(name, params...)` - returns a scalar value\n+ `retrieve(name, params...)` - returns a single row (plus `Entity.fetch(params...)` to get an instance by ID)\n+ `query(name, params...)` - returns a rowset\n+ `perform(name, params...)` - for atomic mutations\n+ `attempt(name, params...)` - for transactions\n\n#### Identifiers Mapping\n\nSkorm automatically maps between database identifiers and Kotlin property names:\n\n```kotlin\ndatabase.configure(mapOf(\n    \"core\" to mapOf(\n        \"mapping\" to mapOf(\n            \"read\" to listOf(\"snakeToCamel\"),   // DB columns: user_name → Kotlin: userName\n            \"write\" to listOf(\"camelToSnake\")   // Kotlin: userName → DB columns: user_name\n        )\n    )\n))\n```\n\nBuilt-in mappers:\n- `snakeToCamel` / `camelToSnake`\n- `lowercase` / `uppercase`\n- Custom mappers can be registered\n\n#### Values Filtering\n\nTransform values during read/write operations:\n\n```kotlin\ndatabase.configure(mapOf(\n    \"core\" to mapOf(\n        \"filter\" to mapOf(\n            \"read\" to mapOf(\n                \"timestamp\" to \"epochToLocalDateTime\"\n            ),\n            \"write\" to mapOf(\n                \"timestamp\" to \"localDateTimeToEpoch\"\n            )\n        )\n    )\n))\n```\n\n#### Connector Configuration\n\n**JDBC Connector:**\n```kotlin\ndatabase.configure(mapOf(\n    \"jdbc\" to mapOf(\n        \"url\" to \"jdbc:postgresql://localhost:5432/mydb\",\n        \"user\" to \"dbuser\",\n        \"password\" to \"secret\"\n    )\n))\n```\n\n**API Client (for JS/WASM):**\n```kotlin\nval database = TodoAppDatabase(ApiClient(\"https://api.example.com\"))\ndatabase.initialize()\n// Same code works on browser/Node.js!\n```\n\n### kddl Syntax\n\nThe kddl (Kotlin Data Definition Language) format defines your database structure. It generates both SQL DDL scripts and Kotlin classes.\n\n#### Basic Structure\n\n```\ndatabase \u003cname\u003e {\n  schema \u003cname\u003e {\n    table \u003cname\u003e {\n      \u003cfield_name\u003e \u003ctype\u003e [modifiers]\n    }\n  }\n}\n```\n\n#### Field Types\n\n- **Strings**: `string`, `string(length)`, `text`\n- **Numbers**: `int`, `long`, `float`, `double`, `decimal(p,s)`\n- **Booleans**: `boolean`\n- **Dates**: `date`, `time`, `datetime`, `timestamp`\n- **Special**: `serial` (auto-increment), `uuid`, `json`\n- **Enums**: `enum('value1', 'value2', ...)`\n\n#### Field Modifiers\n\n- `?` - nullable field\n- `!` - unique constraint\n- `= \u003cvalue\u003e` - default value\n\nExample:\n```\ntable user {\n  !email string(255)              // unique, non-null\n  name string(100)?               // nullable\n  age int = 18                    // default value\n  status enum('active', 'inactive') = 'active'\n  created_at timestamp = now()\n}\n```\n\n#### Primary Keys\n\nPrimary keys are auto-generated as `\u003ctable_name\u003e_id` with type `serial`:\n\n```\ntable book {\n  title string\n}\n// Generates: book_id serial PRIMARY KEY\n```\n\n#### Relationships\n\n- `-\u003e` - many-to-one (foreign key)\n- `*-*` - many-to-many (creates join table)\n- `--\u003e` - one-to-many (reverse navigation)\n\nExamples:\n```\nauthor *-* book           // many-to-many: creates author_book join table\nborrowing -\u003e book, user   // borrowing has book_id and user_id foreign keys\n```\n\nThe kddl compiler generates:\n1. SQL DDL scripts for database creation\n2. Kotlin entity classes with typed properties\n3. Navigation methods for relationships (e.g., `book.author()`, `author.books()`)\n\nFor complete kddl documentation, see the [kddl project](https://github.com/arkanovicz/kddl).\n\n### ksql Syntax\n\nBeyond the basic CRUD operations, skorm allows you to define custom queries and mutations using the `ksql` format. These definitions generate type-safe Kotlin objects and extension functions.\n\n#### Declaration Syntax\n\n```\nattr [Entity.]name: ReturnType = SQL\nmut [Entity.]name[(params)] = SQL\n```\n\n- `attr` - defines a query attribute (SELECT)\n- `mut` - defines a mutation attribute (INSERT/UPDATE/DELETE)\n- Schema-level: `attr name` - function on schema\n- Entity-level: `attr Entity.name` - function on entity instance\n\n#### Return Types\n\n| Syntax | Description | Example |\n|--------|-------------|---------|\n| `Type` | Non-nullable scalar | `Int`, `String`, `LocalDate` |\n| `Type?` | Nullable scalar | `Int?`, `String?` |\n| `(Entity, field: Type, ...)` | Composite object extending entity | `(Dude, borrowing_date: LocalDateTime)` |\n| `(Entity, field: Type, ...)?` | Nullable composite | `(Dude, borrowing_date: LocalDateTime)?` |\n| `(field: Type, ...)` | Anonymous object | `(count: Int, total: Double)` |\n| `(field: Type, ...)?` | Nullable anonymous | `(count: Int, total: Double)?` |\n| `(...)*` | Sequence of objects | `(name: String, count: Int)*` |\n\nSupported scalar types: `Int`, `Long`, `String`, `Boolean`, `Double`, `Float`, `LocalDate`, `LocalDateTime`, `LocalTime`\n\n#### Parameters\n\nSQL parameters are enclosed in curly braces:\n\n```kotlin\nattr getUserByEmail: User? =\n  SELECT * FROM users WHERE email = {email};\n```\n\nFor entity-level attributes, all entity fields are automatically available:\n\n```kotlin\nattr Book.currentBorrower: Dude? =\n  SELECT dude.* FROM borrowing\n    JOIN dude USING (dude_id)\n    WHERE book_id = {book_id}  // book_id from Book instance\n    AND returned_date IS NULL;\n```\n\nMutation parameters are declared in the signature:\n\n```kotlin\nmut Book.lend(dude_id: Long) =\n  INSERT INTO borrowing (book_id, dude_id, borrowed_date)\n    VALUES ({book_id}, {dude_id}, now());\n```\n\n#### Examples\n\n**Schema-level scalar:**\n```kotlin\nattr booksCount: Int =\n  SELECT count(*) FROM book;\n\n// Generates: suspend fun BookshelfSchema.booksCount(): Int\n```\n\n**Entity-level composite object:**\n```kotlin\nattr Book.currentBorrower: (Dude, borrowing_date: LocalDateTime)? =\n  SELECT dude.*, borrowing_date FROM bookshelf.borrowing\n    JOIN dude USING (dude_id)\n    WHERE book_id = {book_id}\n    AND restitution_date IS NULL;\n\n// Generates:\n// class CurrentBorrower: Dude { val borrowingDate: LocalDateTime }\n// suspend fun Book.currentBorrower(): CurrentBorrower?\n```\n\n**Mutation with parameters:**\n```kotlin\nmut Book.lend(dude_id: Long) =\n  INSERT INTO borrowing (dude_id, book_id, borrowing_date)\n    VALUES ({dude_id}, {book_id}, now());\n\n// Generates: suspend fun Book.lend(dude_id: Long): Long\n```\n\n**Anonymous object:**\n```kotlin\nattr Book.stats: (title_length: Int, borrowed: Int) =\n  SELECT\n    CHARACTER_LENGTH(title) title_length,\n    (SELECT COUNT(*) FROM borrowing WHERE book_id = {book_id}) borrowed\n  FROM book\n  WHERE book_id = {book_id};\n\n// Generates:\n// class Stats { val titleLength: Int; val borrowed: Int }\n// suspend fun Book.stats(): Stats\n```\n\n**Sequence (rowset):**\n```kotlin\nattr topBorrowers: (dude_id: Long, borrow_count: Int)* =\n  SELECT dude_id, COUNT(*) borrow_count\n  FROM borrowing\n  GROUP BY dude_id\n  ORDER BY borrow_count DESC\n  LIMIT 10;\n\n// Generates:\n// class TopBorrowers { val dudeId: Long; val borrowCount: Int }\n// suspend fun BookshelfSchema.topBorrowers(): Sequence\u003cTopBorrowers\u003e\n```\n\nAll generated functions are coroutine-based (`suspend`) and type-safe, providing compile-time checking of parameters and return types.\n\n## Complete Example\n\nLet's build a complete bookshelf application that tracks books and borrowings, demonstrating both JVM backend and JS frontend using the same business logic.\n\n### Database Schema (`bookshelf.kddl`)\n\n```\ndatabase example {\n  schema bookshelf {\n\n    table dude { name string }\n\n    table author { name string }\n\n    table book {\n      title string\n      genre enum('essay', 'literature', 'art')\n      language string(2)\n    }\n\n    table borrowing {\n      borrowing_date date = now()\n      restitution_date date?\n    }\n\n    author *-* book\n    book --\u003e author\n    borrowing -\u003e book, dude\n  }\n}\n```\n\nThis generates:\n- SQL creation script\n- Entity classes: `Dude`, `Author`, `Book`, `Borrowing`\n- Relationship methods: `book.author()`, `author.books()`, `book.borrowings()`, etc.\n\n### Custom Queries (`bookshelf.ksql`)\n\n```kotlin\ndatabase example {\n  schema bookshelf {\n\n    // Schema-level scalar attribute\n    attr booksCount: Int =\n      SELECT count(*) FROM book;\n\n    // Entity-level attribute returning a composite object\n    attr Book.currentBorrower: (Dude, borrowing_date: LocalDateTime)? =\n      SELECT dude.*, borrowing_date FROM bookshelf.borrowing\n        JOIN dude USING (dude_id)\n        WHERE book_id = {book_id}\n        AND restitution_date IS NULL;\n\n    // Mutation with parameters\n    mut Book.lend(dude_id: Long) =\n      INSERT INTO borrowing (dude_id, book_id, borrowing_date)\n        VALUES ({dude_id}, {book_id}, now());\n\n    // Mutation without parameters\n    mut Book.restitute =\n      UPDATE borrowing SET restitution_date = NOW()\n        WHERE book_id = {book_id} AND restitution_date IS NULL;\n\n    // Attribute returning an anonymous object\n    attr Book.stats: (title_length: Int, borrowed: Int) =\n      SELECT\n        CHARACTER_LENGTH(title) title_length,\n        (SELECT COUNT(*) FROM borrowing WHERE book_id = {book_id}) borrowed\n      FROM book\n      WHERE book_id = {book_id};\n\n    // Attribute returning a sequence\n    attr topBorrowers: (dude_id: Long, borrowed: Int)* =\n      SELECT dude_id,\n        COUNT(restitution_date) borrowed\n      FROM borrowing\n      GROUP BY dude_id\n      ORDER BY borrowed DESC;\n  }\n}\n```\n\n### JVM Backend (Server.kt)\n\n```kotlin\nimport com.republicate.skorm.core.CoreProcessor\nimport com.republicate.skorm.jdbc.JdbcConnector\n\nval database = ExampleDatabase(CoreProcessor(JdbcConnector()))\n\nfun Application.configureDatabase() {\n    // Configure from application.conf\n    database.configure(environment.config.config(\"skorm\").toMap())\n    database.initialize()\n    database.initJoins()\n    database.initRuntimeModel()\n\n    // Create test data\n    runBlocking {\n        val author = Author().apply {\n            name = \"Isaac Asimov\"\n            insert()\n        }\n\n        val book = Book().apply {\n            title = \"Foundation\"\n            authorId = author.authorId\n            insert()\n        }\n\n        Dude().apply {\n            name = \"Alice\"\n            insert()\n        }\n    }\n}\n\nfun Application.configureRouting() {\n    routing {\n        get(\"/\") {\n            call.respondHtml {\n                body {\n                    h1 { +\"My Bookshelf\" }\n                    ul {\n                        runBlocking {\n                            for (book in Book) {\n                                val author = book.author()\n                                val borrower = book.currentBorrower()\n                                li {\n                                    +book.title\n                                    i { +\" by ${author.name}\" }\n\n                                    if (borrower != null) {\n                                        +\" - borrowed by ${borrower.name}\"\n                                    } else {\n                                        +\" - available\"\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        // REST API endpoint\n        route(\"/api/example\") {\n            rest(ExampleDatabase.bookshelf)\n        }\n    }\n}\n```\n\n### JS Frontend (Client.kt)\n\n```kotlin\nimport com.republicate.skorm.ApiClient\nimport kotlinx.browser.window\n\n// Same database definition, different processor!\nval database = ExampleDatabase(ApiClient(\"${window.location.origin}/api\"))\n\nfun main() {\n    window.onload = {\n        database.initialize()\n        database.initJoins()\n        database.initRuntimeModel()\n\n        // Same code as backend!\n        document.querySelector(\".lend-form\")?.addEventListener(\"submit\") { event -\u003e\n            event.preventDefault()\n            GlobalScope.launch {\n                val bookId = form.getAttribute(\"data-book_id\")\n                val book = Book.fetch(bookId) ?: error(\"Book not found\")\n                val dudeId = selectElement.value.toLong()\n\n                book.lend(dudeId)  // Calls REST API transparently\n                document.location?.reload()\n            }\n        }\n    }\n}\n```\n\n### The Magic\n\nThe same business logic code works on both JVM and JS:\n\n```kotlin\n// This code is identical on server and client:\nval book = Book.fetch(bookId)\nbook?.let {\n    val borrower = it.currentBorrower()\n    it.lend(dudeId)\n    it.restitute()\n}\n```\n\n**On JVM:** `CoreProcessor` → JDBC → Database\n**On JS:** `ApiClient` → HTTP → REST API → `CoreProcessor` → JDBC → Database\n\nThe `Processor` abstraction makes your code platform-agnostic!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farkanovicz%2Fskorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Farkanovicz%2Fskorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Farkanovicz%2Fskorm/lists"}