Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* New experimental API: `DatabaseScope#CREATE`
* New experimental API: `DatabaseScope#DROP`
* New experimental API: `DatabaseSceop#ALERT`
* Support using ByteArray in DSL, that represents BLOB in SQLite

### sqllin-driver

Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

* Support the key word REFERENCE
* Support JOIN sub-query
* Fix the bug of storing ByteArray in DSL
* Support Enum type
* Support typealias for primitive types

## Medium Priority

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.ctrip.sqllin.driver

import android.database.sqlite.SQLiteCursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQuery

/**
* Android implementation of [DatabaseConnection] using Android's SQLiteDatabase.
Expand All @@ -35,7 +37,52 @@ internal class AndroidDatabaseConnection(private val database: SQLiteDatabase) :

override fun executeUpdateDelete(sql: String, bindParams: Array<out Any?>?) = execSQL(sql, bindParams)

override fun query(sql: String, bindParams: Array<out String?>?): CommonCursor = AndroidCursor(database.rawQuery(sql, bindParams))
override fun query(sql: String, bindParams: Array<out Any?>?): CommonCursor {
val cursor = if (bindParams == null) {
database.rawQuery(sql, null)
} else {
// Use rawQueryWithFactory to bind parameters with proper types
// This allows us to bind parameters with their actual types (Int, Long, Double, etc.)
// instead of converting everything to String like rawQuery does
val cursorFactory = SQLiteDatabase.CursorFactory { _, masterQuery, editTable, query ->
bindTypedParameters(query, bindParams)
SQLiteCursor(masterQuery, editTable, query)
}
// Pass emptyArray() for selectionArgs since we bind parameters via the factory
// Use empty string for editTable since it's only needed for updateable cursors
database.rawQueryWithFactory(cursorFactory, sql, null, "")
}
return AndroidCursor(cursor)
}

/**
* Binds parameters to SQLiteQuery with proper type handling.
*
* Unlike rawQuery which only accepts String[], this method binds parameters
* with their actual types (Long, Double, ByteArray, etc.) to ensure correct
* SQLite type affinity and comparisons.
*/
private fun bindTypedParameters(query: SQLiteQuery, bindParams: Array<out Any?>) {
bindParams.forEachIndexed { index, param ->
val position = index + 1 // SQLite bind positions are 1-based
when (param) {
null -> query.bindNull(position)
is ByteArray -> query.bindBlob(position, param)
is Double -> query.bindDouble(position, param)
is Float -> query.bindDouble(position, param.toDouble())
is Long -> query.bindLong(position, param)
is Int -> query.bindLong(position, param.toLong())
is Short -> query.bindLong(position, param.toLong())
is Byte -> query.bindLong(position, param.toLong())
is Boolean -> query.bindLong(position, if (param) 1L else 0L)
is ULong -> query.bindLong(position, param.toLong())
is UInt -> query.bindLong(position, param.toLong())
is UShort -> query.bindLong(position, param.toLong())
is UByte -> query.bindLong(position, param.toLong())
else -> query.bindString(position, param.toString())
}
}
}

