Skip to content

Commit be6c300

Browse files
committed
Progress
1 parent 1226747 commit be6c300

File tree

4 files changed

+273
-311
lines changed

4 files changed

+273
-311
lines changed

SQLiter/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ kotlin {
6363
dependencies {
6464
implementation "co.touchlab:stately:$STATELY_VERSION"
6565
implementation 'org.jetbrains.kotlin:kotlin-stdlib-common'
66-
implementation 'com.squareup.sqldelight:runtime:1.0.0-rc4'
66+
// implementation 'com.squareup.sqldelight:runtime:1.0.0-rc4'
6767
}
6868
}
6969
commonTest {

SQLiter/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
kotlin.code.style=official
1818

1919
GROUP=co.touchlab
20-
VERSION_NAME=0.5.0
20+
VERSION_NAME=0.5.1-SNAPSHOT
2121

22-
STATELY_VERSION=0.4.0
22+
STATELY_VERSION=0.4.1-SNAPSHOT
2323
KOTLIN_VERSION=1.3.10
2424
DOKKA_VERSION=0.9.17
2525

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
/*
2+
* Copyright (C) 2018 Touchlab, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package co.touchlab.sqliter.sqldelight
18+
19+
import co.touchlab.sqliter.*
20+
import co.touchlab.stately.collections.AbstractSharedLinkedList
21+
import co.touchlab.stately.collections.SharedLinkedList
22+
import co.touchlab.stately.collections.frozenHashMap
23+
import co.touchlab.stately.collections.frozenLinkedList
24+
import co.touchlab.stately.concurrency.AtomicReference
25+
import co.touchlab.stately.concurrency.ReentrantLock
26+
import co.touchlab.stately.concurrency.ThreadRef
27+
import co.touchlab.stately.concurrency.withLock
28+
import co.touchlab.stately.freeze
29+
import com.squareup.sqldelight.Transacter
30+
import com.squareup.sqldelight.db.SqlCursor
31+
import com.squareup.sqldelight.db.SqlDatabase
32+
import com.squareup.sqldelight.db.SqlDatabaseConnection
33+
import com.squareup.sqldelight.db.SqlPreparedStatement
34+
35+
class SqlLighterDatabase(private val databaseManager: DatabaseManager) : SqlDatabase {
36+
private val connectionInstanceThreadCache = frozenLinkedList<ThreadConnection>()
37+
38+
/**
39+
* Keep all outstanding cursors to close when closing the db, just in case the user didn't.
40+
*/
41+
internal val cursorCollection = frozenLinkedList<Cursor>() as SharedLinkedList<Cursor>
42+
private val connectionLock = ReentrantLock()
43+
private val singleOpConnection = databaseManager.createMultiThreadedConnection()
44+
private val publicApiConnection = SqlLighterConnection(this)
45+
46+
override fun close() = connectionLock.withLock {
47+
connectionInstanceThreadCache.forEach {
48+
it.connection.close()
49+
}
50+
connectionInstanceThreadCache.clear()
51+
52+
singleOpConnection.close()
53+
}
54+
55+
override fun getConnection(): SqlDatabaseConnection = publicApiConnection
56+
57+
/**
58+
* If we're in a transaction, then I have a connection. Otherwise we lock and
59+
* use the open connection on which all other ops run.
60+
*/
61+
internal fun <R> realConnectionMineOrUnaligned(block: DatabaseConnection.() -> R): R {
62+
val mine = myConnection()
63+
return if (mine != null)
64+
mine.connection.block()
65+
else {
66+
connectionLock.withLock {
67+
singleOpConnection.block()
68+
}
69+
}
70+
}
71+
72+
/**
73+
* If we're in the middle of a transaction, the thread ref will point to us.
74+
*/
75+
internal fun myConnection() = connectionInstanceThreadCache.find { it.threadRef.notNullSame() }
76+
77+
internal fun myConnectionOrNew(): ThreadConnection {
78+
return myConnection() ?: connectionLock.withLock {
79+
val empty = connectionInstanceThreadCache.find { it.threadRef == null } ?: createNewPollConnection()
80+
empty.threadRef = ThreadRef()
81+
empty
82+
}
83+
}
84+
85+
/**
86+
* All connections tracked in our pool cache
87+
*/
88+
private fun createNewPollConnection(): ThreadConnection {
89+
val conn = ThreadConnection(databaseManager.createMultiThreadedConnection())
90+
connectionInstanceThreadCache.add(conn)
91+
return conn
92+
}
93+
94+
internal fun recycleCursor(node: AbstractSharedLinkedList.Node<Cursor>) = connectionLock.withLock {
95+
node.nodeValue.statement.finalizeStatement()
96+
node.remove()
97+
}
98+
99+
internal fun trackCursor(cursor: Cursor):AbstractSharedLinkedList.Node<Cursor> = connectionLock.withLock {
100+
cursorCollection.addNode(cursor)
101+
}
102+
}
103+
104+
class SqlLighterConnection(private val database: SqlLighterDatabase) : SqlDatabaseConnection {
105+
override fun currentTransaction(): Transacter.Transaction? = database.myConnection()?.transaction?.value
106+
107+
override fun newTransaction(): Transacter.Transaction {
108+
val myConn = database.myConnectionOrNew()
109+
return myConn.newTransaction()
110+
}
111+
112+
override fun prepareStatement(sql: String, type: SqlPreparedStatement.Type, parameters: Int): SqlPreparedStatement =
113+
SqlLighterStatement(sql, type, database)
114+
}
115+
116+
class ThreadConnection(val connection: DatabaseConnection) {
117+
var threadRef: ThreadRef? = null
118+
private val transLock = ReentrantLock()
119+
internal val transaction: AtomicReference<Transaction?> = AtomicReference(null)
120+
121+
fun newTransaction(): Transaction {
122+
val enclosing = transaction.value
123+
124+
//Create here, in case we bomb...
125+
if (enclosing == null) {
126+
connection.beginTransaction()
127+
}
128+
129+
val trans = Transaction(enclosing).freeze()
130+
transaction.value = trans
131+
132+
return trans
133+
}
134+
135+
inner class Transaction(
136+
override val enclosingTransaction: Transaction?
137+
) : Transacter.Transaction() {
138+
139+
override fun endTransaction(successful: Boolean) = transLock.withLock {
140+
if (enclosingTransaction == null) {
141+
if (successful) {
142+
connection.setTransactionSuccessful()
143+
}
144+
145+
connection.endTransaction()
146+
}
147+
transaction.value = enclosingTransaction
148+
}
149+
}
150+
}
151+
152+
internal fun ThreadRef?.notNullSame(): Boolean {
153+
return this != null && same()
154+
}
155+
156+
class SqlLighterStatement(
157+
private val sql: String,
158+
private val type: SqlPreparedStatement.Type,
159+
private val database: SqlLighterDatabase
160+
) : SqlPreparedStatement {
161+
162+
//For each statement declared, there can be an instance per-thread.
163+
private val statementInstanceThreadCache = frozenLinkedList<ThreadStatement>()
164+
165+
private fun myThreadCachedStatementInstance() = statementInstanceThreadCache.find { it.mine }
166+
167+
override fun bindBytes(index: Int, value: ByteArray?) = myThreadStatementInstance().bindBytes(index, value)
168+
override fun bindDouble(index: Int, value: Double?) = myThreadStatementInstance().bindDouble(index, value)
169+
override fun bindLong(index: Int, value: Long?) = myThreadStatementInstance().bindLong(index, value)
170+
override fun bindString(index: Int, value: String?) = myThreadStatementInstance().bindString(index, value)
171+
172+
/**
173+
* Executing a statement clears the instance definition. Effectively that means the bindings are reset. We can
174+
* recycle these rather than letting them be deallocated in the future if that improves performance in some way.
175+
*/
176+
override fun execute() {
177+
database.realConnectionMineOrUnaligned {
178+
withStatement(sql) {
179+
applyBindings(this)
180+
when (type) {
181+
SqlPreparedStatement.Type.SELECT -> throw AssertionError()
182+
else -> execute()
183+
}
184+
}
185+
}
186+
removeMyInstance()
187+
}
188+
189+
/**
190+
* Creating a cursor returns an actual sqlite statement instance, so we need to be careful with these. However,
191+
* the design of sqldelight's queries and the QueryWrapper would like us to retain the bindings (I think), so
192+
* we leave the instance for this thread hanging out.
193+
*
194+
* If the developer creates many threads, this will grow over time. We will probably want to either cap the cache
195+
* count, and risk losing the bindings, or redesign the interface somewhat.
196+
*
197+
* It does seem that executable statements and queries are designed somewhat differently, however.
198+
*/
199+
override fun executeQuery(): SqlCursor = database.realConnectionMineOrUnaligned {
200+
val statement = createStatement(sql)
201+
applyBindings(statement)
202+
SQLiterCursor(database.trackCursor(statement.query()), database)
203+
}
204+
205+
private fun applyBindings(statement: Statement){
206+
myThreadCachedStatementInstance()?.binds?.forEach {
207+
it.value(statement)
208+
}
209+
}
210+
211+
//These don't need to be locked because only *your* thread is adding/removing it's own entries
212+
//Assuming the list itself is thread safe, no problem
213+
private fun myThreadStatementInstance(): ThreadStatement {
214+
return myThreadCachedStatementInstance() ?: createMyInstance()
215+
}
216+
217+
private fun createMyInstance(): ThreadStatement {
218+
val myInstance = ThreadStatement()
219+
statementInstanceThreadCache.add(myInstance)
220+
return myInstance
221+
}
222+
223+
private fun removeMyInstance() {
224+
val mine = myThreadCachedStatementInstance()
225+
if (mine != null)
226+
statementInstanceThreadCache.remove(mine)
227+
}
228+
}
229+
230+
class SQLiterCursor(private val cursorCollectionNode: AbstractSharedLinkedList.Node<Cursor>, private val database: SqlLighterDatabase) : SqlCursor {
231+
private val cursor = cursorCollectionNode.nodeValue
232+
233+
override fun close() {
234+
database.recycleCursor(cursorCollectionNode)
235+
}
236+
237+
override fun getBytes(index: Int): ByteArray? = cursor.getBytesOrNull(index)
238+
239+
override fun getDouble(index: Int): Double? = cursor.getDoubleOrNull(index)
240+
241+
override fun getLong(index: Int): Long? = cursor.getLongOrNull(index)
242+
243+
override fun getString(index: Int): String? = cursor.getStringOrNull(index)
244+
245+
override fun next(): Boolean = cursor.next()
246+
}
247+
248+
class ThreadStatement {
249+
private val threadRef = ThreadRef()
250+
internal val binds = frozenHashMap<Int, (Statement) -> Unit>()
251+
252+
val mine: Boolean
253+
get() = threadRef.same()
254+
255+
fun bindBytes(index: Int, value: ByteArray?) {
256+
binds.put(index) { it.bindBlob(index, value) }
257+
}
258+
259+
fun bindDouble(index: Int, value: Double?) {
260+
binds.put(index) { it.bindDouble(index, value) }
261+
}
262+
263+
fun bindLong(index: Int, value: Long?) {
264+
binds.put(index) { it.bindLong(index, value) }
265+
}
266+
267+
fun bindString(index: Int, value: String?) {
268+
binds.put(index) { it.bindString(index, value) }
269+
}
270+
}

0 commit comments

Comments
 (0)