├── .cursorignore ├── .gitattributes ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build-local.sh ├── build-logic ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── conventions.gradle.kts │ └── nativebuild.gradle.kts ├── build.gradle.kts ├── controller-android ├── build.gradle.kts └── src │ └── androidMain │ ├── AndroidManifest.xml │ ├── com │ └── jetbrains │ │ └── kmm │ │ └── shared │ │ └── Platform.kt │ └── kotlin │ └── io │ └── exoquery │ └── controller │ └── android │ ├── AdditionalAndroidEncoding.kt │ ├── AndroidDatabaseController.kt │ ├── AndroidEncoding.kt │ ├── AndroidMixIns.kt │ ├── AndroidOps.kt │ ├── AndroidPool.kt │ ├── Test.kt │ └── time │ └── TimeHelpers.kt ├── controller-core ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── exoquery │ │ └── controller │ │ ├── Annotations.kt │ │ ├── Controller.kt │ │ ├── DecoderAny.kt │ │ ├── EncoderAny.kt │ │ ├── Encoding.kt │ │ ├── EncodingContext.kt │ │ ├── EncodingException.kt │ │ ├── JsonValue.kt │ │ ├── Messages.kt │ │ ├── Model.kt │ │ ├── Param.kt │ │ ├── PreparedStatementElementEncoder.kt │ │ ├── RowDecoder.kt │ │ ├── jdbc │ │ └── CoroutineTransaction.kt │ │ ├── sqlite │ │ ├── DoublePoolBase.kt │ │ ├── LruCache.kt │ │ ├── SqliteSession.kt │ │ ├── SqliteWrappedEncoding.kt │ │ ├── StatementCachingSession.kt │ │ ├── TerpalLruStatementCache.kt │ │ └── TerpalSchema.kt │ │ └── util │ │ └── SerializerFrom.kt │ ├── jvmMain │ └── kotlin │ │ └── sqlite │ │ └── DoublePoolBase.kt │ └── nativeMain │ └── kotlin │ └── io │ └── exoquery │ └── controller │ └── sqlite │ └── DoublePoolBase.kt ├── controller-jdbc ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── exoquery │ └── controller │ ├── JavaEncoding.kt │ ├── RunFunctions.kt │ └── jdbc │ ├── HikariHelper.kt │ ├── JdbcContextConfig.kt │ ├── JdbcContextMixins.kt │ ├── JdbcController.kt │ ├── JdbcControllers.kt │ ├── JdbcDecoders.kt │ ├── JdbcEncoders.kt │ ├── JdbcEncodingContext.kt │ ├── JdbcExecutionOptions.kt │ ├── JdbcUuidStringEncoding.kt │ └── SwitchingContextSerializer.kt ├── controller-native ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── exoquery │ └── controller │ ├── delight │ ├── SqlCursorExt.kt │ ├── SqlDelightContextExt.kt │ ├── SqlDelightController.kt │ └── SqlDelightEncoding.kt │ └── native │ ├── NativeContextMixins.kt │ ├── NativeDatabaseController.kt │ ├── NativeEncodingConfig.kt │ ├── SchemaOps.kt │ ├── SimplePool.kt │ └── SqliterExt.kt ├── docker-compose.yml ├── docs ├── .nojekyll ├── CNAME ├── coverpage.md └── index.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libs ├── linux │ └── cinterop │ │ └── sqlite3.def ├── note.txt └── windows │ └── libsqlite3.a ├── local.properties ├── publish-local.sh ├── scripts ├── Dockerfile-setup ├── setup_databases.sh ├── setup_db_scripts.sh ├── setup_local.sh ├── setup_sqlite_only.sh └── start.sh ├── settings.gradle.kts ├── terpal-sql-android ├── build.gradle.kts └── src │ ├── androidInstrumentedTest │ └── kotlin │ │ └── io │ │ └── exoquery │ │ └── sql │ │ └── android │ │ └── instrumented │ │ ├── InstrumentedPerfSpec.kt │ │ ├── InstrumentedSpec.kt │ │ ├── InstrumentedTransactionSpec.kt │ │ └── InstrumentedWalConcurrencySpec.kt │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── exoquery │ │ └── sql │ │ ├── ParamExtensions.kt │ │ ├── Wrappers.kt │ │ └── android │ │ └── Alias.kt │ └── androidUnitTest │ └── kotlin │ └── io │ └── exoquery │ └── sql │ └── android │ ├── BasicActionSpec.kt │ ├── BasicQuerySpec.kt │ ├── EncodingSpec.kt │ ├── TestDatabase.kt │ ├── TransactionSpec.kt │ ├── WalConcurrencySpec.kt │ └── encodingdata │ ├── JavaEntities.kt │ ├── MiscOpsJvm.kt │ └── TimeEntities.kt ├── terpal-sql-core-testing ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── exoquery │ └── sql │ ├── BasicActionOps.kt │ ├── PerfSchema.kt │ ├── PoolConcurrencyOps.kt │ ├── SqliteSchemas.kt │ ├── TestOps.kt │ ├── TransactionSpecOps.kt │ ├── WalConcurrencyOps.kt │ ├── WalPerformanceTest.kt │ └── encodingdata │ ├── EncodingTestEntity.kt │ ├── EncodingTestEntityImp.kt │ ├── EncodingTestEntityVal.kt │ ├── KmpTestEntity.kt │ └── MiscOps.kt ├── terpal-sql-core ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── exoquery │ ├── controller │ └── Transactions.kt │ └── sql │ ├── IR.kt │ ├── Sql.kt │ └── Statement.kt ├── terpal-sql-jdbc ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── exoquery │ │ └── sql │ │ ├── ParamExtensions.kt │ │ ├── Wrappers.kt │ │ └── jdbc │ │ ├── Alias.kt │ │ └── Sql.kt │ └── test │ ├── kotlin │ └── io │ │ └── exoquery │ │ └── sql │ │ ├── BatchActionSpecData.kt │ │ ├── EncodingSpecData.kt │ │ ├── KotestProjectConfig.kt │ │ ├── Main.kt │ │ ├── Model.kt │ │ ├── NestedInterpolationSpec.kt │ │ ├── TestDatabases.kt │ │ ├── TestExtensions.kt │ │ ├── encodingdata │ │ ├── JavaEntities.kt │ │ ├── MiscOpsJvm.kt │ │ └── TimeEntities.kt │ │ ├── examples │ │ ├── ContextualColumn.kt │ │ ├── ContextualColumnClob.kt │ │ ├── ContextualColumnCustom.kt │ │ ├── JsonColumnExample.kt │ │ ├── NewtypeColumn.kt │ │ ├── NewtypeColumnContextual.kt │ │ ├── NewtypeColumnContextual_DifferentEncoders.kt │ │ ├── PlayingWell_DifferentEncoders.kt │ │ ├── PlayingWell_RowSurrogate.kt │ │ ├── QuickPostgres.kt │ │ ├── Simple_SqlServer.kt │ │ ├── UsingParams.kt │ │ └── UsingPostgresArray.kt │ │ ├── h2 │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ └── TransactionSpec.kt │ │ ├── mysql │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ └── TransactionSpec.kt │ │ ├── oracle │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ └── TransactionSpec.kt │ │ ├── postgres │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ ├── InjectionSpec.kt │ │ ├── JsonSpec.kt │ │ ├── TransactionSpec.kt │ │ └── TypeModuleSpec.kt │ │ ├── sqlite │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ └── TransactionSpec.kt │ │ └── sqlserver │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── BatchValuesSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── InQuerySpec.kt │ │ └── TransactionSpec.kt │ └── resources │ ├── application.conf │ ├── db │ ├── h2-schema.sql │ ├── mysql-schema.sql │ ├── oracle-schema.sql │ ├── postgres-schema.sql │ ├── sqlite-schema.sql │ └── sqlserver-schema.sql │ └── kotest.properties ├── terpal-sql-native ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── exoquery │ │ └── sql │ │ └── native │ │ └── Alias.kt │ └── commonTest │ ├── kotlin │ └── io │ │ └── exoquery │ │ └── sql │ │ └── native │ │ ├── BasicActionSpec.kt │ │ ├── BasicQuerySpec.kt │ │ ├── DelightSpec.kt │ │ ├── EncodingSpec.kt │ │ ├── LruCacheSpec.kt │ │ ├── PerfTest.kt │ │ ├── PoolConcurrencySpec.kt │ │ ├── TestDatabase.kt │ │ ├── TransactionSpec.kt │ │ └── WalConcurrencySpec.kt │ └── resources │ └── sqlite-schema.sql └── test-local.sh /.cursorignore: -------------------------------------------------------------------------------- 1 | # Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) 2 | # Build outputs 3 | /build/ 4 | /**/build/ 5 | 6 | # Gradle caches and logs 7 | .gradle/ 8 | /.gradle-cache/ 9 | /**/.gradle/ 10 | 11 | # IntelliJ project files 12 | .idea/ 13 | /*.iml 14 | *.ipr 15 | *.iws 16 | 17 | # Local configuration 18 | local.properties 19 | 20 | # Dependency locks and metadata 21 | **/*.lock 22 | **/gradle/libs.versions.toml 23 | 24 | # Binary and temp files 25 | *.class 26 | *.jar 27 | *.war 28 | *.ear 29 | *.log 30 | *.tmp 31 | 32 | # Test results 33 | /test-results/ 34 | /test-output/ 35 | 36 | # OS-specific files 37 | .DS_Store 38 | Thumbs.db 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Need to NEVER modify the line endings of this file since it is supposed to be a windows compilation 2 | libs/windows/libsqlite3.a -text 3 | gradlew text eol=lf 4 | gradlew.bat text eol=crlf 5 | gradle/wrapper/gradle-wrapper.jar binary 6 | gradle/wrapper/gradle-wrapper.properties text eol=lf 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ ubuntu-latest, macOS-latest, windows-latest ] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up JDK 17 14 | uses: actions/setup-java@v3 15 | with: 16 | java-version: '17' 17 | distribution: 'adopt' 18 | - name: Validate Gradle wrapper 19 | uses: gradle/actions/wrapper-validation@v3 20 | 21 | - name: 'Run Jdbc, Android, and Native-Linux Tests' 22 | if: matrix.os == 'ubuntu-latest' 23 | run: >- 24 | docker compose build && docker compose run --rm --service-ports setup && 25 | ./gradlew build --stacktrace -PisCI -Pnosign 26 | 27 | - name: 'Run MacOS Tests' 28 | if: matrix.os == 'macOS-latest' 29 | run: ./gradlew :terpal-sql-native:build --stacktrace -PisCI -Pnosign 30 | 31 | - name: 'Run windows tests' 32 | if: matrix.os == 'windows-latest' 33 | run: ./gradlew :terpal-sql-native:mingwX64Test --stacktrace -PisCI -Pnosign 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | build/ 3 | .gradle/ 4 | .kotlin/ 5 | 6 | # IDEA 7 | out/ 8 | .idea/ 9 | .vscode/ 10 | *.iml 11 | 12 | *.db 13 | *.db-shm 14 | *.db-wal 15 | 16 | perf_test 17 | perf_test-shm 18 | perf_test-wal 19 | 20 | build-logic/bin 21 | bin/ 22 | 23 | nohup.out 24 | -------------------------------------------------------------------------------- /build-local.sh: -------------------------------------------------------------------------------- 1 | ./gradlew :terpal-sql-core:build :terpal-sql-jdbc:build -PisLocal 2 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | mavenLocal() 8 | gradlePluginPortal() 9 | } 10 | 11 | dependencies { 12 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.0") 13 | implementation("io.github.gradle-nexus:publish-plugin:1.1.0") 14 | implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.8.20") 15 | 16 | // Override the 1.6.1 dependency coming from kotlin-gradle-plugin 17 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 18 | } -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/nativebuild.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.konan.target.HostManager 2 | 3 | plugins { 4 | id("conventions") 5 | kotlin("multiplatform") 6 | } 7 | 8 | repositories { 9 | //mavenLocal() // Don't include this, it causes all sorts of build horror 10 | mavenCentral() 11 | mavenLocal() 12 | } 13 | 14 | 15 | // When inhereting `nativebuild` put these entries into your local build.gradle.kts. 16 | // ...can they be enabled only here if you don't use kotlin("multiplatform") ??? 17 | //tasks.named { it == "linuxX64Test" }.configureEach { enabled = HostManager.hostIsLinux } 18 | //tasks.named { it == "linkDebugTestLinuxX64" }.configureEach { enabled = HostManager.hostIsLinux } 19 | //tasks.named { it == "mingwX64Test" }.configureEach { enabled = HostManager.hostIsMingw } 20 | //tasks.named { it == "linkDebugTestMingwX64" }.configureEach { enabled = HostManager.hostIsMingw } 21 | 22 | 23 | kotlin { 24 | 25 | val isCI = project.hasProperty("isCI") 26 | // I.e. set this environment variable specifically to true to build (most) targets 27 | val fullLocal = !isCI && System.getenv("TERPAL_FULL_LOCAL")?.toBoolean() ?: false 28 | 29 | if (HostManager.hostIsLinux || fullLocal) { 30 | linuxX64() 31 | iosX64() 32 | iosArm64() 33 | iosSimulatorArm64() 34 | 35 | if (isCI) { 36 | linuxArm64() 37 | 38 | // Need to know about this since we publish the -tooling metadata from 39 | // the linux containers. Although it doesn't build these it needs to know about them. 40 | macosX64() 41 | iosX64() 42 | iosArm64() 43 | watchosArm32() 44 | watchosArm64() 45 | watchosX64() 46 | tvosArm64() 47 | tvosX64() 48 | macosArm64() 49 | iosSimulatorArm64() 50 | mingwX64() 51 | // Terpal-Runtime not published for these yet 52 | //watchosDeviceArm64() 53 | //tvosSimulatorArm64() 54 | //watchosSimulatorArm64() 55 | } 56 | } 57 | 58 | if (HostManager.hostIsMingw || fullLocal) { 59 | mingwX64() 60 | } 61 | 62 | if (HostManager.hostIsMac || fullLocal) { 63 | macosX64() 64 | // Build the other targets only if we are on the CI 65 | if (isCI) { 66 | iosX64() 67 | iosArm64() 68 | watchosArm32() 69 | watchosArm64() 70 | watchosX64() 71 | tvosArm64() 72 | tvosX64() 73 | macosArm64() 74 | iosSimulatorArm64() 75 | // Terpal-Runtime not published for these yet 76 | //watchosSimulatorArm64() 77 | //tvosSimulatorArm64() 78 | //watchosDeviceArm64() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /controller-android/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /controller-android/src/androidMain/com/jetbrains/kmm/shared/Platform.kt: -------------------------------------------------------------------------------- 1 | package com.jetbrains.kmm.shared 2 | 3 | actual class Platform actual constructor() { 4 | actual val platform: String = "Android ${android.os.Build.VERSION.SDK_INT}" 5 | } 6 | -------------------------------------------------------------------------------- /controller-android/src/androidMain/kotlin/io/exoquery/controller/android/AndroidOps.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.android 2 | 3 | import androidx.sqlite.db.SupportSQLiteDatabase 4 | import androidx.sqlite.db.SupportSQLiteOpenHelper 5 | import androidx.sqlite.db.SupportSQLiteStatement 6 | import io.exoquery.controller.CoroutineSession 7 | import io.exoquery.controller.sqlite.Borrowed 8 | import io.exoquery.controller.sqlite.DoublePoolSession 9 | import io.exoquery.controller.sqlite.StatementCachingSession 10 | import io.exoquery.controller.sqlite.TerpalSchema 11 | import kotlinx.coroutines.* 12 | 13 | object EmptyCallback : SupportSQLiteOpenHelper.Callback(1) { 14 | override fun onCreate(db: SupportSQLiteDatabase) = Unit 15 | override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) = Unit 16 | } 17 | 18 | fun TerpalSchema.asSyncCallback(): SupportSQLiteOpenHelper.Callback { 19 | val schema = this@asSyncCallback 20 | return object : SupportSQLiteOpenHelper.Callback(schema.version.toInt()) { 21 | override fun onCreate(db: SupportSQLiteDatabase): Unit { 22 | val session: Connection = 23 | DoublePoolSession( 24 | Borrowed.dummy(StatementCachingSession(db, AndroidLruStatementCache(db, 1))), 25 | true 26 | ) 27 | // Run the schema creation in a context. It is not enough to just create a session with this connection, 28 | // we need to actually do so that in the runActionScoped block it will know there is an existing connection 29 | // on the coroutine context and not attempt to create a new one. 30 | val ctx = AndroidDatabaseController.fromSingleSession(db) 31 | runBlocking(Dispatchers.Unconfined) { 32 | // Note that since this needs to be run on the caller thread (of the code that is calling db.writableDatabase, 33 | // adding `+ Dispatchers.IO` to this context will shift the context away from that and cause a deadlock. 34 | withContext(CoroutineSession(session, AndroidCoroutineContext)) { 35 | schema.create(ctx) 36 | } 37 | } 38 | } 39 | 40 | override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { 41 | val ctx = AndroidDatabaseController.fromSingleSession(db) 42 | runBlocking { schema.migrate(ctx, oldVersion.toLong(), newVersion.toLong()) } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /controller-android/src/androidMain/kotlin/io/exoquery/controller/android/Test.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.android 2 | 3 | import java.util.* 4 | 5 | internal fun julianDateToCalendar(jd: Double?, cal: Calendar): Calendar? { 6 | if (jd == null) { 7 | return null 8 | } 9 | 10 | val yyyy: Int 11 | val dd: Int 12 | val mm: Int 13 | val hh: Int 14 | val mn: Int 15 | val ss: Int 16 | val ms: Int 17 | val A: Int 18 | 19 | val w = jd + 0.5 20 | val Z = w.toInt() 21 | val F = w - Z 22 | 23 | if (Z < 2299161) { 24 | A = Z 25 | } else { 26 | val alpha = ((Z - 1867216.25) / 36524.25).toInt() 27 | A = Z + 1 + alpha - (alpha / 4.0).toInt() 28 | } 29 | 30 | val B = A + 1524 31 | val C = ((B - 122.1) / 365.25).toInt() 32 | val D = (365.25 * C).toInt() 33 | val E = ((B - D) / 30.6001).toInt() 34 | 35 | // month 36 | mm = E - (if (E < 13.5) 1 else 13) 37 | 38 | // year 39 | yyyy = C - (if (mm > 2.5) 4716 else 4715) 40 | 41 | // Day 42 | val jjd = B - D - (30.6001 * E).toInt() + F 43 | dd = jjd.toInt() 44 | 45 | // Hour 46 | val hhd = jjd - dd 47 | hh = (24 * hhd).toInt() 48 | 49 | // Minutes 50 | val mnd = (24 * hhd) - hh 51 | mn = (60 * mnd).toInt() 52 | 53 | // Seconds 54 | val ssd = (60 * mnd) - mn 55 | ss = (60 * ssd).toInt() 56 | 57 | // Milliseconds 58 | val msd = (60 * ssd) - ss 59 | ms = (1000 * msd).toInt() 60 | 61 | cal.set(yyyy, mm - 1, dd, hh, mn, ss) 62 | cal.set(Calendar.MILLISECOND, ms) 63 | 64 | if (yyyy < 1) { 65 | cal.set(Calendar.ERA, GregorianCalendar.BC) 66 | cal.set(Calendar.YEAR, -(yyyy - 1)) 67 | } 68 | 69 | return cal 70 | } 71 | -------------------------------------------------------------------------------- /controller-android/src/androidMain/kotlin/io/exoquery/controller/android/time/TimeHelpers.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.android.time 2 | 3 | import java.time.LocalDate 4 | import java.time.LocalDateTime 5 | import java.time.LocalTime 6 | import java.time.ZoneId 7 | 8 | internal val dayZero = LocalDate.of(1970, 1, 1) 9 | fun java.time.LocalTime.toSqlTime(zoneId: ZoneId) = java.sql.Time(this.atDate(dayZero).atZone(zoneId).toInstant().toEpochMilli()) 10 | fun java.time.LocalDate.toSqlDate(zoneId: ZoneId) = java.sql.Date(this.atStartOfDay(zoneId).toInstant().toEpochMilli()) 11 | fun java.time.LocalDateTime.toSqlTimestamp(zoneId: ZoneId) = java.sql.Timestamp(this.atZone(zoneId).toInstant().toEpochMilli()) 12 | 13 | 14 | //fun LocalDate.toSqlDate(): java.sql.Date = run { 15 | // val date = this 16 | // java.sql.Date(date.getYear() - 1900, date.getMonthValue() -1,date.getDayOfMonth()) 17 | //} 18 | // 19 | //fun LocalTime.toSqlTime(): java.sql.Time = run { 20 | // val time = this 21 | // java.sql.Time(time.getHour(), time.getMinute(), time.getSecond()) 22 | //} 23 | // 24 | //fun LocalDateTime.toSqlTimestamp(): java.sql.Timestamp = run { 25 | // val dateTime = this 26 | // java.sql.Timestamp(dateTime.getYear() - 1900, 27 | // dateTime.getMonthValue() - 1, 28 | // dateTime.getDayOfMonth(), 29 | // dateTime.getHour(), 30 | // dateTime.getMinute(), 31 | // dateTime.getSecond(), 32 | // dateTime.getNano() 33 | // ) 34 | //} 35 | -------------------------------------------------------------------------------- /controller-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 4 | 5 | plugins { 6 | id("conventions") 7 | kotlin("multiplatform") 8 | kotlin("plugin.serialization") version "2.1.0" 9 | id("nativebuild") 10 | } 11 | 12 | version = extra["controllerVersion"].toString() 13 | 14 | kotlin { 15 | jvmToolchain(17) 16 | jvm { 17 | } 18 | 19 | java { 20 | sourceCompatibility = JavaVersion.VERSION_11 21 | targetCompatibility = JavaVersion.VERSION_11 22 | } 23 | 24 | // Enabling this causes: > Querying the mapped value of task ':commonizeNativeDistribution' property 'rootOutputDirectoryProperty$kotlin_gradle_plugin_common' before task ':commonizeNativeDistribution' has completed is not supported 25 | // androidNativeX64() 26 | 27 | sourceSets { 28 | val commonMain by getting { 29 | dependencies { 30 | api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2") 31 | api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2") 32 | api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") 33 | //api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") 34 | implementation("org.jetbrains.kotlinx:atomicfu:0.23.1") 35 | implementation("org.jetbrains:annotations:26.0.2") 36 | } 37 | } 38 | 39 | val commonTest by getting { 40 | //dependencies { 41 | // implementation(kotlin("test")) 42 | // implementation(kotlin("test-common")) 43 | // implementation(kotlin("test-annotations-common")) 44 | //} 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | commonMainApi("org.jetbrains.kotlinx:kotlinx-datetime:0.6.0") 51 | } 52 | 53 | repositories { 54 | mavenCentral() 55 | mavenLocal() 56 | google() 57 | } 58 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/Annotations.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | @RequiresOptIn(message = "This is internal Terpal-SQL API and may change in the future.") 4 | @Retention(AnnotationRetention.BINARY) 5 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 6 | annotation class TerpalSqlInternal 7 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/DecoderAny.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import kotlin.reflect.KClass 4 | 5 | open class DecoderAny( 6 | open override val type: KClass, 7 | open val isNull: (Int, Row) -> Boolean, 8 | open val f: (DecodingContext, Int) -> T?, 9 | ): SqlDecoder() { 10 | override fun isNullable(): Boolean = false 11 | override fun decode(ctx: DecodingContext, index: Int): T { 12 | val value = f(ctx, index) 13 | if (value == null && !isNullable()) { 14 | val msg = 15 | "Got null value for non-nullable column of type ${type.simpleName} at index $index" + 16 | (ctx.columnInfo(index-1)?.let { " (${it.name}:${it.type})" } ?: "") 17 | 18 | throw NullPointerException(msg) 19 | } 20 | return value as T 21 | } 22 | 23 | /** 24 | * Transforms this decoder into another decoder by applying the given function to the decoded value. 25 | * Alias for [map]. 26 | */ 27 | inline fun transformInto(crossinline into: (T) -> R): DecoderAny = 28 | map(into) 29 | 30 | inline fun map(crossinline into: (T) -> R): DecoderAny = 31 | DecoderAny(R::class, isNull) { ctx, index -> into(this.decode(ctx, index)) } 32 | 33 | override fun asNullable(): SqlDecoder = 34 | object: SqlDecoder() { 35 | override fun asNullable(): SqlDecoder = this 36 | override fun isNullable(): Boolean = true 37 | override val type = this@DecoderAny.type 38 | override fun decode(ctx: DecodingContext, index: Int): T? = 39 | if (isNull(index, ctx.row)) 40 | null 41 | else 42 | this@DecoderAny.decode(ctx, index) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/EncoderAny.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import kotlin.reflect.KClass 4 | 5 | open class EncoderAny( 6 | open val dataType: TypeId, 7 | open override val type: KClass, 8 | open val setNull: (Int, Stmt, TypeId) -> Unit, 9 | open val f: (EncodingContext, T, Int) -> Unit 10 | ): SqlEncoder() { 11 | override fun encode(ctx: EncodingContext, value: T, index: Int) = 12 | f(ctx, value, index) 13 | 14 | override fun asNullable(): SqlEncoder = 15 | object: SqlEncoder() { 16 | override val type = this@EncoderAny.type 17 | val jdbcType = this@EncoderAny.dataType 18 | override fun asNullable(): SqlEncoder = this 19 | 20 | override fun encode(ctx: EncodingContext, value: T?, index: Int) = 21 | try { 22 | if (value != null) 23 | this@EncoderAny.encode(ctx, value, index) 24 | else 25 | setNull(index, ctx.stmt, jdbcType) 26 | } catch (e: Throwable) { 27 | throw EncodingException("Error encoding ${type} value: $value at index: $index (whose jdbc-type: ${jdbcType})", e) 28 | } 29 | } 30 | 31 | /** 32 | * Transforms this encoder into another encoder by applying the given function to the value before encoding it. 33 | * Alias for [contramap]. 34 | */ 35 | inline fun transformFrom(crossinline from: (R) -> T): EncoderAny = 36 | contramap(from) 37 | 38 | inline fun contramap(crossinline from: (R) -> T): EncoderAny = 39 | EncoderAny(this@EncoderAny.dataType, R::class, this@EncoderAny.setNull) { ctx, value, i -> this.f(ctx, from(value), i) } 40 | } 41 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/EncodingContext.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import kotlinx.datetime.TimeZone 4 | 5 | data class QueryDebugInfo(val query: String) 6 | 7 | open class EncodingContext(open val session: Session, open val stmt: Stmt, open val timeZone: TimeZone) 8 | open class DecodingContext( 9 | open val session: Session, 10 | open val row: Row, 11 | open val timeZone: TimeZone, 12 | open val startingIndex: StartingIndex, 13 | val columnInfos: List?, 14 | open val debugInfo: QueryDebugInfo? 15 | ) { 16 | /** 17 | * Get the column info for the given index. The index is 1-based since this is the general case for database row-sets. 18 | * (TODO what about the android result-set) 19 | */ 20 | fun columnInfo(index: Int): ColumnInfo? = 21 | columnInfos?.get(index-startingIndex.value) 22 | } 23 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/EncodingException.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | data class EncodingException(val msg: String, val errorCause: Throwable? = null): Exception(msg.toString(), errorCause) { 4 | override fun toString(): String = msg 5 | } 6 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/JsonValue.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class JsonValue(val value: T) -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/Messages.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | object Messages { 4 | 5 | fun catchRethrowColumnInfoExtractError(f:() -> T): T = 6 | try { 7 | f() 8 | } catch (e: Exception) { 9 | throw IllegalStateException( 10 | """Could not extract column information from the row. 11 | This is likely because you attempted to retrieve column information for a row that does not exist. This frequently 12 | happens when you are use a database driver that expects a 1-based index for columns (e.g. JDBC), but the row your context 13 | is using a 0-based index or vice-versa. Make sure the `startingIndex` parameter for your context e.g. JdbcDriver.startingIndex 14 | is set correctly. 15 | ================ 16 | Error: ${e.message} 17 | """.trimMargin(), e) 18 | } 19 | } -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/Model.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(TerpalSqlInternal::class) 2 | 3 | package io.exoquery.controller 4 | 5 | import kotlinx.serialization.* 6 | 7 | @OptIn(ExperimentalSerializationApi::class) 8 | @SerialInfo 9 | @Retention(AnnotationRetention.BINARY) 10 | // TODO this needs to have AnnotationTarget.PROPERTY and not AnnotationTarget.FIELD or AnnotationTarget.VALUE_PARAMETER or else it 11 | // will not be retrieveable with getElementAnnotations. See https://github.com/Kotlin/kotlinx.serialization/issues/1001 for more details. 12 | @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.TYPE) 13 | annotation class SqlJsonValue 14 | 15 | data class ControllerQuery(val sql: String, val params: List>, val resultMaker: KSerializer) 16 | 17 | sealed interface ActionVerb 18 | 19 | data class ControllerAction(val sql: String, val params: List>): ActionVerb 20 | 21 | sealed interface ControllerActionReturning: ActionVerb { 22 | val sql: String 23 | val params: List> 24 | val resultMaker: KSerializer 25 | val returningColumns: List 26 | 27 | data class Row(override val sql: String, override val params: List>, override val resultMaker: KSerializer, override val returningColumns: List): ControllerActionReturning 28 | data class Id(override val sql: String, override val params: List>, override val resultMaker: KSerializer, override val returningColumns: List): ControllerActionReturning { 29 | companion object { 30 | operator fun invoke(sql: String, params: List>, resultMaker: KSerializer, returningColumn: String? = null): ControllerActionReturning.Id { 31 | return ControllerActionReturning.Id(sql, params, resultMaker, listOfNotNull(returningColumn)) 32 | } 33 | } 34 | } 35 | } 36 | 37 | 38 | sealed interface BatchVerb 39 | 40 | data class ControllerBatchAction(val sql: String, val params: Sequence>>): BatchVerb 41 | 42 | sealed interface ControllerBatchActionReturning: BatchVerb { 43 | val sql: String 44 | val params: Sequence>> 45 | val resultMaker: KSerializer 46 | val returningColumns: List 47 | 48 | data class Row(override val sql: String, override val params: Sequence>>, override val resultMaker: KSerializer, override val returningColumns: List): ControllerBatchActionReturning 49 | data class Id(override val sql: String, override val params: Sequence>>, override val resultMaker: KSerializer, override val returningColumns: List): ControllerBatchActionReturning { 50 | companion object { 51 | operator fun invoke(sql: String, params: Sequence>>, resultMaker: KSerializer, returningColumn: String? = null): ControllerBatchActionReturning.Id { 52 | return ControllerBatchActionReturning.Id(sql, params, resultMaker, listOfNotNull(returningColumn)) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/Param.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import kotlinx.serialization.ContextualSerializer 4 | import kotlinx.serialization.ExperimentalSerializationApi as SerApi 5 | import kotlinx.serialization.SerializationStrategy 6 | import kotlinx.serialization.builtins.serializer 7 | import kotlinx.serialization.serializer 8 | //import java.math.BigDecimal 9 | import kotlinx.datetime.* 10 | import kotlin.jvm.JvmName 11 | import kotlin.reflect.KClass 12 | 13 | 14 | // Note that T can't extend Any because then T will not be allowed to be null when it is being decoded 15 | // that is why we have KClass<*> and not KClass 16 | 17 | data class StatementParam(val serializer: SerializationStrategy, val cls: KClass<*>, val value: T?) 18 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/jdbc/CoroutineTransaction.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import kotlin.coroutines.AbstractCoroutineContextElement 4 | import kotlin.coroutines.CoroutineContext 5 | 6 | class CoroutineTransaction(private var completed: Boolean = false) : AbstractCoroutineContextElement(CoroutineTransaction) { 7 | companion object Key : CoroutineContext.Key 8 | val incomplete: Boolean 9 | get() = !completed 10 | 11 | fun complete() { 12 | completed = true 13 | } 14 | override fun toString(): String = "CoroutineTransaction(completed=$completed)" 15 | } -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/sqlite/SqliteSession.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | abstract class SqliteSession( 4 | val conn: Session, 5 | val statementCacheCapacity: Int, 6 | val createStatement: (String) -> Stmt, 7 | val resetStatement: (Stmt) -> Unit, 8 | val finalizeStatement: (Stmt) -> Unit, 9 | // Use an the default implementation of StatementCache but allow overrides e.g. for Android which has a different LruCache 10 | override val cache: StatementCache = TerpalLruStatementCache(statementCacheCapacity, createStatement, resetStatement, finalizeStatement) 11 | ): 12 | StatementCachingSession(conn, cache) -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/sqlite/StatementCachingSession.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | /** 4 | * Since on mobile platforms creating SQL PreparedStatements is expensive, we cache them. 5 | * This is a base-class for a cached-session type to be used with the DoublePool. 6 | * This works by wrapping a session and a cache to check/create statements from. 7 | * (Note that the actual aquisition of new statements is done within in the StatementCache 8 | * implementors since they are the ones that will deal with a real Stmt type that actually have 9 | * session-creation semantics). See TerpalLruStatementCache and SqliteSession 10 | * for examples of how to use this. 11 | */ 12 | open class StatementCachingSession(open val session: Session, open val cache: StatementCache) { 13 | open fun createStatement(sql: String): Stmt = cache.getOrCreate(sql) 14 | } 15 | 16 | interface StatementCache { 17 | fun getOrCreate(sql: String): Stmt 18 | } 19 | -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/sqlite/TerpalLruStatementCache.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | import kotlinx.atomicfu.locks.reentrantLock 4 | import kotlinx.atomicfu.locks.withLock 5 | 6 | class TerpalLruStatementCache(val capacity: Int, val createStmt: (String) -> Stmt, val preparedFromCache: (Stmt) -> Unit, val evictStatement: (Stmt) -> Unit): StatementCache { 7 | // Note that since the only interaction with the LRU cache in the whole app here is via getOrCreate which has it's own 8 | // lock, any locks inside the LRU cache themselves are not needed. For the sake of performance optimization look 9 | // if removing them has any benifit. 10 | private val cacheRef by lazy { 11 | LruCache(capacity) { _, sql, stmt, _ -> evictStatement(stmt) } 12 | } // Don't create it in case the capacity is zero 13 | 14 | private val lock = reentrantLock() 15 | 16 | override fun getOrCreate(sql: String): Stmt = 17 | // if instructed to not cache anything just create the statement 18 | if (capacity == 0) { 19 | createStmt(sql) 20 | } else { 21 | // actually init the cache before the lock 22 | val cache = cacheRef 23 | 24 | lock.withLock { 25 | val stmt = cache.get(sql) 26 | if (stmt != null) { 27 | stmt 28 | } else { 29 | val newStmt = createStmt(sql) 30 | cache.put(sql, newStmt) 31 | preparedFromCache(newStmt) 32 | newStmt 33 | } 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /controller-core/src/commonMain/kotlin/io/exoquery/controller/sqlite/TerpalSchema.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | import io.exoquery.controller.Controller 4 | 5 | interface TerpalSchema { 6 | val version: Long 7 | 8 | /** 9 | * Use [driver] to create the schema from scratch. Assumes no existing database state. 10 | */ 11 | suspend fun create(driver: Controller<*>): T 12 | 13 | /** 14 | * Use [driver] to migrate from schema [oldVersion] to [newVersion]. 15 | * Each of the [callbacks] are executed during the migration whenever the upgrade to the version specified by 16 | * [CallAfterVersion.afterVersion] has been completed. 17 | */ 18 | suspend fun migrate(driver: Controller<*>, oldVersion: Long, newVersion: Long, vararg callbacks: CallAfterVersion): T 19 | } 20 | 21 | /** 22 | * Represents a block of code [block] that should be executed during a migration after the migration 23 | * has finished migrating to [afterVersion]. 24 | */ 25 | class CallAfterVersion( 26 | val afterVersion: Long, 27 | val block: suspend (Controller<*>) -> Unit, 28 | ) 29 | 30 | object EmptyTerpalSchema : TerpalSchema { 31 | override val version: Long = 0 32 | override suspend fun create(driver: Controller<*>) { 33 | } 34 | override suspend fun migrate(driver: Controller<*>, oldVersion: Long, newVersion: Long, vararg callbacks: CallAfterVersion) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /controller-core/src/jvmMain/kotlin/sqlite/DoublePoolBase.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | actual fun getNumProcessorsOnPlatform(): Int = Runtime.getRuntime().availableProcessors() 4 | -------------------------------------------------------------------------------- /controller-core/src/nativeMain/kotlin/io/exoquery/controller/sqlite/DoublePoolBase.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.sqlite 2 | 3 | import kotlin.experimental.ExperimentalNativeApi 4 | 5 | @OptIn(ExperimentalNativeApi::class) 6 | actual fun getNumProcessorsOnPlatform(): Int = Platform.getAvailableProcessors() 7 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/JavaEncoding.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import io.exoquery.controller.SqlDecoder 4 | import io.exoquery.controller.SqlEncoder 5 | import io.exoquery.controller.SqlEncoding 6 | import io.exoquery.controller.TimeEncoding 7 | import java.time.* 8 | import java.math.BigDecimal 9 | import java.util.Date 10 | import java.util.UUID 11 | 12 | interface JavaTimeEncoding: TimeEncoding { 13 | val JDateEncoder: SqlEncoder 14 | val JLocalDateEncoder: SqlEncoder 15 | val JLocalTimeEncoder: SqlEncoder 16 | val JLocalDateTimeEncoder: SqlEncoder 17 | val JZonedDateTimeEncoder: SqlEncoder 18 | val JInstantEncoder: SqlEncoder 19 | val JOffsetTimeEncoder: SqlEncoder 20 | val JOffsetDateTimeEncoder: SqlEncoder 21 | 22 | val JDateDecoder: SqlDecoder 23 | val JLocalDateDecoder: SqlDecoder 24 | val JLocalTimeDecoder: SqlDecoder 25 | val JLocalDateTimeDecoder: SqlDecoder 26 | val JZonedDateTimeDecoder: SqlDecoder 27 | val JInstantDecoder: SqlDecoder 28 | val JOffsetTimeDecoder: SqlDecoder 29 | val JOffsetDateTimeDecoder: SqlDecoder 30 | } 31 | 32 | interface JavaUuidEncoding { 33 | val JUuidEncoder: SqlEncoder 34 | val JUuidDecoder: SqlDecoder 35 | } 36 | 37 | interface JavaLegacyDateEncoding { 38 | val DateEncoder: SqlEncoder 39 | val DateDecoder: SqlDecoder 40 | } 41 | 42 | interface JavaBigDecimalEncoding { 43 | val BigDecimalEncoder: SqlEncoder 44 | val BigDecimalDecoder: SqlDecoder 45 | } 46 | 47 | interface JavaSqlEncoding: 48 | SqlEncoding, 49 | JavaTimeEncoding, 50 | JavaUuidEncoding { 51 | 52 | override fun computeEncoders(): Set> = 53 | super.computeEncoders() + 54 | setOf( 55 | JUuidEncoder, 56 | JDateEncoder, 57 | JLocalDateEncoder, 58 | JLocalTimeEncoder, 59 | JLocalDateTimeEncoder, 60 | JZonedDateTimeEncoder, 61 | JInstantEncoder, 62 | JOffsetTimeEncoder, 63 | JOffsetDateTimeEncoder 64 | ) 65 | 66 | override fun computeDecoders(): Set> = 67 | super.computeDecoders() + 68 | setOf( 69 | JUuidDecoder, 70 | JDateDecoder, 71 | JLocalDateDecoder, 72 | JLocalTimeDecoder, 73 | JLocalDateTimeDecoder, 74 | JZonedDateTimeDecoder, 75 | JInstantDecoder, 76 | JOffsetTimeDecoder, 77 | JOffsetDateTimeDecoder 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/RunFunctions.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller 2 | 3 | import io.exoquery.controller.jdbc.JdbcController 4 | import io.exoquery.controller.jdbc.JdbcExecutionOptions 5 | 6 | suspend fun ControllerQuery.runOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.run(this, options) 7 | suspend fun ControllerQuery.streamOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.stream(this, options) 8 | suspend fun ControllerQuery.runRawOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.runRaw(this, options) 9 | suspend fun ControllerAction.runOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.run(this, options) 10 | suspend fun ControllerActionReturning.runOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.run(this, options) 11 | suspend fun ControllerBatchAction.runOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.run(this, options) 12 | suspend fun ControllerBatchActionReturning.runOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.run(this, options) 13 | suspend fun ControllerBatchActionReturning.streamOn(ctx: JdbcController, options: JdbcExecutionOptions) = ctx.stream(this, options) 14 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/HikariHelper.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import com.zaxxer.hikari.HikariDataSource 5 | 6 | object HikariHelper { 7 | fun makeDataSource(configPrefix: String): HikariDataSource { 8 | val factory = ConfigFactory.load(this::class.java.classLoader) 9 | val config = 10 | if (factory.hasPath(configPrefix)) 11 | factory.getConfig(configPrefix) 12 | else 13 | ConfigFactory.empty() 14 | return JdbcContextConfig(config).dataSource() 15 | } 16 | } 17 | 18 | fun JdbcControllers.Postgres.Companion.fromConfig(prefix: String) = 19 | JdbcControllers.Postgres(HikariHelper.makeDataSource(prefix)) 20 | 21 | fun JdbcControllers.PostgresLegacy.Companion.fromConfig(prefix: String) = 22 | JdbcControllers.PostgresLegacy(HikariHelper.makeDataSource(prefix)) 23 | 24 | fun JdbcControllers.H2.Companion.fromConfig(prefix: String) = 25 | JdbcControllers.H2(HikariHelper.makeDataSource(prefix)) 26 | 27 | fun JdbcControllers.Mysql.Companion.fromConfig(prefix: String) = 28 | JdbcControllers.Mysql(HikariHelper.makeDataSource(prefix)) 29 | 30 | fun JdbcControllers.Sqlite.Companion.fromConfig(prefix: String) = 31 | JdbcControllers.Sqlite(HikariHelper.makeDataSource(prefix)) 32 | 33 | fun JdbcControllers.SqlServer.Companion.fromConfig(prefix: String) = 34 | JdbcControllers.SqlServer(HikariHelper.makeDataSource(prefix)) 35 | 36 | fun JdbcControllers.Oracle.Companion.fromConfig(prefix: String) = 37 | JdbcControllers.Oracle(HikariHelper.makeDataSource(prefix)) 38 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcContextConfig.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import java.util.* 4 | 5 | import com.typesafe.config.Config 6 | import com.zaxxer.hikari.HikariConfig 7 | import com.zaxxer.hikari.HikariDataSource 8 | import java.util.Properties 9 | 10 | data class JdbcContextConfig(val config: Config) { 11 | fun configProperties(): Properties { 12 | val p = Properties() 13 | for ((key, value) in config.entrySet()) { 14 | p.setProperty(key, value.unwrapped().toString()) 15 | } 16 | return p 17 | } 18 | 19 | fun dataSource(): HikariDataSource { 20 | return try { 21 | HikariDataSource(HikariConfig(configProperties())) 22 | } catch (ex: Exception) { 23 | throw IllegalStateException("Failed to load data source", ex) 24 | } 25 | } 26 | } 27 | 28 | /* 29 | // scala 30 | case class JdbcContextConfig(config: Config) { 31 | 32 | def configProperties = { 33 | import scala.jdk.CollectionConverters._ 34 | val p = new Properties 35 | for (entry <- config.entrySet.asScala) 36 | p.setProperty(entry.getKey, entry.getValue.unwrapped.toString) 37 | p 38 | } 39 | 40 | def dataSource = 41 | try 42 | new HikariDataSource(new HikariConfig(configProperties)) 43 | catch { 44 | case NonFatal(ex) => 45 | throw new IllegalStateException("Failed to load data source", ex) 46 | } 47 | } 48 | 49 | */ -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcDecoders.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import io.exoquery.controller.DecoderAny 4 | import io.exoquery.controller.SqlDecoder 5 | import java.sql.Connection 6 | import java.sql.ResultSet 7 | import kotlin.reflect.KClass 8 | 9 | /** Represents a Jdbc Decoder with a nullable or non-nullable output value */ 10 | typealias JdbcDecoder = SqlDecoder 11 | 12 | class JdbcDecoderAny( 13 | override val type: KClass, 14 | override val f: (JdbcDecodingContext, Int) -> T? 15 | ): DecoderAny( 16 | type, 17 | { index, row -> 18 | row.getObject(index) 19 | row.wasNull() 20 | }, 21 | f 22 | ) { 23 | } 24 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcEncodingContext.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import io.exoquery.controller.DecodingContext 4 | import io.exoquery.controller.EncodingContext 5 | import java.sql.Connection 6 | import java.sql.PreparedStatement 7 | import java.sql.ResultSet 8 | import java.sql.Types 9 | import java.time.Instant 10 | import java.time.ZoneOffset 11 | import java.util.* 12 | 13 | typealias JdbcEncodingContext = EncodingContext 14 | typealias JdbcDecodingContext = DecodingContext 15 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcExecutionOptions.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import java.sql.Connection 4 | import java.sql.PreparedStatement 5 | import java.sql.ResultSet 6 | 7 | /** 8 | * Jdbc execution options are designed to provide maximum flexibility so that users can reach down into the JDBC stack 9 | * and set settings as appropriate. The first three options are for regular users to be able to set timings. 10 | * The last three are for power-users in order to control the preparation of the connection, statement and result set. 11 | * The queryTimeout and fetchSize settings are applied on the JDBC PreparedStatement when it is created. 12 | * The sessionTimeout is applied on the JDBC Connection when it is created. The sessionTimeout, queryTimeout and fetchSize 13 | * will be applied to a connection even when prepareConnection and prepareStatement are used so be sure to set the first three 14 | * to `null` if you are doing the work yourself via the latter prepareConnection and prepareStatement options. 15 | * 16 | * Note that if you are inside of a transaction 17 | * it will not create a new connection for every statement, usually only the first in the `transaction` block one will create the statement 18 | * and subsequent statements will reuse it. For example: 19 | * ``` 20 | * ctx.transaction { 21 | * insert(joe).run() // <- only this will create a new connection (applying the sessionTimeout and prepareConnection) 22 | * insert(jim).run() 23 | * } 24 | * ``` 25 | */ 26 | data class JdbcExecutionOptions( 27 | val sessionTimeout: Int? = null, 28 | val fetchSize: Int? = null, 29 | val queryTimeout: Int? = null, 30 | val prepareConnection: (Connection) -> Connection = { it }, 31 | val prepareStatement: (PreparedStatement) -> PreparedStatement = { it }, 32 | val prepareResult: (ResultSet) -> ResultSet = { it } 33 | ) { 34 | companion object { 35 | fun Default() = JdbcExecutionOptions() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/JdbcUuidStringEncoding.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import io.exoquery.controller.SqlDecoder 4 | import io.exoquery.controller.SqlEncoder 5 | import io.exoquery.controller.JavaUuidEncoding 6 | import java.sql.Connection 7 | import java.sql.PreparedStatement 8 | import java.sql.ResultSet 9 | import java.sql.Types 10 | import java.util.* 11 | 12 | object JdbcUuidStringEncoding: JavaUuidEncoding { 13 | override val JUuidEncoder: SqlEncoder = 14 | JdbcEncoderAny(Types.VARCHAR, UUID::class) { ctx, v, i -> ctx.stmt.setString(i, v.toString()) } 15 | 16 | override val JUuidDecoder: SqlDecoder = 17 | //JdbcDecoderAny.fromFunction { ctx, i -> UUID.fromString(ctx.row.getString(i)) } 18 | JdbcDecoderAny(UUID::class) { ctx, i -> UUID.fromString(ctx.row.getString(i)) } 19 | } 20 | 21 | object JdbcUuidObjectEncoding: JavaUuidEncoding { 22 | override val JUuidEncoder: SqlEncoder = 23 | //JdbcEncoderAny.fromFunction(java.sql.Types.OTHER) { ctx, v, i -> ctx.stmt.setObject(i, v) } 24 | JdbcEncoderAny(Types.OTHER, UUID::class) { ctx, v, i -> ctx.stmt.setObject(i, v) } 25 | 26 | override val JUuidDecoder: SqlDecoder = 27 | //JdbcDecoderAny.fromFunction { ctx, i -> ctx.row.getObject(i, UUID::class.java) } 28 | JdbcDecoderAny(UUID::class) { ctx, i -> ctx.row.getObject(i, UUID::class.java) } 29 | } 30 | -------------------------------------------------------------------------------- /controller-jdbc/src/main/kotlin/io/exoquery/controller/jdbc/SwitchingContextSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.jdbc 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.SerialDescriptor 5 | import kotlinx.serialization.encoding.Decoder 6 | import kotlinx.serialization.encoding.Encoder 7 | import java.sql.PreparedStatement 8 | 9 | interface SwitchingContextSerializer: KSerializer { 10 | val alternate: KSerializer 11 | val encoder: Session.(T, Int) -> Unit 12 | 13 | override val descriptor: SerialDescriptor get() = alternate.descriptor 14 | override fun deserialize(decoder: Decoder): T = alternate.deserialize(decoder) 15 | override fun serialize(encoder: Encoder, value: T) = alternate.serialize(encoder, value) 16 | } 17 | 18 | data class SwitchingJdbcSerializer( 19 | override val alternate: KSerializer, 20 | override val encoder: PreparedStatement.(T, Int) -> Unit 21 | ): SwitchingContextSerializer { 22 | companion object { 23 | operator fun invoke(encoder: PreparedStatement.(T, Int) -> Unit) = 24 | SwitchingJdbcSerializer(UnusedSerializer(), encoder) 25 | } 26 | } 27 | 28 | class UnusedSerializer: KSerializer { 29 | override val descriptor: SerialDescriptor = error("Cannot use a UnusedSerializer instance") 30 | override fun deserialize(decoder: Decoder): T = error("Cannot call `deserialize` from UnusedSerializer instance") 31 | override fun serialize(encoder: Encoder, value: T) = error("Cannot call `serialize` from UnusedSerializer instance") 32 | } 33 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/delight/SqlCursorExt.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.delight 2 | 3 | import app.cash.sqldelight.db.QueryResult 4 | import app.cash.sqldelight.db.SqlCursor 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | 8 | fun SqlCursor.awaitAll(mapper: (SqlCursor) -> T): QueryResult> { 9 | val cursor = this 10 | val first = cursor.next() 11 | val result = mutableListOf() 12 | 13 | // If the cursor isn't async, we want to preserve the blocking semantics and execute it synchronously 14 | return when (first) { 15 | is QueryResult.AsyncValue -> { 16 | QueryResult.AsyncValue { 17 | if (first.await()) result.add(mapper(cursor)) else return@AsyncValue result 18 | while (cursor.next().await()) result.add(mapper(cursor)) 19 | result 20 | } 21 | } 22 | 23 | is QueryResult.Value -> { 24 | if (first.value) 25 | result.add(mapper(cursor)) 26 | else 27 | return QueryResult.Value(result) 28 | 29 | while (cursor.next().value) result.add(mapper(cursor)) 30 | QueryResult.Value(result.toList()) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/delight/SqlDelightContextExt.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.delight 2 | 3 | import app.cash.sqldelight.driver.native.NativeSqliteDriver 4 | import io.exoquery.controller.ControllerAction 5 | import io.exoquery.controller.ControllerQuery 6 | 7 | fun ControllerQuery.runOnDelight(ctx: SqlDelightController, sqlDelightId: Int? = null) = ctx.runToResult(this, sqlDelightId) 8 | fun ControllerAction.runOnDelight(ctx: SqlDelightController, sqlDelightId: Int? = null) = ctx.runToResult(this, sqlDelightId) 9 | 10 | fun ControllerQuery.runOnDriver(ctx: NativeSqliteDriver, sqlDelightId: Int? = null) = SqlDelightController(ctx).runToResult(this, sqlDelightId) 11 | fun ControllerAction.runOnDriver(ctx: NativeSqliteDriver, sqlDelightId: Int? = null) = SqlDelightController(ctx).runToResult(this, sqlDelightId) 12 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/delight/SqlDelightController.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.delight 2 | 3 | import app.cash.sqldelight.db.QueryResult 4 | import app.cash.sqldelight.driver.native.NativeSqliteDriver 5 | import io.exoquery.controller.* 6 | import io.exoquery.controller.native.NativeEncodingConfig 7 | import io.exoquery.controller.sqlite.SqliteCursorWrapper 8 | import io.exoquery.controller.sqlite.SqliteSqlEncoding 9 | import io.exoquery.controller.sqlite.SqliteStatementWrapper 10 | import io.exoquery.controller.sqlite.Unused 11 | 12 | class SqlDelightController( 13 | val database: NativeSqliteDriver, 14 | override val encodingConfig: EncodingConfig = NativeEncodingConfig() 15 | ): WithEncoding { 16 | // SqlDelight does not expose the Sqliter cursor directly so there is no way to get column names or types 17 | override fun extractColumnInfo(row: SqliteCursorWrapper): List? = null 18 | 19 | override val encodingApi: SqlEncoding = SqliteSqlEncoding 20 | 21 | override val allEncoders by lazy { encodingApi.computeEncoders() + encodingConfig.additionalEncoders } 22 | override val allDecoders by lazy { encodingApi.computeDecoders() + encodingConfig.additionalDecoders } 23 | 24 | fun runToResult(query: ControllerAction, sqlDelightId: Int?): QueryResult = 25 | database.execute( 26 | sqlDelightId, 27 | query.sql, 28 | query.params.size, 29 | { prepare(DelightStatementWrapper.fromDelightStatement(this), Unused, query.params) } 30 | ) 31 | 32 | fun runToResult(query: ControllerQuery, sqlDelightId: Int?): QueryResult> = 33 | database.executeQuery( 34 | sqlDelightId, 35 | query.sql, 36 | { cursor -> cursor.awaitAll { cursor -> query.resultMaker.makeExtractor(null).invoke(Unused, DelightCursorWrapper.fromDelightCursor(cursor)) } }, 37 | query.params.size, 38 | { prepare(DelightStatementWrapper.fromDelightStatement(this), Unused, query.params) } 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/delight/SqlDelightEncoding.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.delight 2 | 3 | import app.cash.sqldelight.db.SqlCursor 4 | import app.cash.sqldelight.db.SqlPreparedStatement 5 | import co.touchlab.sqliter.Cursor 6 | import co.touchlab.sqliter.Statement 7 | import io.exoquery.controller.sqlite.SqliteCursorWrapper 8 | import io.exoquery.controller.sqlite.SqliteStatementWrapper 9 | 10 | interface DelightStatementWrapper: SqliteStatementWrapper { 11 | companion object { 12 | operator fun invoke(stmt: Statement): StatementWrapper = StatementWrapper(stmt) 13 | fun fromDelightStatement(stmt: SqlPreparedStatement): DelightStatementWrapper = 14 | object: DelightStatementWrapper { 15 | override fun bindBytes(index: Int, bytes: ByteArray) = stmt.bindBytes(index, bytes) 16 | override fun bindLong(index: Int, long: Long) = stmt.bindLong(index, long) 17 | override fun bindDouble(index: Int, double: Double) = stmt.bindDouble(index, double) 18 | override fun bindString(index: Int, string: String) = stmt.bindString(index, string) 19 | override fun bindNull(index: Int) = stmt.bindBytes(index, null) 20 | } 21 | } 22 | } 23 | 24 | data class StatementWrapper(val stmt: Statement): DelightStatementWrapper { 25 | override fun bindBytes(index: Int, bytes: ByteArray) = stmt.bindBlob(index, bytes) 26 | override fun bindLong(index: Int, long: Long) = run { 27 | println("------------- Binding long $long at index: $index") 28 | stmt.bindLong(index, long) 29 | } 30 | override fun bindDouble(index: Int, double: Double) = stmt.bindDouble(index, double) 31 | override fun bindString(index: Int, string: String) = stmt.bindString(index, string) 32 | override fun bindNull(index: Int) = stmt.bindNull(index) 33 | } 34 | 35 | interface DelightCursorWrapper: SqliteCursorWrapper { 36 | companion object { 37 | operator fun invoke(cursor: Cursor): CursorWrapper = CursorWrapper(cursor) 38 | fun fromDelightCursor(cursor: SqlCursor): DelightCursorWrapper = 39 | object: DelightCursorWrapper { 40 | override fun getString(index: Int) = cursor.getString(index) 41 | override fun getLong(index: Int) = cursor.getLong(index) 42 | override fun getBytes(index: Int) = cursor.getBytes(index) 43 | override fun getDouble(index: Int) = cursor.getDouble(index) 44 | override fun isNull(index: Int) = cursor.getBytes(index) == null 45 | } 46 | } 47 | } 48 | 49 | data class CursorWrapper(val cursor: Cursor): DelightCursorWrapper { 50 | fun next() = cursor.next() 51 | override fun getString(index: Int) = cursor.getString(index) 52 | override fun getLong(index: Int) = cursor.getLong(index) 53 | override fun getBytes(index: Int) = cursor.getBytes(index) 54 | override fun getDouble(index: Int) = cursor.getDouble(index) 55 | override fun isNull(index: Int) = cursor.isNull(index) 56 | } 57 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/native/NativeEncodingConfig.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.native 2 | 3 | import io.exoquery.controller.EncodingConfig 4 | import io.exoquery.controller.SqlDecoder 5 | import io.exoquery.controller.SqlEncoder 6 | import io.exoquery.controller.sqlite.SqliteCursorWrapper 7 | import io.exoquery.controller.sqlite.SqliteStatementWrapper 8 | import io.exoquery.controller.sqlite.Unused 9 | import kotlinx.datetime.TimeZone 10 | import kotlinx.serialization.json.Json 11 | import kotlinx.serialization.modules.EmptySerializersModule 12 | import kotlinx.serialization.modules.SerializersModule 13 | 14 | data class NativeEncodingConfig( 15 | override val additionalEncoders: Set> = setOf(), 16 | override val additionalDecoders: Set> = setOf(), 17 | override val json: Json = Json, 18 | override val module: SerializersModule = EmptySerializersModule(), 19 | override val timezone: TimeZone = TimeZone.currentSystemDefault(), 20 | override val debugMode: Boolean = false 21 | ) : EncodingConfig 22 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/native/SchemaOps.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.native 2 | 3 | import io.exoquery.controller.Controller 4 | import io.exoquery.controller.sqlite.CallAfterVersion 5 | import io.exoquery.controller.sqlite.TerpalSchema 6 | import kotlinx.coroutines.runBlocking 7 | 8 | fun TerpalSchema.toCreateCallbackSync(driver: Controller<*>): T = runBlocking { create(driver) } 9 | fun TerpalSchema.toMigrateCallbackSync(driver: Controller<*>, oldVersion: Long, newVersion: Long, vararg callbacks: CallAfterVersion): T = 10 | runBlocking { migrate(driver, oldVersion, newVersion, *callbacks) } 11 | -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/native/SimplePool.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.native 2 | 3 | import co.touchlab.sqliter.DatabaseConnection 4 | import co.touchlab.sqliter.DatabaseManager 5 | import co.touchlab.sqliter.Statement 6 | import io.exoquery.controller.sqlite.* 7 | 8 | sealed interface SqliterPoolType { 9 | data class SingleConnection(val db: DatabaseManager): SqliterPoolType 10 | data class MultiConnection(val db: DatabaseManager, val numReaders: Int): SqliterPoolType 11 | data class Wrapped(val conn: DatabaseConnection): SqliterPoolType 12 | } 13 | 14 | fun createConnection(type: SqliterPoolType, statementCacheCapacity: Int, isWritable: Boolean) = 15 | when (type) { 16 | is SqliterPoolType.MultiConnection -> { 17 | val conn = type.db.createMultiThreadedConnection() 18 | // NOTE: query_only=1 is not actually used for WAL mode to determine what can be simultaneous 19 | // and it is not needed to attain concurrent read access. 20 | // If you use it doing Statement.resetStatement will throw an exception beccause the statement 21 | // could technically be used for a write operation. 22 | //if (!isWritable) conn.withStatement("PRAGMA query_only = 1") { execute() } 23 | SqliterSession(conn, statementCacheCapacity) 24 | } 25 | is SqliterPoolType.SingleConnection -> { 26 | val conn = type.db.createMultiThreadedConnection() 27 | // See note on query_only=1 above 28 | //if (!isWritable) conn.withStatement("PRAGMA query_only = 1") { execute() } 29 | SqliterSession(conn, statementCacheCapacity) 30 | } 31 | is SqliterPoolType.Wrapped -> 32 | SqliterSession(type.conn, statementCacheCapacity) 33 | } 34 | 35 | class SqliterPool(type: SqliterPoolType, val statementCacheCapacity: Int): 36 | DoublePoolBase, Unit>( 37 | when(type) { 38 | is SqliterPoolType.SingleConnection, is SqliterPoolType.Wrapped -> DoublePoolType.Single 39 | is SqliterPoolType.MultiConnection -> DoublePoolType.Multi(type.numReaders) 40 | }, 41 | { createConnection(type, statementCacheCapacity, true) }, 42 | { createConnection(type, statementCacheCapacity, false) }, 43 | {}, {}, 44 | { it.session.close() } 45 | ) 46 | 47 | class SqliterSession(conn: DatabaseConnection, statementCacheCapacity: Int): 48 | SqliteSession(conn, statementCacheCapacity, { conn.createStatement(it) }, { it.resetAndClear() }, { it.finalizeStatement() }) -------------------------------------------------------------------------------- /controller-native/src/commonMain/kotlin/io/exoquery/controller/native/SqliterExt.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.controller.native 2 | 3 | import co.touchlab.sqliter.DatabaseManager 4 | import co.touchlab.sqliter.Statement 5 | 6 | fun Statement.resetAndClear() { 7 | resetStatement() 8 | clearBindings() 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | mysql: 5 | image: mysql/mysql-server:8.0.23 # use this because it supports ARM64 architecture for M1 Mac 6 | command: --default-authentication-plugin=mysql_native_password 7 | ports: 8 | - "33306:3306" 9 | environment: 10 | - MYSQL_ROOT_PASSWORD=root 11 | - MYSQL_ALLOW_EMPTY_PASSWORD=yes 12 | - MYSQL_ROOT_HOST=% 13 | 14 | setup: 15 | build: 16 | context: . 17 | dockerfile: ./scripts/Dockerfile-setup 18 | depends_on: 19 | - oracle 20 | - sqlserver 21 | links: 22 | - mysql:mysql 23 | - sqlserver:sqlserver 24 | # - oracle:oracle 25 | volumes: 26 | - ./:/app:delegated 27 | command: 28 | - ./scripts/setup_local.sh 29 | 30 | sqlserver: 31 | image: mcr.microsoft.com/mssql/server:2022-CU13-ubuntu-22.04 # use this because it supports ARM64 architecture for M1 Mac 32 | user: root 33 | ports: 34 | - "31433:1433" 35 | environment: 36 | - ACCEPT_EULA=Y 37 | - MSSQL_SA_PASSWORD=ExoQueryRocks! 38 | healthcheck: 39 | test: /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P "$$MSSQL_SA_PASSWORD" -Q "SELECT 1" || exit 1 40 | interval: 30s 41 | timeout: 30s 42 | retries: 3 43 | 44 | oracle: 45 | image: quillbuilduser/oracle-18-xe-micro-sq 46 | ports: 47 | - "31521:1521" 48 | # Opatch is an internal java-based daemon in the Oracle container that updates components, don't really need it here. Reduce it's memory settings. 49 | environment: 50 | - OPATCH_JRE_MEMORY_OPTIONS=-Xms128m -Xmx256m -XX:PermSize=16m -XX:MaxPermSize=32m -Xss1m 51 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExoQuery/terpal-sql/6f1ed03e340cbc1290e17031bc407d10d860782a/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | terpal.io -------------------------------------------------------------------------------- /docs/coverpage.md: -------------------------------------------------------------------------------- 1 | # Terpal SQL 2 | 3 | > Brainlessly Simple SQL templating for Kotlin Multiplatform 4 | 5 |

