diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8f2d33b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches/build_file_checksums.ser
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+*.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..eaa990c
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0]
+### Added
+- Initial version
+- Provides a simple interface for creating batch inserts and replaces.
+- Supports Android framework sqlite
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..c63336f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,8 @@
+Contributing
+============
+
+If you would like to contribute code to this project you can do so through GitHub by
+forking the repository and sending a pull request.
+
+When submitting code, please make every effort to follow existing conventions
+and style in order to keep the code as readable as possible.
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..c487b66
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Yello
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f1b5231
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+Batch Light
+=====
+
+Batch Light is a library for batch inserting to SQLite on Android.
+
+A demo project can be found on Google Play, and as an Instant App.
+
+
+
+
+
+## Download
+
+## Usage
+Batch Lite has 4 main steps to use.
+
+1. Create a configuration object. Currently SQLite is the only binder available with the batcher.
+The SQLiteBinderConfig has to default types available, Insert (INSERT INTO) and Replace (REPLACE INTO). When getting
+an instance of the SQLiteBinderConfig there are three params. A reference to the SQLite Database being written to,
+the name of the table being written to and the number of columns that will be written to.
+
+```kotlin
+val binderConfig = SQLiteBinderConfig.getInsertConfig(
+ db = writableDatabase,
+ tableName = batchTableName,
+ columnCount = 3
+)
+```
+
+2. Create a new batcher using the config from step 1.
+
+```kotlin
+val batcher = BatchStatement(binderConfig)
+```
+
+3. call the execute function on the batcher, passing in the list to be inserted and a clojure that tells it how to bind
+the items in the list. The config will generate a statement that looks like:
+
+```kotlin
+data class InsertItem(val id: Int, val text: String)
+```
+
+Table structure:
+```
+TABLE_NAME
+-----------
+id | text |
+```
+
+The generated SQLite for a list of 3 of the items above would be:
+
+```roomsql
+INSERT INTO TABLE_NAME VALUES (?, ?), (?, ?), (?, ?)
+```
+
+To bind the list, the closure below should be used. The binder will use the clojure for each item to bind to the generated
+statement.
+
+```kotlin
+batcher.execute(list) { listItem ->
+ bindLong(listItem.id.toLong())
+ bindString(listItem.text)
+}
+```
+
+4. The Batch Lite does not handle transactions. It is left up to the end user to wrap the batch
+insert into a transaction. If the list that is being batched is over 10,000 items it is recommended to chunk the list
+and batch it in separate transactions to avoid blocking other inserts happening in the application.
+
+An example usage can be found in the sample project.
+
+Check out the [Kdoc](https://yelloco.github.io/Batchlight) for full documentation.
+
+## Contributing
+
+Please read [Contributing.md](CONTRIBUTING.md) for details on the process for submitting pull requests.
+
+## License
+
+This project is licensed under the MIT License - see the [License.txt](LICENSE.txt) file for details.
+
+## Questions
+
+Any questions comments or concerns can be directed to opensource@yello.co.
diff --git a/batchlight/.gitignore b/batchlight/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/batchlight/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/batchlight/build.gradle b/batchlight/build.gradle
new file mode 100644
index 0000000..c8dca90
--- /dev/null
+++ b/batchlight/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.github.dcendents.android-maven'
+apply plugin: 'org.jetbrains.dokka-android'
+
+group = 'co.yello.batchlight'
+
+android {
+ compileSdkVersion 28
+ testOptions.unitTests.includeAndroidResources = true
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion 28
+ }
+}
+
+dokka {
+ outputFormat = 'html'
+ outputDirectory = "$buildDir/../javadoc"
+ reportUndocumented = false
+ skipDeprecated = true
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation "io.mockk:mockk:1.9"
+ testImplementation "org.robolectric:robolectric:4.2"
+}
\ No newline at end of file
diff --git a/batchlight/src/main/AndroidManifest.xml b/batchlight/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..7dbd481
--- /dev/null
+++ b/batchlight/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/BatchBinder.kt b/batchlight/src/main/java/co/yello/db/batchlight/BatchBinder.kt
new file mode 100644
index 0000000..aed238f
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/BatchBinder.kt
@@ -0,0 +1,69 @@
+package co.yello.db.batchlight
+
+/**
+ * A wrapper around a [Binder] to make sure binds happen in sequential order.
+ *
+ * @param binder the specific [Binder] to bind values to.
+ * @param startBindIndex the index to start binding to the binder.
+ *
+ * @constructor Creates a BatchBinder with the given binder. The binder will be cleared on creation to ensure
+ * no pre existing values will be bound unintentionally.
+ */
+class BatchBinder(
+ private val binder: Binder,
+ startBindIndex: Int
+) {
+
+ /**
+ * Marks the current binding position.
+ */
+ var currentBindIndex: Int = startBindIndex
+ private set
+
+ init {
+ binder.clear()
+ }
+
+ /**
+ * Binds a value of type [Long] to the [Binder] and increments the position.
+ *
+ * @param long a [Long] to be bound to the [Binder]
+ */
+ fun bindLong(long: Long) {
+ binder.bindLong(currentBindIndex++, long)
+ }
+
+ /**
+ * Binds a value of type [String] to the [Binder] and increments the position.
+ *
+ * @param string a [String] to be bound to the [Binder].
+ */
+ fun bindString(string: String) {
+ binder.bindString(currentBindIndex++, string)
+ }
+
+ /**
+ * Binds a value of type [Double] to the [Binder] and increments the position.
+ *
+ * @param double a [Double] to be bound to the [Binder].
+ */
+ fun bindDouble(double: Double) {
+ binder.bindDouble(currentBindIndex++, double)
+ }
+
+ /**
+ * Binds a null value to the [Binder] and increments the bind position.
+ */
+ fun bindNull() {
+ binder.bindNull(currentBindIndex++)
+ }
+
+ /**
+ * Binds a [ByteArray] to the [Binder] and increments the position.
+ *
+ * @param blob a [ByteArray] to be bound to the [Binder].
+ */
+ fun bindBlob(blob: ByteArray) {
+ binder.bindBlob(currentBindIndex++, blob)
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/BatchStatement.kt b/batchlight/src/main/java/co/yello/db/batchlight/BatchStatement.kt
new file mode 100644
index 0000000..9a70e9f
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/BatchStatement.kt
@@ -0,0 +1,86 @@
+package co.yello.db.batchlight
+
+/**
+ * Drives the inserts into the database.
+ *
+ * @param T The type of the collection that will be inserted. This is used to pass into the bind function that is
+ * passed into the execute function.
+ * @property binderConfig A [BinderConfig] used to generate all of the [Binder]s that are required to perform
+ * the inserts. This flexibility lets the library work with a variety of databases.
+ */
+class BatchStatement(
+ private val binderConfig: BinderConfig
+) {
+
+ /**
+ * Chunks the list into the maximum sized list that can be inserted at a time, then executes the insert
+ * with the [bindItemFunction] passed into the class.
+ *
+ * @param itemsToInsert The collection of items of type [T] to batch insert into the database.
+ * @param bindItemFunction A function that performs the binds for each field in the [T] passed to the function.
+ * The function is an extension on [BatchBinder] so all bind functions can be accessed from within.
+ *
+ * When binding the order of the bind calls should match the structure of the table inserting.
+ *
+ * Example usage:
+ *
+ * A simple class would look something like
+ *
+ * ```
+ * data class BatchClass(val id: Int, val text: String, val num: Int)
+ *```
+ *
+ * The SQL Create statement for the corresponding table to that object would be:
+ *
+ * ```
+ * CREATE TABLE BatchClass (
+ * id INTEGER,
+ * text TEXT,
+ * num INTEGER
+ * )
+ *```
+ *
+ * The [Binder] generated for this table will will have the params (?,?,?). To correctly bind the three values
+ * to their correct ? in the prepared statement the function would be:
+ *
+ * ```
+ * { batchObject - >
+ * bindLong(batchObject.id)
+ * bindString(batchObject.text)
+ * bindLong(batchObject.num)
+ * }
+ * ```
+ */
+ fun execute(itemsToInsert: Collection, bindItemFunction: BatchBinder.(T) -> Unit) {
+ itemsToInsert.asSequence()
+ .chunked(binderConfig.maxInsertSize)
+ .forEach { maxSizeList -> performInsert(maxSizeList, bindItemFunction) }
+ }
+
+ /**
+ * Binds all values to the correct [Binder] for the given collection then executes the statement.
+ *
+ * @param collection The collection of items to be bound and executed.
+ * @param bindItem The bind statement to execute on each item of the collection.
+ */
+ private fun performInsert(collection: Collection, bindItem: BatchBinder.(T) -> Unit) {
+ val statement = if (collection.size == binderConfig.maxInsertSize) {
+ binderConfig.maxInsertBinder
+ } else {
+ binderConfig.buildBinder(collection.size)
+ }
+
+ val binder = BatchBinder(statement, binderConfig.startIndex)
+
+ collection.forEach { item ->
+ binder.bindItem(item)
+ }
+
+ check(binder.currentBindIndex - 1 == collection.size * binderConfig.fieldsPerItem) {
+ "Expected to bind ${binderConfig.fieldsPerItem} columns per record, " +
+ "found ${binder.currentBindIndex / collection.size} binds per record."
+ }
+
+ statement.execute()
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/Binder.kt b/batchlight/src/main/java/co/yello/db/batchlight/Binder.kt
new file mode 100644
index 0000000..e39caf7
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/Binder.kt
@@ -0,0 +1,56 @@
+package co.yello.db.batchlight
+
+/**
+ * Provides way to bind values to any DB or ORM.
+ */
+interface Binder {
+
+ /**
+ * Clears any bound values.
+ */
+ fun clear()
+
+ /**
+ * Executes with any bound values.
+ */
+ fun execute()
+
+ /**
+ * Binds a [Long] at the given position.
+ *
+ * @param position position to bind at.
+ * @param long a value of type [Long] to bind.
+ */
+ fun bindLong(position: Int, long: Long)
+
+ /**
+ * Binds a [String] at the given position.
+ *
+ * @param position position to bind at.
+ * @param string a value of type [String] to bind.
+ */
+ fun bindString(position: Int, string: String)
+
+ /**
+ * Binds a [Double] at the given position.
+ *
+ * @param position position to bind at.
+ * @param double a value of type [Double] to bind.
+ */
+ fun bindDouble(position: Int, double: Double)
+
+ /**
+ * Binds a [ByteArray] at the given position.
+ *
+ * @param position position to bind at.
+ * @param blob a value of type [ByteArray] to bind.
+ */
+ fun bindBlob(position: Int, blob: ByteArray)
+
+ /**
+ * Binds a null value at the given position.
+ *
+ * @param position position to bind at.
+ */
+ fun bindNull(position: Int)
+}
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/BinderConfig.kt b/batchlight/src/main/java/co/yello/db/batchlight/BinderConfig.kt
new file mode 100644
index 0000000..94ec28b
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/BinderConfig.kt
@@ -0,0 +1,45 @@
+package co.yello.db.batchlight
+
+/**
+ * Holds all the configurations for a [Binder].
+ */
+interface BinderConfig {
+
+ /**
+ * Creates a [Binder] that has the given number of bind locations.
+ *
+ * @param insertCount the number of fields that will be bound.
+ * @return a [Binder] object that has [insertCount] number of fields
+ */
+ fun buildBinder(insertCount: Int): Binder
+
+ /**
+ * Holds a reference to the max insert binder. This will be the most common case so it should
+ * be cached.
+ *
+ * @return a [Binder] object that has maximum number of insert params available.
+ */
+ val maxInsertBinder: Binder
+
+ /**
+ * @return The start index of where to bind begin binding.
+ */
+ val startIndex: Int
+
+ /**
+ * If an object has 4 fields and only 3 should be written to the database this value should be set to 3.
+ *
+ * @return The number of fields to be inserted per item.
+ */
+ val fieldsPerItem: Int
+
+ /**
+ * @return The maximum number fields that can be bound at one time. For example this number is 999 for SQLite Android.
+ */
+ val maxFields: Int
+
+ /**
+ * @return The max number of items that can be inserted at one time.
+ */
+ val maxInsertSize: Int
+}
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/Constants.kt b/batchlight/src/main/java/co/yello/db/batchlight/Constants.kt
new file mode 100644
index 0000000..610cb39
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/Constants.kt
@@ -0,0 +1,15 @@
+@file:Suppress("SpellCheckingInspection")
+
+package co.yello.db.batchlight
+
+/**
+ * The maximum number of bind params that Android SQLite allows per prepared statement.
+ *
+ * See SQLITE_MAX_VARIABLE_NUMBER in the SQLite docs at https://sqlite.org/limits.html
+ */
+const val sqlMaxBinds = 999
+
+/**
+ * The start index of Android SQLite prepared statement binds.
+ */
+const val sqlAndroidPreparedStatementStartIndex = 1
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinder.kt b/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinder.kt
new file mode 100644
index 0000000..82d53d7
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinder.kt
@@ -0,0 +1,50 @@
+package co.yello.db.batchlight.androidsqlite
+
+import android.database.sqlite.SQLiteStatement
+import co.yello.db.batchlight.Binder
+
+/**
+ * The [Binder] implementation for SQLite on Android.
+ *
+ * @property sqLiteStatement android SQLite statement to interact with.
+ */
+class AndroidSQLiteBinder(
+ private val sqLiteStatement: SQLiteStatement
+) : Binder {
+
+ override fun execute() {
+ sqLiteStatement.execute()
+ }
+
+ override fun clear() {
+ sqLiteStatement.clearBindings()
+ }
+
+ override fun bindLong(position: Int, long: Long) {
+ sqLiteStatement.bindLong(position, long)
+ }
+
+ override fun bindString(position: Int, string: String) {
+ sqLiteStatement.bindString(position, string)
+ }
+
+ override fun bindDouble(position: Int, double: Double) {
+ sqLiteStatement.bindDouble(position, double)
+ }
+
+ override fun bindBlob(position: Int, blob: ByteArray) {
+ sqLiteStatement.bindBlob(position, blob)
+ }
+
+ override fun bindNull(position: Int) {
+ sqLiteStatement.bindNull(position)
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return sqLiteStatement == (other as? AndroidSQLiteBinder)?.sqLiteStatement
+ }
+
+ override fun hashCode(): Int {
+ return sqLiteStatement.hashCode()
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfig.kt b/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfig.kt
new file mode 100644
index 0000000..f62a812
--- /dev/null
+++ b/batchlight/src/main/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfig.kt
@@ -0,0 +1,98 @@
+package co.yello.db.batchlight.androidsqlite
+
+import android.database.sqlite.SQLiteDatabase
+import co.yello.db.batchlight.Binder
+import co.yello.db.batchlight.BinderConfig
+import co.yello.db.batchlight.sqlAndroidPreparedStatementStartIndex
+import co.yello.db.batchlight.sqlMaxBinds
+
+/**
+ * The [BinderConfig] for Android SQLite.
+ *
+ * @property db the SQLite database that is being written to.
+ * @property batchStatement the statement that will be executed. For SQLite it will be either REPLACE INTO TABLE_NAME or
+ * INSERT INTO TABLE_NAME
+ *
+ * @constructor
+ *
+ * @param fieldsPerItem the number of fields per row to be inserted.
+ * @param maxFields the maximum number of binds that can happen per statement.
+ */
+class SQLiteBinderConfig(
+ private val db: SQLiteDatabase,
+ private val batchStatement: String,
+ override val fieldsPerItem: Int,
+ override val maxFields: Int
+) : BinderConfig {
+
+
+ /**
+ * The maximum number of items that can be inserted per statement.
+ */
+ override val maxInsertSize= if (fieldsPerItem > 0) maxFields / fieldsPerItem else 1
+
+ /**
+ * For a SQLite insert statement each item has the format (?, ?, ?)
+ */
+ private val objectStatement = (1..fieldsPerItem).joinToString(
+ prefix = "(",
+ transform = { "?" },
+ separator = ",",
+ postfix= ")"
+ )
+
+ override val maxInsertBinder: Binder by lazy {
+ buildBinder(maxInsertSize)
+ }
+
+ override val startIndex: Int = sqlAndroidPreparedStatementStartIndex
+
+ override fun buildBinder(insertCount: Int): Binder {
+ val allObjects = (1..insertCount).joinToString(",") { objectStatement }
+ val compiledStatement = db.compileStatement("$batchStatement $allObjects")
+ return AndroidSQLiteBinder(compiledStatement)
+ }
+
+ companion object {
+
+ /**
+ * Creates a statement generator for batch replace statements.
+ *
+ * @param db the database to perform the replaces on.
+ * @param tableName the name of the table to preform the replaces on.
+ * @param columnCount the number of columns that will be replaced per row.
+ * @param maxBinds the total number of values that can be bound to the statement.
+ */
+ fun getReplaceConfig(
+ db: SQLiteDatabase,
+ tableName: String,
+ columnCount: Int,
+ maxBinds: Int = sqlMaxBinds
+ ) = SQLiteBinderConfig(
+ db,
+ "REPLACE INTO $tableName VALUES",
+ columnCount,
+ maxBinds
+ )
+
+ /**
+ * Creates a statement generator for batch insert statements.
+ *
+ * @param db the database to perform the insert on.
+ * @param tableName the name of the table to preform the insert on.
+ * @param columnCount the number of columns that will be insert per row.
+ * @param maxBinds the total number of values that can be bound to the statement.
+ */
+ fun getInsertConfig(
+ db: SQLiteDatabase,
+ tableName: String,
+ columnCount: Int,
+ maxBinds: Int = sqlMaxBinds
+ ) = SQLiteBinderConfig(
+ db,
+ "INSERT INTO $tableName VALUES",
+ columnCount,
+ maxBinds
+ )
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/test/java/co/yello/db/batchlight/BatchBinderTest.kt b/batchlight/src/test/java/co/yello/db/batchlight/BatchBinderTest.kt
new file mode 100644
index 0000000..53c7bf3
--- /dev/null
+++ b/batchlight/src/test/java/co/yello/db/batchlight/BatchBinderTest.kt
@@ -0,0 +1,110 @@
+package co.yello.db.batchlight
+
+import io.mockk.*
+import io.mockk.impl.annotations.MockK
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class BatchBinderTest {
+
+ @MockK
+ lateinit var binder: Binder
+
+ @Before
+ fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true)
+
+ private val startIndex = sqlAndroidPreparedStatementStartIndex
+
+ @Test
+ fun `Test init clears bindings and sets position to 1`() {
+ val binder = BatchBinder(binder, startIndex)
+ verify { this@BatchBinderTest.binder.clear() }
+ assertEquals(1, binder.currentBindIndex)
+ }
+
+ @Test
+ fun `Test bindLong binds and increments the position` () {
+ val binder = BatchBinder(binder, startIndex)
+ binder.bindLong(1)
+ binder.bindLong(2)
+ binder.bindLong(3)
+
+ verifyOrder {
+ this@BatchBinderTest.binder.bindLong(1, 1)
+ this@BatchBinderTest.binder.bindLong(2, 2)
+ this@BatchBinderTest.binder.bindLong(3, 3)
+ }
+
+ assertEquals(4, binder.currentBindIndex)
+ }
+
+ @Test
+ fun `Test bindNull binds and increments the position`() {
+ val binder = BatchBinder(binder, startIndex)
+ binder.bindNull()
+ binder.bindNull()
+ binder.bindNull()
+
+ verifyOrder {
+ this@BatchBinderTest.binder.bindNull(1)
+ this@BatchBinderTest.binder.bindNull(2)
+ this@BatchBinderTest.binder.bindNull(3)
+ }
+
+ assertEquals(4, binder.currentBindIndex)
+ }
+
+ @Test
+ fun `Test bindDouble binds and increments the position`() {
+ val binder = BatchBinder(binder, startIndex)
+ binder.bindDouble(1.0)
+ binder.bindDouble(2.0)
+ binder.bindDouble(3.0)
+
+ verifyOrder {
+ this@BatchBinderTest.binder.bindDouble(1, 1.0)
+ this@BatchBinderTest.binder.bindDouble(2, 2.0)
+ this@BatchBinderTest.binder.bindDouble(3, 3.0)
+ }
+
+ assertEquals(4, binder.currentBindIndex)
+ }
+
+ @Test
+ fun `Test bindString binds and increments the position`() {
+ val binder = BatchBinder(binder, startIndex)
+ binder.bindString("1")
+ binder.bindString("2")
+ binder.bindString("3")
+
+ verifyOrder {
+ this@BatchBinderTest.binder.bindString(1, "1")
+ this@BatchBinderTest.binder.bindString(2, "2")
+ this@BatchBinderTest.binder.bindString(3, "3")
+ }
+
+ assertEquals(4, binder.currentBindIndex)
+ }
+
+ @Test
+ fun `Test bindBlob binds and increments the position`() {
+ val binder = BatchBinder(binder, startIndex)
+
+ val oneByte = ByteArray(1)
+ val twoByte = ByteArray(2)
+ val threeByte = ByteArray(3)
+
+ binder.bindBlob(oneByte)
+ binder.bindBlob(twoByte)
+ binder.bindBlob(threeByte)
+
+ verifyOrder {
+ this@BatchBinderTest.binder.bindBlob(1, oneByte)
+ this@BatchBinderTest.binder.bindBlob(2, twoByte)
+ this@BatchBinderTest.binder.bindBlob(3, threeByte)
+ }
+
+ assertEquals(4, binder.currentBindIndex)
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/test/java/co/yello/db/batchlight/BatchStatementTest.kt b/batchlight/src/test/java/co/yello/db/batchlight/BatchStatementTest.kt
new file mode 100644
index 0000000..b1d0d82
--- /dev/null
+++ b/batchlight/src/test/java/co/yello/db/batchlight/BatchStatementTest.kt
@@ -0,0 +1,138 @@
+package co.yello.db.batchlight
+
+import io.mockk.*
+import io.mockk.impl.annotations.MockK
+import org.junit.Before
+import org.junit.Test
+
+class BatchStatementTest {
+
+ @MockK
+ lateinit var maxBinder: Binder
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this, relaxUnitFun = true)
+ }
+
+ @Test
+ fun `Test bindEmptyList does nothing`() {
+ val binderConfig = mockBinderConfig(0)
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ batchStatement.execute(listOf()) { }
+
+ verify { listOf(maxBinder) wasNot called }
+ }
+
+ @Test
+ fun `Test execute max statement`() {
+ val binderConfig = mockBinderConfig(1)
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ val insertCount = maxInsertSize
+ val list = mutableListOf()
+ for (i in 1..insertCount) { list.add(TestClass(i)) }
+
+ batchStatement.execute(list) {item ->
+ bindLong(item.id.toLong())
+ }
+
+ verifyOrder {
+ for (i in 1..insertCount/ maxInsertSize) {
+ binderConfig.maxInsertBinder
+ maxBinder.execute()
+ }
+ }
+ }
+
+ @Test
+ fun `Test execute with list size max + 1`() {
+ val statement = mockk(relaxUnitFun = true)
+ val columnCount = 1
+
+ val binderConfig = mockBinderConfig(columnCount, 2)
+ every { binderConfig.buildBinder(any()) } returns statement
+
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ // 2 of the inserts will use the max binder and the last insert will need a new binder
+ val insertCount = 5
+ val list = mutableListOf()
+ for (i in 1..insertCount) { list.add(TestClass(i)) }
+
+ batchStatement.execute(list) { item ->
+ bindLong(item.id.toLong())
+ }
+
+ verifyOrder {
+ for (i in 1..insertCount/ (maxInsertSize/columnCount)) {
+ binderConfig.maxInsertBinder
+ maxBinder.execute()
+ }
+ binderConfig.buildBinder(insertCount.rem(maxInsertSize))
+ statement.execute()
+ }
+ }
+
+ @Test
+ fun `Test binder lambda gets executed`() {
+ val binderConfig = mockBinderConfig(1)
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ val insertCount = 2
+ val list = mutableListOf()
+ for (i in 1..insertCount) { list.add(TestClass(i)) }
+
+ batchStatement.execute(list) {
+ bindLong(it.id.toLong())
+ }
+
+ verifyOrder {
+ for (i in 1..insertCount/ maxInsertSize) {
+ binderConfig.maxInsertBinder
+ maxBinder.bindLong(1, i*2L-1)
+ maxBinder.bindLong(2, i*2L)
+ maxBinder.execute()
+ }
+ }
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `Test too many binds in the lambda throws exception`() {
+ val binderConfig = mockBinderConfig(0, 1)
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ batchStatement.execute(mutableListOf(TestObject)) {
+ bindLong(1)
+ }
+ }
+
+ @Test(expected = IllegalStateException::class)
+ fun `Test too few binds in the lambda throws exception`() {
+ val binderConfig = mockBinderConfig(1, 1)
+
+ val batchStatement = BatchStatement(binderConfig)
+
+ batchStatement.execute(mutableListOf(TestClass(1))) {}
+ }
+
+ private fun mockBinderConfig(fieldsPer: Int, maxInsert: Int = maxInsertSize) = mockk().apply {
+ every { maxInsertBinder } returns maxBinder
+ every { fieldsPerItem } returns fieldsPer
+ every { maxInsertSize } returns maxInsert
+ every { startIndex } returns 1
+ }
+
+ companion object {
+ private const val maxInsertSize = 2
+ }
+
+ object TestObject
+ data class TestClass(val id: Int)
+}
diff --git a/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinderTest.kt b/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinderTest.kt
new file mode 100644
index 0000000..6f77413
--- /dev/null
+++ b/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/AndroidSQLiteBinderTest.kt
@@ -0,0 +1,83 @@
+package co.yello.db.batchlight.androidsqlite
+
+import android.database.sqlite.SQLiteStatement
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+
+import org.junit.Assert.*
+
+class AndroidSQLiteBinderTest {
+ @MockK
+ lateinit var compiledStatement: SQLiteStatement
+
+ private lateinit var androidSQLiteBinder: AndroidSQLiteBinder
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this, relaxUnitFun = true)
+ androidSQLiteBinder = AndroidSQLiteBinder(compiledStatement)
+ }
+
+ @Test
+ fun execute() {
+ androidSQLiteBinder.execute()
+ verify { compiledStatement.execute() }
+ }
+
+ @Test
+ fun clearBindings() {
+ androidSQLiteBinder.clear()
+ verify { compiledStatement.clearBindings() }
+ }
+
+ @Test
+ fun bindLong() {
+ androidSQLiteBinder.bindLong(1, 1)
+ verify { compiledStatement.bindLong(1, 1) }
+ }
+
+ @Test
+ fun bindString() {
+ androidSQLiteBinder.bindString(1, "")
+ verify { compiledStatement.bindString(1, "") }
+ }
+
+ @Test
+ fun bindDouble() {
+ androidSQLiteBinder.bindDouble(1, 0.0)
+ verify { compiledStatement.bindDouble(1, 0.0) }
+ }
+
+ @Test
+ fun bindBlob() {
+ val byteArray = ByteArray(1)
+ androidSQLiteBinder.bindBlob(1, byteArray)
+ verify { compiledStatement.bindBlob(1, byteArray) }
+ }
+
+ @Test
+ fun bindNull() {
+ androidSQLiteBinder.bindNull(1)
+ verify { compiledStatement.bindNull(1) }
+ }
+
+ @Test
+ fun testHashCode() {
+ assertEquals(compiledStatement.hashCode(), androidSQLiteBinder.hashCode())
+ }
+
+ @Test
+ fun `Test two AndroidSQLiteStatements with the same prepared statement are equal` () {
+ assertEquals(androidSQLiteBinder, AndroidSQLiteBinder(compiledStatement))
+ }
+
+ @Test
+ fun `Test two AndroidSQLiteStatements with different prepared statement are equal` () {
+ val compiledStatement2 = mockk()
+ assertNotEquals(androidSQLiteBinder, AndroidSQLiteBinder(compiledStatement2))
+ }
+}
\ No newline at end of file
diff --git a/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfigTest.kt b/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfigTest.kt
new file mode 100644
index 0000000..1188e23
--- /dev/null
+++ b/batchlight/src/test/java/co/yello/db/batchlight/androidsqlite/SQLiteBinderConfigTest.kt
@@ -0,0 +1,77 @@
+package co.yello.db.batchlight.androidsqlite
+
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteStatement
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.verify
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+class SQLiteBinderConfigTest {
+
+ @MockK
+ lateinit var sqLiteDatabase: SQLiteDatabase
+ @MockK
+ lateinit var compiledStatement: SQLiteStatement
+
+ @Before
+ fun setUp() = MockKAnnotations.init(this, relaxUnitFun = true)
+
+ @Test
+ fun `Test maxInsertBinder returns a binder with the maximum number of binds`() {
+ every { sqLiteDatabase.compileStatement("$insertSQL (?),(?)") } returns compiledStatement
+ val generator = SQLiteBinderConfig(sqLiteDatabase,
+ insertSQL, 1, 2)
+
+ assertEquals(AndroidSQLiteBinder(compiledStatement), generator.maxInsertBinder)
+ }
+
+ @Test
+ fun `Test generateBinder works with column size of 1`() {
+ every { sqLiteDatabase.compileStatement("$insertSQL (?)") } returns compiledStatement
+
+ val generator = SQLiteBinderConfig(sqLiteDatabase, insertSQL, 1, 2)
+
+ assertEquals(AndroidSQLiteBinder(compiledStatement), generator.buildBinder(1))
+ }
+
+ @Test
+ fun `Test generateStatement works with column size greater than 1`() {
+ every { sqLiteDatabase.compileStatement("$insertSQL (?,?),(?,?)") } returns compiledStatement
+
+ val generator = SQLiteBinderConfig(sqLiteDatabase,
+ insertSQL, 2, 2)
+
+ assertEquals(AndroidSQLiteBinder(compiledStatement), generator.buildBinder(2))
+ }
+
+ @Suppress("SyntaxError")
+ @Test
+ fun `Test getReplaceGenerator uses replace into to compile the query`() {
+ val tableName = "MediumObjects"
+ val expectedSql = "REPLACE INTO $tableName VALUES (?)"
+ every { sqLiteDatabase.compileStatement(expectedSql) } returns compiledStatement
+
+ SQLiteBinderConfig.getReplaceConfig(sqLiteDatabase, tableName, 1, 1).maxInsertBinder
+
+ verify { sqLiteDatabase.compileStatement(expectedSql) }
+ }
+
+ @Test
+ fun `Test getReplaceGenerator uses insert into to compile the query`() {
+ val tableName = "MediumObjects"
+ val expectedSql = "INSERT INTO $tableName VALUES (?)"
+ every { sqLiteDatabase.compileStatement(expectedSql) } returns compiledStatement
+
+ SQLiteBinderConfig.getInsertConfig(sqLiteDatabase, tableName, 1, 1).maxInsertBinder
+
+ verify { sqLiteDatabase.compileStatement(expectedSql) }
+ }
+
+ companion object {
+ private const val insertSQL = "INSERT INTO TABLE EXAMPLE"
+ }
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..e75473e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,26 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext.kotlin_version = '1.3.30'
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.4.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
+ classpath 'org.jetbrains.dokka:dokka-android-gradle-plugin:0.9.18'
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/docs/alltypes/index.html b/docs/alltypes/index.html
new file mode 100644
index 0000000..6978b14
--- /dev/null
+++ b/docs/alltypes/index.html
@@ -0,0 +1,56 @@
+
+
+
+alltypes - batchlight
+
+
+
+
Creates a BatchBinder with the given binder. The binder will be cleared on creation to ensure
+no pre existing values will be bound unintentionally.
+
Constructor
+Creates a BatchBinder with the given binder. The binder will be cleared on creation to ensure
+no pre existing values will be bound unintentionally.
Chunks the list into the maximum sized list that can be inserted at a time, then executes the insert
+with the bindItemFunction passed into the class.
+
Parameters
+
+itemsToInsert - The collection of items of type T to batch insert into the database.
+
+bindItemFunction -
+
A function that performs the binds for each field in the T passed to the function.
+The function is an extension on BatchBinder so all bind functions can be accessed from within.
+
+
+
When binding the order of the bind calls should match the structure of the table inserting.
+
+
+
Example usage:
+
+
+
A simple class would look something like
+
+
+
data class BatchClass(val id: Int, val text: String, val num: Int)
+
+
+
The SQL Create statement for the corresponding table to that object would be:
+
+
+
CREATE TABLE BatchClass (
+ id INTEGER,
+ text TEXT,
+ num INTEGER
+)
+
+
+
The Binder generated for this table will will have the params (?,?,?). To correctly bind the three values
+to their correct ? in the prepared statement the function would be: