├── .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