6 | 7 |

8 | 9 | 10 | [Get Started](#main) 11 | [GitHub](https://github.com/ExoQuery/terpal-sql) 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Dterpal.trace.wrappers=true 2 | kotlin.native.cacheKind.linuxX64=none 3 | android.useAndroidX=true 4 | android.enableJetifier=false 5 | kotlin.mpp.import.enableKgpDependencyResolution=false 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExoQuery/terpal-sql/6f1ed03e340cbc1290e17031bc407d10d860782a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /libs/note.txt: -------------------------------------------------------------------------------- 1 | NOTE: 2 | sqlite3.def was taken from the sqldelight library. 3 | 4 | libsqlite3.a was originally also from the sqldelight library, but it was replaced 5 | with a newer version from: https://packages.msys2.org/package/mingw-w64-x86_64-sqlite3 6 | The version is: 3.46.1-1 7 | -------------------------------------------------------------------------------- /libs/windows/libsqlite3.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExoQuery/terpal-sql/6f1ed03e340cbc1290e17031bc407d10d860782a/libs/windows/libsqlite3.a -------------------------------------------------------------------------------- /local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Mon Mar 31 22:13:45 EDT 2025 8 | sdk.dir=/usr/lib/android-sdk 9 | -------------------------------------------------------------------------------- /publish-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./gradlew :controller-core:publishToMavenLocal \ 4 | :controller-jdbc:publishToMavenLocal \ 5 | :controller-android:publishToMavenLocal \ 6 | :controller-native:publishToMavenLocal \ 7 | :terpal-sql-core:publishToMavenLocal \ 8 | :terpal-sql-jdbc:publishToMavenLocal \ 9 | :terpal-sql-native:publishToMavenLocal \ 10 | :terpal-sql-android:publishToMavenLocal 11 | -------------------------------------------------------------------------------- /scripts/Dockerfile-setup: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye 2 | MAINTAINER gustavo.amigo@gmail.com 3 | 4 | RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 5 | ca-certificates \ 6 | curl \ 7 | mariadb-client \ 8 | netcat \ 9 | postgresql-client \ 10 | python \ 11 | sqlite3 \ 12 | tar \ 13 | apt-transport-https \ 14 | locales \ 15 | default-jre \ 16 | apt-utils 17 | 18 | ADD https://repo1.maven.org/maven2/sqlline/sqlline/1.12.0/sqlline-1.12.0-jar-with-dependencies.jar /sqlline/sqlline.jar 19 | ADD https://repo1.maven.org/maven2/com/oracle/ojdbc/ojdbc8/19.3.0.0/ojdbc8-19.3.0.0.jar /sqlline/ojdbc.jar 20 | 21 | RUN curl https://packages.microsoft.com/keys/microsoft.asc > /etc/apt/trusted.gpg.d/microsoft.asc && \ 22 | curl https://packages.microsoft.com/config/debian/11/prod.list > /etc/apt/sources.list.d/mssql-release.list && \ 23 | apt-get update && \ 24 | ACCEPT_EULA=Y apt-get install -y msodbcsql18 mssql-tools 25 | 26 | ENV PATH $PATH:/opt/mssql-tools/bin 27 | 28 | RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ 29 | echo 'LANG="en_US.UTF-8"'>/etc/default/locale && \ 30 | dpkg-reconfigure --frontend=noninteractive locales && \ 31 | update-locale LANG=en_US.UTF-8 32 | 33 | WORKDIR /app 34 | -------------------------------------------------------------------------------- /scripts/setup_databases.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "### Bringing Down Any Docker Containers that May Be Running ###" 6 | time docker-compose down 7 | 8 | echo "### Bringing Up sqlserver, oracle, postgres, mysql Images ###" 9 | #time docker-compose up -d sqlserver oracle postgres mysql 10 | time docker-compose up -d mysql 11 | echo "### DONE Bringing Up sqlserver and oracle Images ###" 12 | 13 | echo "### Checking Docker Images" 14 | docker ps 15 | 16 | # import setup functions 17 | echo "### Sourcing DB Scripts ###" 18 | . scripts/setup_db_scripts.sh 19 | 20 | # run setup scripts for local databases 21 | #echo "### Running Setup for sqlite ###" 22 | #time setup_sqlite 127.0.0.1 23 | echo "### Running Setup for mysql ###" 24 | time setup_mysql 127.0.0.1 33306 25 | 26 | echo "### Running Setup for sqlserver ###" 27 | # setup sqlserver in docker 28 | send_script sqlserver $SQL_SERVER_SCRIPT sqlserver-schema.sql 29 | send_script sqlserver ./build/setup_db_scripts.sh setup_db_scripts.sh 30 | time docker-compose exec -T sqlserver bash -c ". setup_db_scripts.sh && setup_sqlserver 127.0.0.1 sqlserver-schema.sql" 31 | 32 | # Can't do absolute paths here so need to do relative 33 | mkdir sqlline/ 34 | curl 'https://repo1.maven.org/maven2/sqlline/sqlline/1.12.0/sqlline-1.12.0-jar-with-dependencies.jar' -o 'sqlline/sqlline.jar' 35 | curl 'https://repo1.maven.org/maven2/com/oracle/ojdbc/ojdbc8/19.3.0.0/ojdbc8-19.3.0.0.jar' -o 'sqlline/ojdbc.jar' 36 | 37 | echo "### Starting to Wait for Oracle ###" 38 | while ! nc -z 127.0.0.1 31521; do 39 | echo "Waiting for Oracle" 40 | sleep 2; 41 | done; 42 | 43 | #echo "Running Oracle Setup Script" 44 | #java -cp 'sqlline/sqlline.jar:sqlline/ojdbc.jar' 'sqlline.SqlLine' \ 45 | # -u 'jdbc:oracle:thin:@localhost:11521:xe' \ 46 | # -n quill_test -p 'QuillRocks!' \ 47 | # -f "$ORACLE_SCRIPT" \ 48 | # --showWarnings=false 49 | # 50 | #sleep 2; 51 | # 52 | #echo "Oracle Setup Complete" 53 | # 54 | #echo "Databases are ready!" -------------------------------------------------------------------------------- /scripts/setup_local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | # import setup functions 6 | . scripts/setup_db_scripts.sh 7 | 8 | time setup_sqlite 9 | 10 | time setup_mysql mysql 11 | # SQL Server needs to be passed different script paths based on environment (based on exports in setup_db_scripts). Therefore it has a 2nd arg. 12 | time setup_sqlserver sqlserver $SQL_SERVER_SCRIPT 13 | 14 | time setup_oracle oracle 15 | 16 | 17 | echo "Databases are ready!" 18 | -------------------------------------------------------------------------------- /scripts/setup_sqlite_only.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Convenience script to just setup/bounce the sqlite database 4 | 5 | set -e 6 | 7 | # import setup functions 8 | . scripts/setup_db_scripts.sh 9 | 10 | time setup_sqlite 11 | 12 | echo "Sqlite is Ready!" 13 | -------------------------------------------------------------------------------- /scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker-compose down && docker-compose build && docker-compose run --rm --service-ports setup 4 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("build-logic") 3 | 4 | repositories { 5 | gradlePluginPortal() 6 | mavenCentral() 7 | mavenLocal() 8 | google() 9 | } 10 | } 11 | 12 | include("controller-core") 13 | include("controller-jdbc") 14 | include("controller-native") 15 | include("controller-android") 16 | 17 | include("terpal-sql-core") 18 | include("terpal-sql-core-testing") 19 | include("terpal-sql-jdbc") 20 | include("terpal-sql-native") 21 | include("terpal-sql-android") 22 | 23 | 24 | rootProject.name = "terpal-sql" 25 | -------------------------------------------------------------------------------- /terpal-sql-android/src/androidInstrumentedTest/kotlin/io/exoquery/sql/android/instrumented/InstrumentedSpec.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.sql.android.instrumented 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import app.cash.sqldelight.db.QueryResult 5 | import app.cash.sqldelight.db.SqlSchema 6 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver 7 | import io.exoquery.controller.android.AndroidDatabaseController 8 | 9 | interface InstrumentedSpec { 10 | fun createDriver(databaseName: String, schema: SqlSchema>) = run { 11 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 12 | AndroidDatabaseController.fromApplicationContext(databaseName, appContext, AndroidSqliteDriver.Callback(schema)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /terpal-sql-android/src/androidInstrumentedTest/kotlin/io/exoquery/sql/android/instrumented/InstrumentedTransactionSpec.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.sql.android.instrumented 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import io.exoquery.sql.PersonSchema 5 | import io.exoquery.sql.TransactionSpecOps 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | import kotlin.test.BeforeTest 9 | 10 | @RunWith(AndroidJUnit4::class) 11 | class InstrumentedTransactionSpec: InstrumentedSpec { 12 | val ctx get() = createDriver("tran_spec.db", PersonSchema) 13 | val ops get() = TransactionSpecOps(ctx) 14 | 15 | @BeforeTest 16 | fun clearTables() = ops.clearTables() 17 | @Test 18 | fun success() = ops.success() 19 | @Test 20 | fun failure() = ops.failure() 21 | @Test 22 | fun nested() = ops.nested() 23 | } 24 | -------------------------------------------------------------------------------- /terpal-sql-android/src/androidInstrumentedTest/kotlin/io/exoquery/sql/android/instrumented/InstrumentedWalConcurrencySpec.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.sql.android.instrumented 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import app.cash.sqldelight.driver.android.AndroidSqliteDriver 6 | import io.exoquery.sql.WalConcurrencyOps 7 | import io.exoquery.sql.WalTestSchema 8 | import io.exoquery.controller.android.AndroidDatabaseController 9 | import org.junit.runner.RunWith 10 | import org.junit.Test 11 | import kotlin.test.BeforeTest 12 | 13 | @RunWith(AndroidJUnit4::class) 14 | class InstrumentedWalConcurrencySpec: InstrumentedSpec { 15 | val ctx by lazy { 16 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 17 | val callback = AndroidSqliteDriver.Callback(WalTestSchema) 18 | AndroidDatabaseController.fromApplicationContext("wal_test.db", appContext, callback, poolingMode = AndroidDatabaseController.PoolingMode.MultipleReaderWal(3)) 19 | } 20 | 21 | val ops by lazy { WalConcurrencyOps(ctx) } 22 | 23 | @BeforeTest 24 | fun clearTables() = ops.clearTables() 25 | 26 | @Test 27 | fun `Write_Should_Not_Block_Read`() { 28 | ops.`Write_Should_Not_Block_Read`() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /terpal-sql-android/src/androidMain/kotlin/io/exoquery/sql/ParamExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.exoquery.sql 2 | 3 | import kotlinx.serialization.ContextualSerializer 4 | import java.math.BigDecimal 5 | import java.sql.Date 6 | import java.sql.Time 7 | import java.sql.Timestamp 8 | import java.time.ZonedDateTime 9 | import java.util.* 10 | import java.time.* 11 | import kotlinx.serialization.ExperimentalSerializationApi as SerApi 12 | 13 | @OptIn(SerApi::class) operator fun Param.Companion.invoke(value: LocalDate?): Param = Param(ContextualSerializer(LocalDate::class), LocalDate::class, value) 14 | @OptIn(SerApi::class) operator fun Param.Companion.invoke(value: LocalTime?): Param = Param(ContextualSerializer(LocalTime::class), LocalTime::class, value) 15 | @OptIn(SerApi::class) operator fun Param.Companion.invoke(value: LocalDateTime?): Param = Param(ContextualSerializer(LocalDateTime::class), LocalDateTime::class, value) 16 | @OptIn(SerApi::class) operator fun Param.Companion.invoke(value: Instant?): Param = Param(ContextualSerializer(Instant::class), Instant::class, value) 17 | 18 | @OptIn(SerApi::class) fun Param.Companion.fromUtilDate(value: java.util.Date?): Param = Param(ContextualSerializer(java.util.Date::class), java.util.Date::class, value) 19 | @OptIn(SerApi::class) fun Param.Companion.fromSqlDate(value: Date?): Param = Param(ContextualSerializer(Date::class), Date::class, value) 20 | @OptIn(SerApi::class) operator fun Param.Companion.invoke(value: Time?): Param