override fun beginTransaction() = database.beginTransaction()
override fun endTransaction() = database.endTransaction()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ public interface DatabaseConnection {
* Executes a SELECT query and returns a cursor.
*
* @param sql The SELECT statement
* @param bindParams Optional string parameters to bind to the query
* @param bindParams Optional parameters to bind to the query
* @return A cursor for iterating over query results
*/
public fun query(sql: String, bindParams: Array<out String?>? = null): CommonCursor
public fun query(sql: String, bindParams: Array<out Any?>? = null): CommonCursor

/**
* Begins a database transaction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ internal class ConcurrentDatabaseConnection(private val delegateConnection: Data
delegateConnection.executeUpdateDelete(sql, bindParams)
}

override fun query(sql: String, bindParams: Array<out String?>?): CommonCursor = accessLock.withLock {
override fun query(sql: String, bindParams: Array<out Any?>?): CommonCursor = accessLock.withLock {
delegateConnection.query(sql, bindParams)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,11 @@ internal class JdbcDatabaseConnection(private val connection: Connection) : Abst
it.executeUpdate()
}

override fun query(sql: String, bindParams: Array<out String?>?): CommonCursor {
val statement = connection.prepareStatement(sql)
bindParams?.forEachIndexed { index, str ->
str?.let {
statement.setString(index + 1, it)
}
}
return statement.executeQuery()?.let { JdbcCursor(it) } ?: throw IllegalStateException("The query result is null.")
}
override fun query(sql: String, bindParams: Array<out Any?>?): CommonCursor =
bindParamsToSQL(sql, bindParams)
.executeQuery()
?.let { JdbcCursor(it) }
?: throw IllegalStateException("The query result is null.")

private val isTransactionSuccess = AtomicBoolean(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ internal class ConcurrentDatabaseConnection(
delegateConnection.executeUpdateDelete(sql, bindParams)
}

override fun query(sql: String, bindParams: Array<out String?>?): CommonCursor = accessLock.withLock {
override fun query(sql: String, bindParams: Array<out Any?>?): CommonCursor = accessLock.withLock {
delegateConnection.query(sql, bindParams)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,8 @@ internal class RealDatabaseConnection(
}
}

override fun query(sql: String, bindParams: Array<out String?>?): CommonCursor {
val statement = createStatement(sql)
bindParams?.forEachIndexed { index, str ->
str?.let {
statement.bindString(index + 1, it)
}
}
return statement.query()
}
override fun query(sql: String, bindParams: Array<out Any?>?): CommonCursor =
bindParamsToSQL(sql, bindParams).query()

override fun beginTransaction() = transactionLock.withLock {
database.rawExecSql("BEGIN;")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ class AndroidTest {
@Test
fun testNullValue() = commonTest.testNullValue()

@Test
fun testCreateTableWithLongPrimaryKey() = commonTest.testCreateTableWithLongPrimaryKey()

@Test
fun testCreateTableWithStringPrimaryKey() = commonTest.testCreateTableWithStringPrimaryKey()

@Test
fun testCreateTableWithAutoincrement() = commonTest.testCreateTableWithAutoincrement()

@Test
fun testCreateTableWithCompositePrimaryKey() = commonTest.testCreateTableWithCompositePrimaryKey()

@Test
fun testInsertWithId() = commonTest.testInsertWithId()

@Test
fun testCreateInDatabaseScope() = commonTest.testCreateInDatabaseScope()

@Test
fun testUpdateAndDeleteWithPrimaryKey() = commonTest.testUpdateAndDeleteWithPrimaryKey()

@Test
fun testByteArrayInsert() = commonTest.testByteArrayInsert()

@Test
fun testByteArraySelect() = commonTest.testByteArraySelect()

@Test
fun testByteArrayUpdate() = commonTest.testByteArrayUpdate()

@Test
fun testByteArrayDelete() = commonTest.testByteArrayDelete()

@Test
fun testByteArrayMultipleOperations() = commonTest.testByteArrayMultipleOperations()

@Before
fun setUp() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

package com.ctrip.sqllin.dsl.test

import com.ctrip.sqllin.dsl.annotation.CompositePrimaryKey
import com.ctrip.sqllin.dsl.annotation.DBRow
import com.ctrip.sqllin.dsl.annotation.PrimaryKey
import kotlinx.serialization.Serializable

/**
Expand Down Expand Up @@ -63,4 +65,68 @@ data class NullTester(
val paramInt: Int?,
val paramString: String?,
val paramDouble: Double?,
)
)

@DBRow("person_with_id")
@Serializable
data class PersonWithId(
@PrimaryKey val id: Long?,
val name: String,
val age: Int,
)

@DBRow("product")
@Serializable
data class Product(
@PrimaryKey val sku: String?,
val name: String,
val price: Double,
)

@DBRow("student_with_autoincrement")
@Serializable
data class StudentWithAutoincrement(
@PrimaryKey(isAutoincrement = true) val id: Long?,
val studentName: String,
val grade: Int,
)

@DBRow("enrollment")
@Serializable
data class Enrollment(
@CompositePrimaryKey val studentId: Long,
@CompositePrimaryKey val courseId: Long,
val semester: String,
)

@DBRow("file_data")
@Serializable
data class FileData(
@PrimaryKey(isAutoincrement = true) val id: Long?,
val fileName: String,
val content: ByteArray,
val metadata: String,
) {
// ByteArray doesn't implement equals/hashCode properly for data class
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as FileData

if (id != other.id) return false
if (fileName != other.fileName) return false
if (!content.contentEquals(other.content)) return false
if (metadata != other.metadata) return false

return true
}

override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + fileName.hashCode()
result = 31 * result + content.contentHashCode()
result = 31 * result + metadata.hashCode()
return result
}
}
Loading
Loading