Add Lyng DB contract and SQLite provider skeleton
This commit is contained in:
1 parent
b42ceec686
commit
55ba6113e7
21 files changed
+3860
No files matched your search
@@ -0,0 +1,573 @@
|
||||
@file:OptIn(ExperimentalForeignApi::class)
|
||||
|
||||
/*
|
||||
* Copyright 2026 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng.io.db.sqlite
|
||||
|
||||
import cnames.structs.sqlite3
|
||||
import cnames.structs.sqlite3_stmt
|
||||
import kotlinx.cinterop.ByteVar
|
||||
import kotlinx.cinterop.CFunction
|
||||
import kotlinx.cinterop.CPointer
|
||||
import kotlinx.cinterop.COpaquePointer
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.MemScope
|
||||
import kotlinx.cinterop.allocArray
|
||||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.memScoped
|
||||
import kotlinx.cinterop.readBytes
|
||||
import kotlinx.cinterop.reinterpret
|
||||
import kotlinx.cinterop.toCPointer
|
||||
import kotlinx.cinterop.toKString
|
||||
import kotlinx.cinterop.usePinned
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toInstant
|
||||
import net.sergeych.lyng.ExecutionError
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_BLOB
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_CONSTRAINT
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_DONE
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_FLOAT
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_INTEGER
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_NULL
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_OK
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_OPEN_CREATE
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_OPEN_READONLY
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_OPEN_READWRITE
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_OPEN_URI
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_ROW
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.SQLITE_TEXT
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_blob
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_double
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_int64
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_null
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_parameter_count
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_bind_text
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_busy_timeout
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_changes
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_close_v2
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_blob
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_bytes
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_count
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_decltype
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_double
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_int64
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_name
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_text
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_column_type
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_errmsg
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_extended_errcode
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_extended_result_codes
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_finalize
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_last_insert_rowid
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.lyng_sqlite3_open
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.lyng_sqlite3_prepare
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_open_v2
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_reset
|
||||
import net.sergeych.lyng.io.db.sqlite.cinterop.sqlite3_step
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjBool
|
||||
import net.sergeych.lyng.obj.ObjBuffer
|
||||
import net.sergeych.lyng.obj.ObjDateTime
|
||||
import net.sergeych.lyng.obj.ObjEnumEntry
|
||||
import net.sergeych.lyng.obj.ObjException
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import net.sergeych.lyng.obj.ObjInstant
|
||||
import net.sergeych.lyng.obj.ObjNull
|
||||
import net.sergeych.lyng.obj.ObjReal
|
||||
import net.sergeych.lyng.obj.ObjString
|
||||
import net.sergeych.lyng.requireScope
|
||||
import platform.posix.memcpy
|
||||
import kotlin.time.Instant
|
||||
|
||||
internal actual suspend fun openSqliteBackend(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
options: SqliteOpenOptions,
|
||||
): SqliteDatabaseBackend {
|
||||
if (options.busyTimeoutMillis < 0) {
|
||||
scope.raiseIllegalArgument("busyTimeoutMillis must be >= 0")
|
||||
}
|
||||
return NativeSqliteDatabaseBackend(core, options)
|
||||
}
|
||||
|
||||
private class NativeSqliteDatabaseBackend(
|
||||
private val core: SqliteCoreModule,
|
||||
private val options: SqliteOpenOptions,
|
||||
) : SqliteDatabaseBackend {
|
||||
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqliteTransactionBackend) -> T): T {
|
||||
val handle = openHandle(scope, core, options)
|
||||
val savepoints = SavepointCounter()
|
||||
try {
|
||||
handle.execUnit(scope, core, "begin")
|
||||
val tx = NativeSqliteTransactionBackend(core, handle, savepoints)
|
||||
return try {
|
||||
val result = block(tx)
|
||||
handle.execUnit(scope, core, "commit")
|
||||
result
|
||||
} catch (e: Throwable) {
|
||||
handle.execUnitQuietly("rollback")
|
||||
throw e
|
||||
}
|
||||
} finally {
|
||||
handle.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeSqliteTransactionBackend(
|
||||
private val core: SqliteCoreModule,
|
||||
private val handle: NativeSqliteHandle,
|
||||
private val savepoints: SavepointCounter,
|
||||
) : SqliteTransactionBackend {
|
||||
override suspend fun select(scope: ScopeFacade, clause: String, params: List<Obj>): SqliteResultSetData {
|
||||
return handle.select(scope, core, clause, params)
|
||||
}
|
||||
|
||||
override suspend fun execute(scope: ScopeFacade, clause: String, params: List<Obj>): SqliteExecutionResultData {
|
||||
return handle.execute(scope, core, clause, params)
|
||||
}
|
||||
|
||||
override suspend fun <T> transaction(scope: ScopeFacade, block: suspend (SqliteTransactionBackend) -> T): T {
|
||||
val savepoint = "lyng_sp_${savepoints.next()}"
|
||||
handle.execUnit(scope, core, "savepoint $savepoint")
|
||||
return try {
|
||||
val result = block(NativeSqliteTransactionBackend(core, handle, savepoints))
|
||||
handle.execUnit(scope, core, "release savepoint $savepoint")
|
||||
result
|
||||
} catch (e: Throwable) {
|
||||
handle.execUnitQuietly("rollback to savepoint $savepoint")
|
||||
handle.execUnitQuietly("release savepoint $savepoint")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SavepointCounter {
|
||||
private var nextValue = 0
|
||||
|
||||
fun next(): Int {
|
||||
nextValue += 1
|
||||
return nextValue
|
||||
}
|
||||
}
|
||||
|
||||
private class NativeSqliteHandle(
|
||||
private val db: CPointer<sqlite3>,
|
||||
) {
|
||||
suspend fun select(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
clause: String,
|
||||
params: List<Obj>,
|
||||
): SqliteResultSetData = memScoped {
|
||||
val stmt = prepare(scope, core, clause)
|
||||
try {
|
||||
bindParams(scope, core, stmt, params, this)
|
||||
readResultSet(scope, core, stmt)
|
||||
} finally {
|
||||
sqlite3_finalize(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun execute(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
clause: String,
|
||||
params: List<Obj>,
|
||||
): SqliteExecutionResultData = memScoped {
|
||||
if (containsRowReturningClause(clause)) {
|
||||
raiseExecuteReturningUsage(scope, core)
|
||||
}
|
||||
val stmt = prepare(scope, core, clause)
|
||||
try {
|
||||
bindParams(scope, core, stmt, params, this)
|
||||
when (val rc = sqlite3_step(stmt)) {
|
||||
SQLITE_DONE -> {
|
||||
val affectedRows = sqlite3_changes(db)
|
||||
val generatedKeys = readGeneratedKeys(core, clause, affectedRows)
|
||||
SqliteExecutionResultData(affectedRows, generatedKeys)
|
||||
}
|
||||
SQLITE_ROW -> raiseExecuteReturningUsage(scope, core)
|
||||
else -> throw sqlError(scope, core, rc)
|
||||
}
|
||||
} finally {
|
||||
sqlite3_reset(stmt)
|
||||
sqlite3_finalize(stmt)
|
||||
}
|
||||
}
|
||||
|
||||
fun execUnit(scope: ScopeFacade, core: SqliteCoreModule, sql: String) {
|
||||
memScoped {
|
||||
val stmt = prepare(scope, core, sql)
|
||||
try {
|
||||
when (val rc = sqlite3_step(stmt)) {
|
||||
SQLITE_DONE, SQLITE_ROW -> Unit
|
||||
else -> throw sqlError(scope, core, rc)
|
||||
}
|
||||
} finally {
|
||||
sqlite3_finalize(stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun execUnitQuietly(sql: String) {
|
||||
memScoped {
|
||||
val stmt = lyng_sqlite3_prepare(db, sql) ?: return@memScoped
|
||||
try {
|
||||
sqlite3_step(stmt)
|
||||
} finally {
|
||||
sqlite3_finalize(stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
sqlite3_close_v2(db)
|
||||
}
|
||||
|
||||
private fun MemScope.prepare(scope: ScopeFacade, core: SqliteCoreModule, sql: String): CPointer<sqlite3_stmt> {
|
||||
return lyng_sqlite3_prepare(db, sql) ?: throw sqlError(scope, core, sqlite3_extended_errcode(db))
|
||||
}
|
||||
|
||||
private suspend fun bindParams(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
stmt: CPointer<sqlite3_stmt>,
|
||||
params: List<Obj>,
|
||||
memScope: MemScope,
|
||||
) {
|
||||
val expectedCount = sqlite3_bind_parameter_count(stmt)
|
||||
if (expectedCount != params.size) {
|
||||
throw usageError(
|
||||
scope,
|
||||
core,
|
||||
"SQL parameter count mismatch: statement expects $expectedCount value(s), got ${params.size}"
|
||||
)
|
||||
}
|
||||
params.forEachIndexed { index, value ->
|
||||
val parameterIndex = index + 1
|
||||
val rc = when (value) {
|
||||
ObjNull -> sqlite3_bind_null(stmt, parameterIndex)
|
||||
is ObjBool -> sqlite3_bind_int64(stmt, parameterIndex, if (value.value) 1L else 0L)
|
||||
is ObjInt -> sqlite3_bind_int64(stmt, parameterIndex, value.value)
|
||||
is ObjReal -> sqlite3_bind_double(stmt, parameterIndex, value.value)
|
||||
is ObjString -> bindText(stmt, parameterIndex, value.value, memScope)
|
||||
is ObjBuffer -> bindBlob(stmt, parameterIndex, value.byteArray.toByteArray(), memScope)
|
||||
is ObjInstant -> bindText(stmt, parameterIndex, value.instant.toString(), memScope)
|
||||
is ObjDateTime -> bindText(stmt, parameterIndex, value.localDateTime.toString(), memScope)
|
||||
else -> when (value.objClass.className) {
|
||||
"Date", "Decimal" -> bindText(stmt, parameterIndex, scope.toStringOf(value).value, memScope)
|
||||
else -> scope.raiseClassCastError("Unsupported SQLite parameter type: ${value.objClass.className}")
|
||||
}
|
||||
}
|
||||
if (rc != SQLITE_OK) {
|
||||
throw sqlError(scope, core, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindText(
|
||||
stmt: CPointer<sqlite3_stmt>,
|
||||
parameterIndex: Int,
|
||||
value: String,
|
||||
memScope: MemScope,
|
||||
): Int {
|
||||
return sqlite3_bind_text(stmt, parameterIndex, value, -1, SQLITE_TRANSIENT)
|
||||
}
|
||||
|
||||
private fun bindBlob(
|
||||
stmt: CPointer<sqlite3_stmt>,
|
||||
parameterIndex: Int,
|
||||
value: ByteArray,
|
||||
memScope: MemScope,
|
||||
): Int {
|
||||
if (value.isEmpty()) {
|
||||
return sqlite3_bind_blob(stmt, parameterIndex, null, 0, null)
|
||||
}
|
||||
val target = memScope.allocArray<ByteVar>(value.size)
|
||||
value.usePinned { pinned ->
|
||||
memcpy(target, pinned.addressOf(0), value.size.toULong())
|
||||
}
|
||||
return sqlite3_bind_blob(stmt, parameterIndex, target, value.size, SQLITE_TRANSIENT)
|
||||
}
|
||||
|
||||
private suspend fun readResultSet(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
stmt: CPointer<sqlite3_stmt>,
|
||||
): SqliteResultSetData {
|
||||
val columnCount = sqlite3_column_count(stmt)
|
||||
val columns = (0 until columnCount).map { index ->
|
||||
val nativeType = sqlite3_column_decltype(stmt, index)?.toKString().orEmpty()
|
||||
SqliteColumnMeta(
|
||||
name = sqlite3_column_name(stmt, index)?.toKString().orEmpty(),
|
||||
sqlType = mapSqlType(core, nativeType, SQLITE_NULL),
|
||||
nullable = true,
|
||||
nativeType = nativeType,
|
||||
)
|
||||
}.toMutableList()
|
||||
if (columnCount == 0) {
|
||||
return emptyResultSet()
|
||||
}
|
||||
|
||||
val rows = mutableListOf<List<Obj>>()
|
||||
while (true) {
|
||||
when (val rc = sqlite3_step(stmt)) {
|
||||
SQLITE_ROW -> {
|
||||
val row = (0 until columnCount).map { index ->
|
||||
val dynamicType = sqlite3_column_type(stmt, index)
|
||||
if (columns[index].nativeType.isBlank()) {
|
||||
columns[index] = columns[index].copy(sqlType = mapSqlType(core, columns[index].nativeType, dynamicType))
|
||||
}
|
||||
readColumnValue(scope, core, stmt, index, columns[index].nativeType)
|
||||
}
|
||||
rows += row
|
||||
}
|
||||
SQLITE_DONE -> return SqliteResultSetData(columns, rows)
|
||||
else -> throw sqlError(scope, core, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readColumnValue(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
stmt: CPointer<sqlite3_stmt>,
|
||||
index: Int,
|
||||
nativeType: String,
|
||||
): Obj {
|
||||
val normalizedNativeType = nativeType.trim().uppercase()
|
||||
return when (val type = sqlite3_column_type(stmt, index)) {
|
||||
SQLITE_NULL -> ObjNull
|
||||
SQLITE_INTEGER -> {
|
||||
val value = sqlite3_column_int64(stmt, index)
|
||||
when {
|
||||
normalizedNativeType == "BOOLEAN" -> ObjBool(value != 0L)
|
||||
isDecimalNativeType(nativeType) -> decimalFromString(scope, value.toString())
|
||||
else -> ObjInt.of(value)
|
||||
}
|
||||
}
|
||||
SQLITE_FLOAT -> {
|
||||
val value = sqlite3_column_double(stmt, index)
|
||||
if (isDecimalNativeType(nativeType)) decimalFromString(scope, value.toString()) else ObjReal.of(value)
|
||||
}
|
||||
SQLITE_TEXT -> {
|
||||
val textPtr = sqlite3_column_text(stmt, index)?.reinterpret<ByteVar>()
|
||||
val value = textPtr?.toKString() ?: ""
|
||||
convertStringValue(scope, nativeType, value)
|
||||
}
|
||||
SQLITE_BLOB -> {
|
||||
val size = sqlite3_column_bytes(stmt, index)
|
||||
val blob = sqlite3_column_blob(stmt, index)
|
||||
val bytes = if (blob == null || size <= 0) byteArrayOf() else blob.reinterpret<ByteVar>().readBytes(size)
|
||||
ObjBuffer(bytes.toUByteArray())
|
||||
}
|
||||
else -> ObjString(columnText(stmt, index))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun convertStringValue(scope: ScopeFacade, nativeType: String, value: String): Obj {
|
||||
val normalized = nativeType.trim().uppercase()
|
||||
return when {
|
||||
normalized == "DECIMAL" || normalized == "NUMERIC" -> decimalFromString(scope, value)
|
||||
normalized == "DATETIME" || normalized == "TIMESTAMP" -> dateTimeFromString(value)
|
||||
normalized == "TIMESTAMP WITH TIME ZONE" || normalized == "TIMESTAMPTZ" -> ObjInstant(Instant.parse(value))
|
||||
else -> ObjString(value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readGeneratedKeys(
|
||||
core: SqliteCoreModule,
|
||||
clause: String,
|
||||
affectedRows: Int,
|
||||
): SqliteResultSetData {
|
||||
if (affectedRows <= 0 || !looksLikeInsert(clause)) {
|
||||
return emptyResultSet()
|
||||
}
|
||||
return SqliteResultSetData(
|
||||
columns = listOf(
|
||||
SqliteColumnMeta(
|
||||
name = "generated_key",
|
||||
sqlType = core.sqlTypes.require("Int"),
|
||||
nullable = false,
|
||||
nativeType = "INTEGER",
|
||||
)
|
||||
),
|
||||
rows = listOf(listOf(ObjInt.of(sqlite3_last_insert_rowid(db))))
|
||||
)
|
||||
}
|
||||
|
||||
private fun columnText(stmt: CPointer<sqlite3_stmt>, index: Int): String {
|
||||
return sqlite3_column_text(stmt, index)?.reinterpret<ByteVar>()?.toKString().orEmpty()
|
||||
}
|
||||
|
||||
private fun sqlError(scope: ScopeFacade, core: SqliteCoreModule, rc: Int): ExecutionError {
|
||||
val code = sqlite3_extended_errcode(db)
|
||||
val message = sqlite3_errmsg(db)?.toKString() ?: "SQLite error ($rc)"
|
||||
val exceptionClass = if ((code and 0xff) == SQLITE_CONSTRAINT) core.sqlConstraintException else core.sqlExecutionException
|
||||
return ExecutionError(
|
||||
ObjException(exceptionClass, scope.requireScope(), ObjString(message)),
|
||||
scope.pos,
|
||||
message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openHandle(
|
||||
scope: ScopeFacade,
|
||||
core: SqliteCoreModule,
|
||||
options: SqliteOpenOptions,
|
||||
): NativeSqliteHandle = memScoped {
|
||||
val flags = buildOpenFlags(options)
|
||||
val db = lyng_sqlite3_open(options.path, flags)
|
||||
val rc = db?.let { sqlite3_extended_errcode(it) } ?: SQLITE_OK
|
||||
if (db == null || rc != SQLITE_OK) {
|
||||
val message = db?.let { sqlite3_errmsg(it)?.toKString() } ?: "SQLite open failed"
|
||||
if (db != null) {
|
||||
sqlite3_close_v2(db)
|
||||
}
|
||||
throw databaseError(scope, core, message)
|
||||
}
|
||||
sqlite3_extended_result_codes(db, 1)
|
||||
if (sqlite3_busy_timeout(db, options.busyTimeoutMillis) != SQLITE_OK) {
|
||||
val message = sqlite3_errmsg(db)?.toKString() ?: "Failed to configure SQLite busy timeout"
|
||||
sqlite3_close_v2(db)
|
||||
throw databaseError(scope, core, message)
|
||||
}
|
||||
val handle = NativeSqliteHandle(db)
|
||||
try {
|
||||
handle.execUnit(scope, core, if (options.foreignKeys) "pragma foreign_keys = on" else "pragma foreign_keys = off")
|
||||
} catch (e: Throwable) {
|
||||
handle.close()
|
||||
throw e
|
||||
}
|
||||
handle
|
||||
}
|
||||
|
||||
private fun buildOpenFlags(options: SqliteOpenOptions): Int {
|
||||
var flags = SQLITE_OPEN_URI
|
||||
if (options.readOnly) {
|
||||
flags = flags or SQLITE_OPEN_READONLY
|
||||
} else {
|
||||
flags = flags or SQLITE_OPEN_READWRITE
|
||||
if (options.createIfMissing) {
|
||||
flags = flags or SQLITE_OPEN_CREATE
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
private fun containsRowReturningClause(clause: String): Boolean =
|
||||
Regex("""\breturning\b""", RegexOption.IGNORE_CASE).containsMatchIn(clause)
|
||||
|
||||
private fun looksLikeInsert(clause: String): Boolean = clause.trimStart().startsWith("insert", ignoreCase = true)
|
||||
|
||||
private val SQLITE_TRANSIENT = (-1L).toCPointer<CFunction<(COpaquePointer?) -> Unit>>()
|
||||
|
||||
private fun emptyResultSet(): SqliteResultSetData = SqliteResultSetData(emptyList(), emptyList())
|
||||
|
||||
private fun mapSqlType(core: SqliteCoreModule, nativeType: String, sqliteType: Int): ObjEnumEntry = when {
|
||||
nativeType.trim().equals("BOOLEAN", ignoreCase = true) -> core.sqlTypes.require("Bool")
|
||||
nativeType.trim().equals("DATE", ignoreCase = true) -> core.sqlTypes.require("Date")
|
||||
nativeType.trim().equals("DATETIME", ignoreCase = true) || nativeType.trim().equals("TIMESTAMP", ignoreCase = true) -> core.sqlTypes.require("DateTime")
|
||||
nativeType.trim().equals("TIMESTAMP WITH TIME ZONE", ignoreCase = true) || nativeType.trim().equals("TIMESTAMPTZ", ignoreCase = true) -> core.sqlTypes.require("Instant")
|
||||
nativeType.trim().equals("DECIMAL", ignoreCase = true) || nativeType.trim().equals("NUMERIC", ignoreCase = true) -> core.sqlTypes.require("Decimal")
|
||||
nativeType.contains("BLOB", ignoreCase = true) -> core.sqlTypes.require("Binary")
|
||||
nativeType.contains("INT", ignoreCase = true) -> core.sqlTypes.require("Int")
|
||||
nativeType.contains("CHAR", ignoreCase = true) || nativeType.contains("TEXT", ignoreCase = true) || nativeType.contains("CLOB", ignoreCase = true) -> core.sqlTypes.require("String")
|
||||
nativeType.contains("REAL", ignoreCase = true) || nativeType.contains("FLOA", ignoreCase = true) || nativeType.contains("DOUB", ignoreCase = true) -> core.sqlTypes.require("Double")
|
||||
sqliteType == SQLITE_INTEGER -> core.sqlTypes.require("Int")
|
||||
sqliteType == SQLITE_FLOAT -> core.sqlTypes.require("Double")
|
||||
sqliteType == SQLITE_BLOB -> core.sqlTypes.require("Binary")
|
||||
else -> core.sqlTypes.require("String")
|
||||
}
|
||||
|
||||
private fun isDecimalNativeType(nativeType: String): Boolean {
|
||||
val normalized = nativeType.trim().uppercase()
|
||||
return normalized == "DECIMAL" || normalized == "NUMERIC"
|
||||
}
|
||||
|
||||
private suspend fun decimalFromString(scope: ScopeFacade, value: String): Obj {
|
||||
val decimalModule = scope.requireScope().currentImportProvider.createModuleScope(scope.pos, "lyng.decimal")
|
||||
val decimalClass = decimalModule.requireClass("Decimal")
|
||||
return decimalClass.invokeInstanceMethod(scope.requireScope(), "fromString", ObjString(value))
|
||||
}
|
||||
|
||||
private fun dateTimeFromString(value: String): ObjDateTime {
|
||||
val trimmed = value.trim()
|
||||
return if (hasExplicitTimeZone(trimmed)) {
|
||||
val instant = Instant.parse(trimmed)
|
||||
ObjDateTime(instant, parseTimeZoneOrUtc(trimmed))
|
||||
} else {
|
||||
val local = LocalDateTime.parse(trimmed)
|
||||
ObjDateTime(local.toInstant(TimeZone.UTC), TimeZone.UTC)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasExplicitTimeZone(value: String): Boolean {
|
||||
if (value.endsWith("Z", ignoreCase = true)) return true
|
||||
val tIndex = value.indexOf('T')
|
||||
if (tIndex < 0) return false
|
||||
val plus = value.lastIndexOf('+')
|
||||
val minus = value.lastIndexOf('-')
|
||||
val offsetStart = maxOf(plus, minus)
|
||||
return offsetStart > tIndex
|
||||
}
|
||||
|
||||
private fun parseTimeZoneOrUtc(value: String): TimeZone {
|
||||
if (value.endsWith("Z", ignoreCase = true)) return TimeZone.UTC
|
||||
val tIndex = value.indexOf('T')
|
||||
if (tIndex < 0) return TimeZone.UTC
|
||||
val plus = value.lastIndexOf('+')
|
||||
val minus = value.lastIndexOf('-')
|
||||
val offsetStart = maxOf(plus, minus)
|
||||
if (offsetStart <= tIndex) return TimeZone.UTC
|
||||
return try {
|
||||
TimeZone.of(value.substring(offsetStart))
|
||||
} catch (_: IllegalArgumentException) {
|
||||
TimeZone.UTC
|
||||
}
|
||||
}
|
||||
|
||||
private fun raiseExecuteReturningUsage(scope: ScopeFacade, core: SqliteCoreModule): Nothing {
|
||||
scope.raiseError(
|
||||
ObjException(
|
||||
core.sqlUsageException,
|
||||
scope.requireScope(),
|
||||
ObjString("execute(...) cannot be used with statements that return rows; use select(...)")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun usageError(scope: ScopeFacade, core: SqliteCoreModule, message: String): ExecutionError {
|
||||
return ExecutionError(
|
||||
ObjException(core.sqlUsageException, scope.requireScope(), ObjString(message)),
|
||||
scope.pos,
|
||||
message,
|
||||
)
|
||||
}
|
||||
|
||||
private fun databaseError(scope: ScopeFacade, core: SqliteCoreModule, message: String): ExecutionError {
|
||||
return ExecutionError(
|
||||
ObjException(core.databaseException, scope.requireScope(), ObjString(message)),
|
||||
scope.pos,
|
||||
message,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user