{"id":17792908,"url":"https://github.com/exoquery/terpal-sql","last_synced_at":"2025-03-16T17:36:35.889Z","repository":{"id":246977561,"uuid":"824744021","full_name":"ExoQuery/terpal-sql","owner":"ExoQuery","description":"Brainlessly simple and safe SQL Database Access for Kotlin","archived":false,"fork":false,"pushed_at":"2024-12-15T07:18:13.000Z","size":3382,"stargazers_count":39,"open_issues_count":4,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-16T04:23:58.358Z","etag":null,"topics":["jdbc","kotlin","kotlin-multiplatform","postgresql","sql","sqlite"],"latest_commit_sha":null,"homepage":"http://terpal.io/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExoQuery.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","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}},"created_at":"2024-07-05T20:50:29.000Z","updated_at":"2025-03-05T13:45:48.000Z","dependencies_parsed_at":"2024-07-06T02:27:04.757Z","dependency_job_id":"6175503f-dc7f-4491-ada1-494beb39111f","html_url":"https://github.com/ExoQuery/terpal-sql","commit_stats":null,"previous_names":["deusaquilus/terpal-sql"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2Fterpal-sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2Fterpal-sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2Fterpal-sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2Fterpal-sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExoQuery","download_url":"https://codeload.github.com/ExoQuery/terpal-sql/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243909033,"owners_count":20367519,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["jdbc","kotlin","kotlin-multiplatform","postgresql","sql","sqlite"],"created_at":"2024-10-27T11:02:06.958Z","updated_at":"2025-03-16T17:36:35.880Z","avatar_url":"https://github.com/ExoQuery.png","language":"Kotlin","readme":"\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/user-attachments/assets/f9a9ac27-47e2-429e-997a-beb72a1bb81e\" width=70% height=70% \u003e\n\u003c/p\u003e\n\n# terpal-sql\n\nTerpal is a Kotlin Multiplatform library that allows you to write SQL queries using interpolated strings\nin an SQL-injection-safe way. Inspired by Scala libraries such as Doobie and Zio-Jdbc, Terpal\nsuspends the splicing of \"$dollar $sign $variables\" in `SQL(\"...\")` strings and preserves them in a separate data structure until they can be\nsafely injected into an SQL statement.\n\n```kotlin\nval ds: DataSource = PGSimpleDataSource(...)\nval ctx = TerpalDriver.Postgres(ds)\n\n// Let's try a pesky SQL injection attack:\nval name = \"'Joe'; DROP TABLE Person\"\n\n// Boom! The `Person` table will be dropped:\nds.connection.use { conn -\u003e\n  conn.createStatement().execute(\"SELECT * FROM Person WHERE name = $name\")\n}\n\n// No problem! The `Person` table will be safe:\nSql(\"SELECT * FROM Person WHERE name = $name\").queryOf\u003cPerson\u003e().runOn(ctx)\n// Behind the scenes:\n// val query = \"SELECT * FROM Person WHERE name = ?\", params = listOf(Param(name))\n// conn.prepareStatement(query).use { stmt -\u003e stmt.setString(1, name); stmt.executeQuery() }\n```\nFor a deep dive on how this works, have Have a look at the [Terpal Compiler Plugin](https://github.com/ExoQuery/Terpal).\n\nIn addition Terpal allows you to decode the results of SQL queries into Kotlin data classes using\nthe kotlinx-serialization library.\n\n```kotlin\n// Annotate a class using the kotlinx-serialization library\n@Serializable\ndata class Person(val id: Int, val firstName: String, val lastName: String)\n\n// Declare a Terpal context\nval ctx = TerpalDriver.Postgres.fromConfig(\"mydb\")\n\n// Run a Query\nval person: List\u003cPerson\u003e = Sql(\"SELECT id, firstName, lastName FROM person WHERE id = $id\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\nTerpal SQL:\n* Uses no reflection!\n* Uses no code-generation!\n* Allows $dollar_sign_variables to be used safely in SQL queries!\n* Does not require queries to be written in a separate file.\n* Is built on top of kotlinx-serialization.\n* Works idiomatically with Kotlin coroutines suspended functions.\n\n# Getting Started\n\nCurrently Terpal is supported \n* **On the JVM** using JDBC with: PostgreSQL, MySQL, SQL Server, Oracle, SQLite, and H2. \n* **On Android, iOS, OSX, Linux and Windows** with SQLite\n\nFistly, be sure that you have the following repositories defined:\n```kotlin\nrepositories {\n  gradlePluginPortal()\n  mavenCentral()\n  mavenLocal()\n}\n```\n\n\n## Using JDBC\nWhen using JDBC, add the following to your `build.gradle.kts` file:\n\n```kotlin\nplugins {\n    kotlin(\"jvm\") version \"2.1.0\" // Currently the plugin is only available for Kotlin-JVM\n    id(\"io.exoquery.terpal-plugin\") version \"2.1.0-2.0.0.PL\"\n    kotlin(\"plugin.serialization\") version \"2.1.0\"\n}\n\ndependencies {\n    api(\"io.exoquery:terpal-sql-jdbc:2.0.0.PL-1.2.0\")\n    api(\"org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2\")\n    api(\"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2\")\n    // Your databse driver for example postgres:\n    implementation(\"org.postgresql:postgresql:42.7.0\")\n}\n```\n\nA Terpal context is equivalent to a SQLite driver. It is the object that manages the connection to the database.\n\n#### When using JDBC:\n```kotlin\n// You either construct a context from a JDBC DataSource:\nval ctx = TerpalDriver.Postgres(dataSource)\n\n// Or you can use the fromConfig helper to read from a configuration file\nval ctx = TerpalDriver.Postgres.fromConfig(\"myPostgresDB\")\n\n// ====== application.conf ======\n// myPostgresDB {\n//   dataSourceClassName=org.postgresql.ds.PGSimpleDataSource\n//   dataSource.user=postgres\n//   dataSource.password=mysecretpassword\n//   dataSource.portNumber=35433\n//   dataSource.serverName=localhost \n// }\n```\nHave a look at the Terpal-SQL [Sample Project](https://github.com/ExoQuery/terpal-sql-sample) for more details.\n\n## Using Android\n\nFor Android development, add the following to your `build.gradle.kts` file:\n\n```kotlin\nplugins {\n    kotlin(\"android\") version \"2.1.0\"\n    id(\"io.exoquery.terpal-plugin\") version \"2.1.0-2.0.0.PL\"\n    kotlin(\"plugin.serialization\") version \"2.1.0\"\n}\n\ndependencies {\n    api(\"io.exoquery:terpal-sql-android:2.0.0.PL-1.2.0\")\n    api(\"org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2\")\n    api(\"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2\")\n    implementation(\"androidx.sqlite:sqlite-framework:2.4.0\")\n}\n```\n\n\u003e #### Duplicate class issue\n\u003e When using the `terpal-sql-android` dependency with `terpal-sql-core` in commonMain, be sure\n\u003e to add and exclusion for `jb-annotations-kmp` otherwise symbol conflicts will occur e.g.\n\u003e ```\n\u003e Duplicate class org.intellij.lang.annotations.Flow found in modules annotations-26.0.1.jar -\u003e annotations-26.0.1 (org.jetbrains:annotations:26.0.1) and jb-annotations-kmp-jvm-24.1.0+apple.jar -\u003e jb-annotations-kmp-jvm-24.1.0+apple (com.sschr15.annotations:jb-annotations-kmp-jvm:24.1.0+apple)\n\u003e Duplicate class org.intellij.lang.annotations.Identifier found in modules annotations-26.0.1.jar -\u003e annotations-26.0.1 (org.jetbrains:annotations:26.0.1) and jb-annotations-kmp-jvm-24.1.0+apple.jar -\u003e jb-annotations-kmp-jvm-24.1.0+apple (com.sschr15.annotations:jb-annotations-kmp-jvm:24.1.0+apple)\n\u003e ...\n\u003e ```\n\u003e To fix it do this:\n\u003e ```kotlin\n\u003e commonMain.dependencies {\n\u003e   implementation(\"io.exoquery:terpal-sql-core:2.0.0.PL-1.2.0\") {\n\u003e     exclude(\"com.sschr15.annotations\",\"jb-annotations-kmp\")\n\u003e   }\n\u003e }\n\u003e ```\n\nThen create the `TerpalAndroidDriver` using one of the following constructors.\n\n### Construct from ApplicationContext\nUse the `TerpalAndroidDriver.fromApplicationDriver` method to create a terpal context from an Android Application Driver\n```kotlin\nval ctx = \n  TerpalAndroidDriver.fromApplicationContext(\n    databaseName = \"mydb\",\n    applicationContext = ApplicationProvider.getApplicationContext(),\n    // Optional: A TerpalSchema object defining the database schema and migrations. Similar to an SqlDelight SqlSchema object.\n    //           Alternatively, a SqlDelight SqlSchema object or any SupportSQLiteOpenHelper.Callback can be used.\n    //           See the section below on how to define a schema.\n    schema = MyTerpalSchema,\n    // Optional: A setting describing how to pool connections. The default is a single-threaded pool.\n    poolingModel =\n      // The default mode that uses Android WAL compatibility mode\n      PoolingMode.SingleSessionWal\n      // Use this to get MVCC-like transaction isolation and real multi-reader concurrency. \n      // Mutiple instances of SupportSQLiteOpenHelper are used so be careful with memory consumption.\n      PoolingMode.MultipleReaderWal(2)\n      // Use Pre-WAL mode for compatibility with older Android versions\n      PoolingMode.SingleSessionLegacy\n  )\n// Run a query:\nval person: List\u003cPerson\u003e = Sql(\"SELECT * FROM Person\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\n### Constuct from SupportSQLiteOpenHelper\nUse the `TerpalAndroidDriver.fromSingleOpenHelper` method to create a terpal context from a single instance of `SupportSQLiteOpenHelper`\n```kotlin\nval myOpenHelperInstance = FrameworkSQLiteOpenHelperFactory().create(\n  SupportSQLiteOpenHelper.Configuration.builder(androidApplicationContext)\n    .name(databaseName)\n    // Other options e.g. callback, factory, etc.\n    .build(),\n)\nval ctx =\n  TerpalAndroidDriver.fromSingleOpenHelper(\n    openHelper = myOpenHelperInstance\n  )\n// Run a query:\nval person: List\u003cPerson\u003e = Sql(\"SELECT * FROM Person\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\nUse the `TerpalAndroidDriver.fromSingleSession` method to create a terpal context from a single instance of `SupportSQLiteDatabase`\n```kotlin\nval myDatabaseInstance = myOpenHelperInstance.writableDatabase\nval ctx =\n  TerpalAndroidDriver.fromSingleSession(\n    database = myDatabaseInstance\n  )\n// Run a query:\nval person: List\u003cPerson\u003e = Sql(\"SELECT * FROM Person\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\n\u003e Note that most of the constructors on the `TerpalAndroidDriver` object are suspended functions. This is because\n\u003e creating a `SupportSQLiteDatabase` frequently involves database schema creation and migration that is done\n\u003e as part of the SupportSQLiteOpenHelper.Callback. These contexts are required to be create from a coroutine\n\u003e to avoid blocking the main thread.\n\n## Using iOS, OSX, Linux and Windows\n\nFor iOS, OSX, Linux and Windows development, with Kotlin Multiplatform, add the following to your `build.gradle.kts` file:\n```kotlin\nplugins {\n    kotlin(\"multiplatform\") version \"2.1.0\"\n    id(\"io.exoquery.terpal-plugin\") version \"2.1.0-2.0.0.PL\"\n    kotlin(\"plugin.serialization\") version \"2.1.0\"\n}\n\nkotlin {\n    sourceSets {\n        val commonMain by getting {\n            dependencies {\n                implementation(\"org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2\")\n                implementation(\"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2\")\n                // Note that terpal-sql-native supports iOS, OSX, Linux and Windows\n                api(\"io.exoquery:terpal-runtime:2.1.0-2.0.0.PL\")\n                implementation(\"io.exoquery:terpal-sql-native:2.0.0.PL-1.2.0\")\n            }\n        }\n    }\n}\n```\n\nThe create the TerpalNativeDriver using one of the following constructors.\n\nUse the `TerpalNativeDriver.fromSchema` method to create a native Terpal context from a `TerpalSchema` object.\n\n```kotlin\nval ctx = \n  TerpalNativeDriver.fromSchema(\n    schema = MyTerpalSchema\n  )\n```\n\nThe `TerpalNativeDriver` uses SQLighter as the underlying database driver.\n```kotlin\nval ctx =\n  TerpalNativeDriver.fromSchema(\n    // Optional: A TerpalSchema object defining the database schema and migrations. Similar to an SqlDelight SqlSchema object.\n    //           Alternatively, a SqlDelight SqlSchema object can be used.\n    //           See the section below on how to define a schema.\n    schema = MyTerpalSchema,\n    // Name of the database file to use\n    databaseName = \"mydb\",\n    // Base-Path fo the database file to use (this will be the Kotlin working directory by default)\n    basePath = \"/my/custom/path\"\n  )\n```\n\n# Features\n\n## Queries and Actions\n\nTerpal SQL supports queries i.e. SELECT, actions i.e. INSERT, UPDATE, DELETE and actions that return a value i.e. INSERT... RETURNING.\nActions are supported both for single rows and in batch-mode. Terpal uses batch optimization (e.g. JDBC addBatch/excutBatch) for where possible.\n\n### Queries\n```kotlin\n@Serializable\ndata class Person(val id: Int, val firstName: String, val lastName: String)\n// Run a Query. Note that when doing a `SELECT *` the fields and  field-ordering in the table must match the data class \nval person: List\u003cPerson\u003e = Sql(\"SELECT * FROM Person WHERE id = $id\").queryOf\u003cPerson\u003e().runOn(ctx)\n\n// You can also decode the results into a sequence\nval person: Flow\u003cPerson\u003e = Sql(\"SELECT * FROM Person WHERE id = $id\").queryOf\u003cPerson\u003e().streamOn(ctx)\n\n// The output can also be a single scalar\nval firstName = Sql(\"SELECT firstName FROM Person WHERE id = $id\").queryOf\u003cString\u003e().runOn(ctx)\n```\n\n### Actions\n```kotlin\n// Regular actions:\nval firstName = \"Joe\"\nval lastName = \"Bloggs\"\nSql(\"INSERT INTO Person (firstName, lastName) VALUES ($firstName, $lastName)\").action().runOn(ctx)\n```\n\n### Actions that return a value\nUse the `actionReturning\u003cDatatype\u003e()` method to run an action that returns a value.\n\n```kotlin\nval firstName = \"Joe\"\nval lastName = \"Bloggs\"\nval id = \n  Sql(\"INSERT INTO Person (firstName, lastName) VALUES ($firstName, $lastName) RETURNING id\")\n    .actionReturning\u003cInt\u003e().runOn(ctx)\n\n// You can also decode the results into a data class\nval person: Person = \n  Sql(\"INSERT INTO Person (firstName, lastName) VALUES ($firstName, $lastName) RETURNING id, firstName, lastName\")\n    .queryOf\u003cPerson\u003e().runOn(ctx)\n\n// Note that the fields and field-ordering in the table must match the data class (we are assuming the id column is auto-generated)\n// @Serializable\n// data class Person(val id: Int, val firstName: String, val lastName: String)\n```\n\nNote that the actual syntax of the actual syntax of the returning query will very based on your database,\nfor example in SQL Server you would use:\n```kotlin\nval id = Sql(\"INSERT INTO Person (firstName, lastName) OUTPUT INSERTED.id VALUES ($firstName, $lastName)\").actionReturning\u003cInt\u003e().runOn(ctx)\n```\n\n\u003e For other databases e.g. Oracle you may need to specify the columns being returned as a list:\n\u003e ```kotlin\n\u003e Sql(\"INSERT INTO Person (firstName, lastName) VALUES ($firstName, $lastName)\").actionReturning\u003cPerson\u003e(\"id\", \"firstName\", \"lastName\").runOn(ctx)\n\u003e ```\n\u003e When a list of columns is specified in `actionReturning`, the are passed into the statement-preparation of the underlying\n\u003e database driver. For example `Connection.prepareStatement(sql, arrayOf(\"id\", \"firstName\", \"lastName\"))`.\n\n\n## Batch Actions\nTerpal batch actions are supported for both regular actions and actions that return a value.\n```kotlin\nval people = listOf(\n  Person(1, \"Joe\", \"Bloggs\"),\n  Person(2, \"Jim\", \"Roogs\")\n)\n\n// Regular batch action\nSqlBatch { p: Person -\u003e\n  \"INSERT INTO Person (id, firstName, lastName) VALUES (${p.id}, ${p.firstName}, ${p.lastName})\")\n}.values(people).action().runOn(ctx)\n\n// Batch action that returns a value\nval ids = SqlBatch { p: Person -\u003e\n  \"INSERT INTO Person (id, firstName, lastName) VALUES (${p.id}, ${p.firstName}, ${p.lastName}) RETURNING id\")\n}.values(people).actionReturning\u003cInt\u003e().runOn(ctx)\n```\nBatch actions can return records. They can also take streaming inputs as well as outputs.\n```kotlin\n// Batch queries can take a stream of values using the `.values(Flow\u003cT\u003e)` method.\nval people: Flow\u003cPerson\u003e = flowOf(Person(1, \"Joe\", \"Bloggs\"), Person(2, \"Jim\", \"Roogs\"), ...)\n// Use stream-on to stream the outputs\nval outputs: Flow\u003cPerson\u003e =\n  val ids = SqlBatch { p: Person -\u003e\n    \"INSERT INTO Person (id, firstName, lastName) VALUES (${p.id}, ${p.firstName}, ${p.lastName}) RETURNING id, firstName, lastName\")\n  }.values(people).actionReturning\u003cInt\u003e().streamOn(ctx)\n```\n\nIn each case, the batch action uses the driver-level batch optimization (e.g. JDBC addBatch/executeBatch) wherever possible.\nFor example, SQLite does not support batch actions returning values.\n\n\u003e Note that similar to regular actions, the actual syntax of the returning query will very based on your database,\n\u003e for example for Oracle you would use:\n\u003e ```kotlin\n\u003e val ids = SqlBatch { p: Person -\u003e\n\u003e  \"INSERT INTO Person (id, firstName, lastName) VALUES (${p.id}, ${p.firstName}, ${p.lastName})\")\n\u003e }.values(people).actionReturning\u003cPerson\u003e(\"id\", \"firstName\", \"lastName\").runOn(ctx)\n\n## Transactions\nTerpal supports transactions using the `transaction` method. The transaction method takes a lambda that\ncontains the actions to be performed in the transaction. If the lambda throws an exception the transaction\nis rolled back, otherwise it is committed.\n```kotlin\nval ctx = TerpalDriver.Postgres.fromConfig(\"mydb\")\nctx.transaction {\n  Sql(\"INSERT INTO Person (id, firstName, lastName) VALUES (1, 'Joe', 'Bloggs')\").action().run()\n  Sql(\"INSERT INTO Person (id, firstName, lastName) VALUES (2, 'Jim', 'Roogs')\").action().run()\n}\n// Note that in the body of `transaction` you can use the `run` method without passing a context since the context is passed as a reciever.\n```\n\nIf the transaction is aborted in the middle (e.g. by throwing an exception) the transaction is rolled back.\n```kotlin\ntry {\n  ctx.transaction {\n    Sql(\"INSERT INTO Person (id, firstName, lastName) VALUES (1, 'Joe', 'Bloggs')\").action().run()\n    throw Exception(\"Abort\")\n    Sql(\"INSERT INTO Person (id, firstName, lastName) VALUES (2, 'Jim', 'Roogs')\").action().run()\n  }\n} catch (e: Exception) {\n  // The transaction is rolled back\n}\n```\n\n\n## Custom Parameters\nWhen a variable used in a Sql clause e.g. `Sql(\"... $dollar_sign_variable ...\")` it needs\nto be wrapped in a `Param` object.\n\n### Transient Values\n\nFrequently, serializeable classes will have additional values that are not stored in the database\nbut rather can be computed by default. For example:\n```kotlin\n@Serializable\ndata class Person(val name: String, val age: Int) {\n  val title = \"Mr \" + name\n}\n\n// Given the database schema:\n// CREATE TABLE person (name TEXT, age INT)\n```\nMake sure to add the annotation `@Transient` to the field that is not stored in the database!\n```kotlin\n@Serializable\ndata class Person(val name: String, val age: Int) {\n  @Transient\n  val title = \"Mr \" + name\n}\n```\nIf you forget to add this annotation, the Kotlin Serialization library will attempt to read the field\nfrom the database row i.e. as though the database schema had a third `title` column \n(and then write it into the `val title` field... which should technically be impossible, see the note).\n\n\u003e NOTE: As of version 1.6.2 the behavior of kotlinx-serialization regarding `val` fields in data classes is highly\n\u003e unintuitive. The Kotlin serialization library will create a hidden constructor that takes the `val` fields marked\n\u003e inside the class body that technically are not accessible at all. It is as though the `Person` class actually \n\u003e looks like this:\n\u003e ```kotlin\n\u003e // This primary constructor is hidden:\n\u003e data class Person(val name: String, val age: Int, val title: String, val title: String) {\n\u003e  constructor(name: String, age: Int, title: Int = \"Mr \" + name) : this(name, age, name)\n\u003e }\n\u003e ```\n\u003e This is why the `@Transient` annotation is required to prevent the Kotlin Serialization library from trying to\n\u003e alter the class structure in this bizarre way.\n\n\n\n### Automatic Wrapping\n\nYou can use the `io.exoquery.sql.Param` object to splice a variable in a `Sql(...)` clause.\n```kotlin\nval id = 123\nval manualWrapped = Sql(\"SELECT * FROM Person WHERE id = ${Param(id)}\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\nThis will happen automatically when using Kotlin primitives and some date-types.\n```kotlin\nval id = 123\nval automaticWrapped = Sql(\"SELECT * FROM Person WHERE id = $id\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\nThe following types are automatically wrapped:\n - Primitives: String, Int, Long, Short, Byte, Float, Double, Boolean\n - Time Types: `java.util.Date`, LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, OffsetTime, OffsetDateTime\n - Kotlin Multiplatform Time Types: `kotlinx.datetime.LocalDate`, `kotlinx.datetime.LocalTime`, `kotlinx.datetime.LocalDateTime`, `kotlinx.datetime.Instant`\n - SQL Time Types: `java.sql.Date`, `java.sql.Timestamp`, `java.sql.Time`\n - Other: BigDecimal, ByteArray\n - Note that in all the time-types Nano-second granularity is not supported. It will be rounded to the nearest millisecond.\n\n### Custom Wrapped Types\n\nIf you want to use use a custom datatype in an `Sql(...)` clause you need to wrap it in a `Param` object.\nTypically you need to define a primitive wrapper-serializer in order to do this.\n```kotlin\n@JvmInline\nvalue class Email(val value: String)\n\nval email: Email = Email(\"...\")\nSql(\"INSERT INTO customers (firstName, lastName, email) VALUES ($firstName, $lastName, ${email})\").action().runOn(ctx)\n//\u003e /my/project/path/NewtypeColumn.kt:46:5 The interpolator Sql does not have a wrap function for the type: my.package.Email.\n\n// Define a serializer for the custom type\nobject EmailSerializer : KSerializer\u003cEmail\u003e {\n  override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(\"Email\", PrimitiveKind.STRING)\n  override fun serialize(encoder: Encoder, value: Email) = encoder.encodeString(value.value)\n  override fun deserialize(decoder: Decoder) = Email(decoder.decodeString())\n}\n\n// Then use in the Param wrapper (`withSer` is an alias for `withSerializer`) giving it a serializer. \nSql(\"INSERT INTO customers (firstName, lastName, email) VALUES ($firstName, $lastName, ${Param.withSer(email, EmailSerialzier)})\")\n  .action().runOn(ctx)\n```\n\nKeep in mind that if you want to use this custom datatype in a parent case-class you will need to let kotlinx-serialization\nknow that the EmailSerializer needs to be used. The simplest way to do this is to use the `@Serializable(with = ...)` annotation on the parent class.\n```kotlin\ndata class Customer(\n  val id: Int, \n  val firstName: String, val lastName: String, \n  @Serializable(with = EmailSerializer::class) val email: Email\n)\n\n// The you can query the data class as normal:\nval customers: List\u003cCustomer\u003e = Sql(\"SELECT * FROM customers\").queryOf\u003cCustomer\u003e().runOn(ctx)\n```\n\nThere are several other ways to do this, have a look at the [Custom Serializers](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#custom-serializers) section kotlinx-serialization documentation for more information.\n\n### Custom Primitives\n\nIn some situations, with a custom datatype you may need to control how it is encoded in the database driver.\nTake for example this highly custom type representing input byptes:\n```kotlin\ndata class ByteContent(val bytes: InputStream) {\n  companion object {\n    fun bytesFrom(input: InputStream) = ByteContent(ByteArrayInputStream(input.readAllBytes()))\n  }\n}\n\n// Instead of making the ByteContent a serializable type, we mark it as a contextual type.\n@Serializable\ndata class Image(val id: Int, @Contextual val content: ByteContent)\n\n// Then we provide an encoder and decoder for it on the driver-level (i.e. JDBC) when creating the context:\nval ctx = object: TerpalDriver.Postgres(postgres.postgresDatabase) {\n  override val additionalDecoders =\n    super.additionalDecoders + JdbcDecoderAny.fromFunction { ctx, i -\u003e ByteContent(ctx.row.getBinaryStream(i)) }\n  override val additionalEncoders =\n    super.additionalEncoders + JdbcEncoderAny.fromFunction(Types.BLOB) { ctx, v: ByteContent, i -\u003e ctx.stmt.setBinaryStream(i, v.bytes) }\n}\n\n// We can then read the content:\nval customers = ctx.run(Sql(\"SELECT * FROM images\").queryOf\u003cImage\u003e())\n```\nNote that in order to splice a contextual datatype into a `Sql(...)` clause you will need to use `Param.contextual`.\n```kotlin\nval data: ByteContent = ...\nSql(\"INSERT INTO images (id, content) VALUES ($id, ${Param.contextual(data)})\").action().runOn(ctx)\n```\n\nHave a look at the [Contextual Column Clob](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/ContextualColumnCustom.kt) example for more details.\n\n### Nested Data Classes\n\nTerpal supports nested data classes and will \"flatten\" the data classes when decoding the results.\nNote that both the outer class and inner class must be annotated with `@Serializable` in most cases.\n```kotlin\n@Serializable\ndata class Person(val id: Int, val name: Name, val age: Int)\n@Serializable\ndata class Name(val firstName: String, val lastName: String)\n\n// Use a regular table schema. Terpal knows how to flatten the data classes.\n// CREATE TABLE person (id INT, firstName TEXT, lastName TEXT, age INT)\n\nval people: List\u003cPerson\u003e = Sql(\"SELECT * FROM people\").queryOf\u003cPerson\u003e().runOn(ctx)\nprintln(people)\n//\u003e Person(id=1, name=Name(firstName=Joe, lastName=Bloggs), age=30)\n```\n\n### IN (...) Clauses\n\nTerpal supports lifting lists of arbitrary objects for `IN (...)` clauses. The\nsame encoding mechanisms that are used for regular parameters are used for lists.\n\nUse the `io.exoquery.sql.Params.invoke(...)` family of functions to lift\nlists of elements (similar to the way `Param.invoke(...)` is used for single elements).\n```kotlin\nval peopleQuery = Sql(\"SELECT * FROM people WHERE firstName IN ${Params(\"Joe\", \"Jim\")}\").queryOf\u003cPerson\u003e()\n// peopleQuery.sql = \"SELECT * FROM people WHERE firstName IN (?, ?)\"\n\nprintln(peopleQuery.runOn(ctx))\n//\u003e List(Person(id=1, firstName=Joe, lastName=Bloggs), Person(id=2, firstName=Jim, lastName=Roogs))\n```\nMake sure to *not* add parentheses around the `Params(...)` clause. This is done automatically.\n\nIf you want to use an instance of `List\u003cT\u003e` in an `IN (...)` clause you can use the `Params.list`\nconvenience function.\n```kotlin\nval ids = listOf(1, 2, 3)\nval people = Sql(\"SELECT * FROM people WHERE id IN ${Params.list(ids)}\").queryOf\u003cPerson\u003e().runOn(ctx)\n```\n\n\u003e Note that since `column IN ()` is invalid SQL syntax, therefore invoking `Params.list(emptyList)`\n\u003e will result in `column IN (null)` being synthesized instead.\n\u003e ```kotlin\n\u003e val emptyList = listOf\u003cInt\u003e()\n\u003e val peopleQuery = Sql(\"SELECT * FROM people WHERE id IN ${Params.list(emptyList)}\").queryOf\u003cPerson\u003e()\n\u003e // peopleQuery.sql = \"SELECT * FROM people WHERE id IN (null)\"\n\u003e ```\n\n### JSON Valued Columns\n\u003e NOTE: This is currently only supported in Postgres\n\n#### Using the SqlJsonValue Annotation\n\nIn Postgres you can store JSON data in a column. Terpal can automatically decode the JSON data into a Kotlin data class in two ways.\nThe first way is to add a `@SqlJsonValue` on the data class field. This will tell Terpal to decode that particular column\nas JSON when the parent-object is queried.\n\n```kotlin\n@Serializeable\ndata class Person(val name: String, val age: Int)\n@Serializeable\ndata class JsonExample(val id: Int, @SqlJsonValue val person: Person)\n\n// Inserting an example value directly:\nSql(\"\"\"INSERT INTO JsonExample (id, person) VALUES (1, '{\"name\": \"Joe\", \"value\": 30}')\"\"\").action().runOn(ctx)\n// Retrieve it like this:\nval values: List\u003cJsonExample\u003e = Sql(\"SELECT id, person FROM JsonExample\").queryOf\u003cJsonExample\u003e().runOn(ctx)\n//\u003e List(JsonExample(1, Person(name=Joe, value=30)))\n```\nThis list of `JsonExample` parent-objects is returned.\n\n\u003e Note how you cannot query for the `Person` class directly in the above example because it is not annotated with `@SqlJsonValue`.\n\u003e Querying for it will make Terpal try to fetch the Person.name and Person.age columns because it will treat\n\u003e the Person class as a regular data class.\n\u003e ```\n\u003e Sql(\"SELECT person FROM JsonExample\").queryOf\u003cPerson\u003e().runOn(ctx)\n\u003e //\u003e Column mismatch. The columns from the SQL ResultSet metadata did not match the expected columns from the deserialized type\n\u003e //\u003e SQL Columns (1): [(0)value:jsonb], Class Columns (2): [(0)name:kotlin.String, (1)age:kotlin.Int]\n\u003e ```\n\u003e The next example will show how to get around this.\n\nYou can also place the `@SqlJsonValue` annotation on the actual child data-class. The advantage of this is that Terpal\nwill know to decode the JSON data into the child data-class directly (not only when it is queried as part of the parent).\n\n```kotlin\n@SqlJsonValue\n@Serializeable\ndata class Person(val name: String, val age: Int)\n@Serializeable\ndata class JsonExample(val id: Int, val person: Person)\n\nval person = Person(\"Joe\", 30)\nSql(\"\"\"INSERT INTO JsonExample (id, person) VALUES (1, '{\"name\": \"Joe\", \"value\": 30}')\"\"\").action().runOn(ctx)\n\n//\u003e List(JsonExample(1, Person(name=Joe, value=30)))\n\n// Can insert the Person class directly:\nSql(\"INSERT INTO JsonExample (id, person) VALUES (1, ${Param.withSer(person)})\").action().runOn(ctx)\n\n// Can select the Person class directly:\nval values: List\u003cPerson\u003e = Sql(\"SELECT person FROM JsonExample\").queryOf\u003cPerson\u003e().runOn(ctx)\n//\u003e List(Person(name=Joe, value=30))\n\n// Can select the the parent entity:\nval values: List\u003cJsonExample\u003e = Sql(\"SELECT id, person FROM JsonExample\").queryOf\u003cJsonExample\u003e().runOn(ctx)\n//\u003e List(JsonExample(1, Person(name=Joe, value=30)))\n```\n\nYou can find more examples using JSON columns in the [Json Column Examples](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/JsonColumnExample.kt)\ndocumentation.\n\n#### Using the JsonValue Wrapper\n\nIf you want more explicit control over how JSON data is encoded/decoded you can use the `JsonValue` wrapper.\nThe advantage of this approach is that you can store arbitrary JSON data in a column and decode it\ninto a datatype without a wrapper-class (i.e. what the `Person` class was doing above).\n```kotlin\n@Serializeable\ndata class JsonExample(val id: Int, val names: JsonValue\u003cList\u003cString\u003e\u003e)\n\n// This:\nSql(\"\"\"INSERT INTO JsonExample (id, names) VALUES (1, '[\"Joe\", \"Jack\"]')\"\"\").action().runOn(ctx)\n// Is equivalent to:\nSql(\"INSERT INTO JsonExample (id, names) VALUES (1, ${JsonValue(listOf(\"Joe\", \"Jack\"))})\").action().runOn(ctx)\n\n// Can select the JSON data directly:\nval values: List\u003cJsonValue\u003cList\u003cString\u003e\u003e\u003e = Sql(\"SELECT names FROM JsonExample\").queryOf\u003cJsonValue\u003cList\u003cString\u003e\u003e\u003e().runOn(ctx)\n//\u003e List(JsonValue(List(\"Joe\", \"Jack\")))\n\n// Can select the parent entity:\nval values: List\u003cJsonExample\u003e = Sql(\"SELECT id, names FROM JsonExample\").queryOf\u003cJsonExample\u003e().runOn(ctx)\n//\u003e List(JsonExample(1, JsonValue(List(\"Joe\", \"Jack\"))))\n```\n\n#### Mix and Match\n\nYou can mix and match the `@SqlJsonValue` annotation and the `JsonValue` wrapper. This is useful when you want to\ndefine a field using `@SqlJsonValue` but then need to insert and/or query that JSON-column by itself.\n```kotlin\n@Serializeable\ndata class Person(val name: String, val age: Int)\n@Serializeable\ndata class JsonExample(val id: Int, @SqlJsonValue val person: Person)\n\n// Insert the JSON data directly\nSql(\"INSERT INTO JsonExample (id, person) VALUES (1, ${Param(JsonValue(Person(\"Joe\", 30)))})\").action().runOn(ctx)\n// A helper function Param.json has been introduced to make this easier\nSql(\"INSERT INTO JsonExample (id, person) VALUES (1, ${Param.json(Person(\"Joe\", 30))})\").action().runOn(ctx)\n\n// Select the JSON data directly\nval values: List\u003cJsonValue\u003cPerson\u003e\u003e = Sql(\"SELECT person FROM JsonExample\").queryOf\u003cJsonValue\u003cPerson\u003e\u003e().runOn(ctx)\n//\u003e List(JsonValue(Person(name=Joe, value=30)))\n\n// Select the parent entity\nval values: List\u003cJsonExample\u003e = Sql(\"SELECT id, person FROM JsonExample\").queryOf\u003cJsonExample\u003e().runOn(ctx)\n//\u003e List(JsonExample(1, Person(name=Joe, value=30)))\n```\n\nYou can find more examples using JSON columns in the [Json Column Examples](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/JsonColumnExample.kt) \ndocumentation.\n\n### Playing well with other Kotlinx Formats\n\nWhen using Terpal-SQL with kotlinx-serialization with other formats such as JSON in real-world situations, you may\nfrequently need either different encodings or even entirely different schemas for the same data. For example, you may\nwant to encode a `LocalDate` using the SQL `DATE` type, but when sending the data to a REST API you may want to encode \nthe same `LocalDate` as a `String` in ISO-8601 format (i.e. using DateTimeFormatter.ISO_LOCAL_DATE).\n\nThere are several ways to do this in Kotlinx-serialization, I will discuss two of them.\n\n#### Using a Contextual Serializer\nThe simplest way to have a different encoding for the same data in different contexts is to use a contextual serializer.\n```kotlin\n@Serializable\ndata class Customer(val id: Int, val firstName: String, val lastName: String, @Contextual val createdAt: LocalDate)\n\n// Database Schema:\n// CREATE TABLE customers (id INT, first_name TEXT, last_name TEXT, created_at DATE)\n\n// This Serializer encodes the LocalDate as a String in ISO-8601 format and it will only be used for JSON encoding.\nobject DateAsIsoSerializer: KSerializer\u003cLocalDate\u003e {\n  override val descriptor = PrimitiveSerialDescriptor(\"LocalDate\", PrimitiveKind.STRING)\n  override fun serialize(encoder: Encoder, value: LocalDate) = encoder.encodeString(value.format(DateTimeFormatter.ISO_LOCAL_DATE))\n  override fun deserialize(decoder: Decoder): LocalDate = LocalDate.parse(decoder.decodeString(), DateTimeFormatter.ISO_LOCAL_DATE)\n}\n\n// When working with the database, the LocalDate will be encoded as a SQL DATE type. The Terpal Driver knows\n// will behave this way by default when a field is marked as @Contextual.\nval ctx = TerpalDriver.Postgres.fromConfig(\"mydb\")\nval customer = Customer(1, \"Alice\", \"Smith\", LocalDate.of(2021, 1, 1))\nSql(\"INSERT INTO customers (first_name, last_name, created_at) VALUES (${customer.firstName}, ${customer.lastName}, ${customer.createdAt})\").action().runOn(ctx)\n\n// When encoding the data as JSON, the make sure to specify the DateAsIsoSerializer in the serializers-module.\nval json = Json {\n  serializersModule = SerializersModule {\n    contextual(LocalDate::class, DateAsIsoSerializer)\n  }\n}\nval jsonCustomer = json.encodeToString(Customer.serializer(), customer)\nprintln(jsonCustomer)\n//\u003e {\"id\":1,\"firstName\":\"Alice\",\"lastName\":\"Smith\",\"createdAt\":\"2021-01-01\"}\n```\nSee the [Playing Well using Different Encoders](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/PlayingWell_DifferentEncoders.kt)\nexample for more details.\n\n#### Using Row-Surrogate Encoder\nWhen the changes in encoding between the Database and JSON are more complex, you may want to use a row-surrogate encoder.\n\nA row-surrogate encoder will take a data-class and copy it into another data-class (i.e. the surrogate data-class) whose schema is appropriate\nfor the target format. The surrogate data-class needs to also be serializable and know how to create itself from the original data-class.\n\n```kotlin\n// Create the \"original\" data class\n@Serializable\ndata class Customer(\n  val id: Int, \n  val firstName: String, \n  val lastName: String, \n  @Serializable(with = DateAsIsoSerializer::class) val createdAt: LocalDate\n)\n\n// Create the \"surrogate\" data class\n@Serializable\ndata class CustomerSurrogate(val id: Int, val firstName: String, val lastName: String, @Contextual val createdAt: LocalDate) {\n  fun toCustomer() = Customer(id, firstName, lastName, createdAt)\n  companion object {\n    fun fromCustomer(customer: Customer): CustomerSurrogate {\n      return CustomerSurrogate(customer.id, customer.firstName, customer.lastName, customer.createdAt)\n    }\n  }\n}\n```\n\nThen create a surrogate serializer which uses the surrogate data-class to encode the original data-class.\n```kotlin\nobject CustomerSurrogateSerializer: KSerializer\u003cCustomer\u003e {\n  override val descriptor = CustomerSurrogate.serializer().descriptor\n  override fun serialize(encoder: Encoder, value: Customer) = \n    encoder.encodeSerializableValue(CustomerSurrogate.serializer(), CustomerSurrogate.fromCustomer(value))\n  override fun deserialize(decoder: Decoder): Customer = \n    decoder.decodeSerializableValue(CustomerSurrogate.serializer()).toCustomer()\n}\n```\n\nThen use the surrogate serializer when reading data from the database.\n```kotlin\n// You can then use the surrogate class when reading/writing information from the database:\nval customers = ctx.run(Sql(\"SELECT * FROM customers\").queryOf\u003cCustomer\u003e(CustomerSurrogateSerializer))\n\n// ...and use the regular data-class/serializer when encoding/decoding to JSON\nprintln(Json.encodeToString(ListSerializer(Customer.serializer()), customers))\n//\u003e [{\"id\":1,\"firstName\":\"Alice\",\"lastName\":\"Smith\",\"createdAt\":\"2021-01-01\"}]\n```\n\nSee the [Playing Well using Row-Surrogate Encoder](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/PlayingWell_RowSurrogate.kt)\nsection for more details.\n\n### Using Array Columns\n\nCurrently Terpal-SQL does not support direct encoding/decoding of generic\ncollections. In order to use array columns create a value-class with a specific\ncollection type and use a combination of `@Contextual`, custom encoders, and\n`Param.contextual` in order to achieve the desired behavior.\n\nThe following example illustrates how to do this for Postgres array columns.\n```kotlin\n// Given the table schema looks like this:\n// CREATE TABLE friend (firstName TEXT, lastName TEXT, nickNames TEXT[])\n\n// First create custom datatypes:\n@JvmInline\nvalue class MyStringList(val value: List\u003cString\u003e)\n@Serializable\ndata class Friend(val id: Int, val firstName: String, val lastName: String, @Contextual val nickNames: MyStringList)\n\n// Then create the encoder/decoder\nval MyStringListEncoder: JdbcEncoderAny\u003cMyStringList\u003e =\n  JdbcEncoderAny(Types.VARCHAR, MyStringList::class) { ctx, v, i -\u003e\n    val arr = ctx.session.createArrayOf(JDBCType.VARCHAR.toString(), v.value.toTypedArray())\n    ctx.stmt.setArray(i, arr)\n  }\nval MyStringListDecoder: JdbcDecoderAny\u003cMyStringList\u003e =\n  JdbcDecoderAny(MyStringList::class) { ctx, i -\u003e\n    MyStringList((ctx.row.getArray(i).array as Array\u003cString\u003e).toList())\n  }\n\n// Pass them into the context\nval ctx = TerpalDriver.Postgres(\n  postgres.postgresDatabase,\n  JdbcEncodingConfig.Default(setOf(MyStringListEncoder), setOf(MyStringListDecoder))\n)\n\n// Then configuration is complete!\n// You can now use the custom list datatype MyStringList during insertion (this uses the encoders)\nval joeNicknames = MyStringList(listOf(\"Joey\", \"Jay\"))\nSql(\"INSERT INTO person (firstName, lastName, age) VALUES ('Joe', 'Bloggs', ${Param.contextual(joeNicknames)})\").action().runOn(ctx)\n\n// As well as during selection (this uses the decoders)\nval friends = Sql(\"SELECT * FROM friend WHERE\").queryOf\u003cFriend\u003e()\n\n// ...or both at the same time:\nval joeOrJill = MyStringList(listOf(\"Joe\", \"Jill\"))\nval friends = Sql(\"SELECT * FROM friend WHERE firstName = ANY(${Param.contextual(joeOrJill)})\").queryOf\u003cFriend\u003e()\n\nprintln(friends)\n//\u003e [Friend(firstName=Joe, lastName=Bloggs, nickNames=MyStringList(value=[Joey, Jay])), Friend(firstName=Jill, lastName=Doggs, nickNames=MyStringList(value=[Jilly, Jillaroo]))]\n```\nFor a working example of the above instructions see [UsingPostgresArray](terpal-sql-jdbc/src/test/kotlin/io/exoquery/sql/examples/UsingPostgresArray.kt).\n\n## IntelliJ Language Injection Support\n\nTerpal-SQL is optimized for IntelliJ [Language Injection](https://www.jetbrains.com/help/idea/using-language-injections.html) support. As such, IntelliJ will understand that\nstrings inside of the `Sql(...)` clause should be treated as SQL constructs and provide custom auto-completion as such:\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/user-attachments/assets/2d211073-bacf-49ae-9f2f-545c9558a544\" width=70% height=70% \u003e\n\u003c/p\u003e\n\nIt is important to note that this auto-complete is based on what database IntelliJ believes the current SQL code snippet is actually related to, and conveying this information to IntelliJ may require some manual steps.\nTypically you need to click on the [Intention-actions](https://www.jetbrains.com/help/idea/intention-actions.html) i.e. small yellow lightbulb menu (Alt+Enter) and then on Run query in console. Based on the database that you select, IntelliJ will deduce appropriate hints and code completion.\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/user-attachments/assets/f0a95a6d-2cf7-479f-872e-428f1040307e\" width=70% height=70% \u003e\n\u003c/p\u003e\n\nAs in all things related to auto-complete and IDEs, your personal mileage may vary. Particularly when it comes to other integrated tools such as Copilot or Jetbrains AI.\nOne additional note, when looking for correct Intention-action, make sure that your cursor is in the SQL string itself not in the surrounding `Sql(...)` function.\nThe following two screenshots demonstrate this.\n\n\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"https://github.com/user-attachments/assets/03fe9db2-19da-452b-9b92-acacff1ed48e\" width=40% height=40% \u003e\n\u003cimg src=\"https://github.com/user-attachments/assets/f09a3f97-d70a-4a8b-b7e6-afb6aa790113\" width=40% height=40% \u003e\n\u003c/p\u003e\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fterpal-sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexoquery%2Fterpal-sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fterpal-sql/lists"}