diff --git a/src/common/pom.xml b/src/common/pom.xml
index 110ad24..8c65730 100644
--- a/src/common/pom.xml
+++ b/src/common/pom.xml
@@ -78,17 +78,16 @@
             2.1.0
             test
         
-        
-            org.junit.jupiter
-            junit-jupiter
-            5.10.0
-            test
-        
         
             org.jetbrains.kotlin
             kotlin-stdlib
             2.1.0
         
+        
+            org.jetbrains.kotlin
+            kotlin-reflect
+            2.0.20
+        
     
 
 
\ No newline at end of file
diff --git a/src/common/src/main/kotlin/AutoId.kt b/src/common/src/main/kotlin/AutoId.kt
index f42e5a0..cf34802 100644
--- a/src/common/src/main/kotlin/AutoId.kt
+++ b/src/common/src/main/kotlin/AutoId.kt
@@ -1,5 +1,7 @@
 package solutions.bitbadger.documents.common
 
+import kotlin.reflect.full.*
+
 /**
  * Strategies for automatic document IDs
  */
@@ -32,8 +34,46 @@ enum class AutoId {
         fun generateRandomString(length: Int): String =
             kotlin.random.Random.nextBytes((length + 2) / 2)
                 .joinToString("") { String.format("%02x", it) }
-                .substring(0, length - 1)
+                .substring(0, length)
 
-        // TODO: fun  needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean
+        /**
+         * Determine if a document needs an automatic ID applied
+         *
+         * @param strategy The auto ID strategy for which the document is evaluated
+         * @param document The document whose need of an automatic ID should be determined
+         * @param idProp The name of the document property containing the ID
+         * @return `true` if the document needs an automatic ID, `false` if not
+         * @throws IllegalArgumentException If bad input prevents the determination
+         */
+        fun  needsAutoId(strategy: AutoId, document: T, idProp: String): Boolean {
+            if (document == null) throw IllegalArgumentException("document cannot be null")
+
+            if (strategy == DISABLED) return false;
+
+            val id = document!!::class.memberProperties.find { it.name == idProp }
+            if (id == null) throw IllegalArgumentException("$idProp not found in document")
+
+            if (strategy == NUMBER) {
+                if (id.returnType == Byte::class.createType()) {
+                    return id.call(document) == 0.toByte()
+                }
+                if (id.returnType == Short::class.createType()) {
+                    return id.call(document) == 0.toShort()
+                }
+                if (id.returnType == Int::class.createType()) {
+                    return id.call(document) == 0
+                }
+                if (id.returnType == Long::class.createType()) {
+                    return id.call(document) == 0.toLong()
+                }
+                throw IllegalArgumentException("$idProp was not a number; cannot auto-generate number ID")
+            }
+
+            if (id.returnType == String::class.createType()) {
+                return id.call(document) == ""
+            }
+
+            throw IllegalArgumentException("$idProp was not a string; cannot auto-generate UUID or random string")
+        }
     }
 }
diff --git a/src/common/src/main/kotlin/Configuration.kt b/src/common/src/main/kotlin/Configuration.kt
index 1d035f0..ca82639 100644
--- a/src/common/src/main/kotlin/Configuration.kt
+++ b/src/common/src/main/kotlin/Configuration.kt
@@ -5,7 +5,7 @@ object Configuration {
     // TODO: var jsonOpts = Json { some cool options }
 
     /** The field in which a document's ID is stored */
-    var idField = "Id"
+    var idField = "id"
 
     /** The automatic ID strategy to use */
     var autoIdStrategy = AutoId.DISABLED
diff --git a/src/common/src/main/kotlin/Query.kt b/src/common/src/main/kotlin/Query.kt
index 28e553d..31e6a67 100644
--- a/src/common/src/main/kotlin/Query.kt
+++ b/src/common/src/main/kotlin/Query.kt
@@ -44,12 +44,12 @@ object Query {
          * @param dialect The SQL dialect to use when creating this index
          * @return A query to create the field index
          */
-        fun ensureIndexOn(tableName: String, indexName: String, fields: List, dialect: Dialect): String {
+        fun ensureIndexOn(tableName: String, indexName: String, fields: Collection, dialect: Dialect): String {
             val (_, tbl) = splitSchemaAndTable(tableName)
             val jsonFields = fields.joinToString(", ") {
                 val parts = it.split(' ')
                 val direction = if (parts.size > 1) " ${parts[1]}" else ""
-                "(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ") $direction"
+                "(" + Field.nameToPath(parts[0], dialect, FieldFormat.SQL) + ")$direction"
             }
             return "CREATE INDEX IF NOT EXISTS idx_${tbl}_$indexName ON $tableName ($jsonFields)"
         }
@@ -81,8 +81,7 @@ object Query {
      * @return A query to save a document
      */
     fun save(tableName: String): String =
-        String.format("INSERT INTO %s VALUES (:data) ON CONFLICT ((data->>'%s')) DO UPDATE SET data = EXCLUDED.data",
-            tableName, Configuration.idField)
+        "${insert(tableName)} ON CONFLICT ((data->>'${Configuration.idField}')) DO UPDATE SET data = EXCLUDED.data"
 
     /**
      * Query to count documents in a table (this query has no `WHERE` clause)
@@ -137,7 +136,7 @@ object Query {
      * @param dialect The SQL dialect for the generated clause
      * @return An `ORDER BY` clause for the given fields
      */
-    fun orderBy(fields: List>, dialect: Dialect): String {
+    fun orderBy(fields: Collection>, dialect: Dialect): String {
         if (fields.isEmpty()) return ""
         val orderFields = fields.joinToString(", ") {
             val (field, direction) =
@@ -165,6 +164,6 @@ object Query {
                 }
             "$path${direction ?: ""}"
         }
-        return "ORDER BY $orderFields"
+        return " ORDER BY $orderFields"
     }
 }
diff --git a/src/common/src/test/kotlin/AutoIdTest.kt b/src/common/src/test/kotlin/AutoIdTest.kt
index 9552a81..038d322 100644
--- a/src/common/src/test/kotlin/AutoIdTest.kt
+++ b/src/common/src/test/kotlin/AutoIdTest.kt
@@ -2,16 +2,159 @@ package solutions.bitbadger.documents.common
 
 import org.junit.jupiter.api.DisplayName
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
 import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
+import kotlin.test.assertFalse
+import kotlin.test.assertNotEquals
+import kotlin.test.assertTrue
 
 class AutoIdTest {
 
     @Test
     @DisplayName("Generates a UUID string")
-    fun testGenerateUUID() {
-        val generated = AutoId.generateUUID()
-        assertNotNull(generated, "The UUID string should not have been null")
-        assertEquals(32, generated.length, "The UUID should have been a 32-character string")
+    fun generateUUID() {
+        assertEquals(32, AutoId.generateUUID().length, "The UUID should have been a 32-character string")
     }
-}
\ No newline at end of file
+
+    @Test
+    @DisplayName("Generates a random hex character string of an even length")
+    fun generateRandomStringEven() {
+        val result = AutoId.generateRandomString(8)
+        assertEquals(8, result.length, "There should have been 8 characters in $result")
+    }
+
+    @Test
+    @DisplayName("Generates a random hex character string of an odd length")
+    fun generateRandomStringOdd() {
+        val result = AutoId.generateRandomString(11)
+        assertEquals(11, result.length, "There should have been 11 characters in $result")
+    }
+
+    @Test
+    @DisplayName("Generates different random hex character strings")
+    fun generateRandomStringIsRandom() {
+        val result1 = AutoId.generateRandomString(16)
+        val result2 = AutoId.generateRandomString(16)
+        assertNotEquals(result1, result2, "There should have been 2 different strings generated")
+    }
+
+    @Test
+    @DisplayName("needsAutoId fails for null document")
+    fun needsAutoIdFailsForNullDocument() {
+        assertThrows { AutoId.needsAutoId(AutoId.DISABLED, null, "id") }
+    }
+
+    @Test
+    @DisplayName("needsAutoId fails for missing ID property")
+    fun needsAutoIdFailsForMissingId() {
+        assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(0), "Id") }
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false if disabled")
+    fun needsAutoIdFalseIfDisabled() {
+        assertFalse(AutoId.needsAutoId(AutoId.DISABLED, "", ""), "Disabled Auto ID should always return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for Number strategy and byte ID of 0")
+    fun needsAutoIdTrueForByteWithZero() {
+        assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(0), "id"), "Number Auto ID with 0 should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for Number strategy and byte ID of non-0")
+    fun needsAutoIdFalseForByteWithNonZero() {
+        assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ByteIdClass(77), "id"),
+            "Number Auto ID with 77 should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for Number strategy and short ID of 0")
+    fun needsAutoIdTrueForShortWithZero() {
+        assertTrue(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(0), "id"), "Number Auto ID with 0 should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for Number strategy and short ID of non-0")
+    fun needsAutoIdFalseForShortWithNonZero() {
+        assertFalse(AutoId.needsAutoId(AutoId.NUMBER, ShortIdClass(31), "id"),
+            "Number Auto ID with 31 should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for Number strategy and int ID of 0")
+    fun needsAutoIdTrueForIntWithZero() {
+        assertTrue(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(0), "id"), "Number Auto ID with 0 should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for Number strategy and int ID of non-0")
+    fun needsAutoIdFalseForIntWithNonZero() {
+        assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(6), "id"), "Number Auto ID with 6 should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for Number strategy and long ID of 0")
+    fun needsAutoIdTrueForLongWithZero() {
+        assertTrue(AutoId.needsAutoId(AutoId.NUMBER, LongIdClass(0), "id"), "Number Auto ID with 0 should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for Number strategy and long ID of non-0")
+    fun needsAutoIdFalseForLongWithNonZero() {
+        assertFalse(AutoId.needsAutoId(AutoId.NUMBER, IntIdClass(2), "id"), "Number Auto ID with 2 should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId fails for Number strategy and non-number ID")
+    fun needsAutoIdFailsForNumberWithStringId() {
+        assertThrows { AutoId.needsAutoId(AutoId.NUMBER, StringIdClass(""), "id") }
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for UUID strategy and blank ID")
+    fun needsAutoIdTrueForUUIDWithBlank() {
+        assertTrue(AutoId.needsAutoId(AutoId.UUID, StringIdClass(""), "id"),
+            "UUID Auto ID with blank should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for UUID strategy and non-blank ID")
+    fun needsAutoIdFalseForUUIDNotBlank() {
+        assertFalse(AutoId.needsAutoId(AutoId.UUID, StringIdClass("howdy"), "id"),
+            "UUID Auto ID with non-blank should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId fails for UUID strategy and non-string ID")
+    fun needsAutoIdFailsForUUIDNonString() {
+        assertThrows { AutoId.needsAutoId(AutoId.UUID, IntIdClass(5), "id") }
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns true for Random String strategy and blank ID")
+    fun needsAutoIdTrueForRandomWithBlank() {
+        assertTrue(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass(""), "id"),
+            "Random String Auto ID with blank should return true")
+    }
+
+    @Test
+    @DisplayName("needsAutoId returns false for Random String strategy and non-blank ID")
+    fun needsAutoIdFalseForRandomNotBlank() {
+        assertFalse(AutoId.needsAutoId(AutoId.RANDOM_STRING, StringIdClass("full"), "id"),
+            "Random String Auto ID with non-blank should return false")
+    }
+
+    @Test
+    @DisplayName("needsAutoId fails for Random String strategy and non-string ID")
+    fun needsAutoIdFailsForRandomNonString() {
+        assertThrows { AutoId.needsAutoId(AutoId.RANDOM_STRING, ShortIdClass(55), "id") }
+    }
+}
+
+data class ByteIdClass(var id: Byte)
+data class ShortIdClass(var id: Short)
+data class IntIdClass(var id: Int)
+data class LongIdClass(var id: Long)
+data class StringIdClass(var id: String)
diff --git a/src/common/src/test/kotlin/ConfigurationTest.kt b/src/common/src/test/kotlin/ConfigurationTest.kt
new file mode 100644
index 0000000..29a2a61
--- /dev/null
+++ b/src/common/src/test/kotlin/ConfigurationTest.kt
@@ -0,0 +1,26 @@
+package solutions.bitbadger.documents.common
+
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+
+class ConfigurationTest {
+
+    @Test
+    @DisplayName("Default ID field is `id`")
+    fun defaultIdField() {
+        assertEquals("id", Configuration.idField, "Default ID field incorrect")
+    }
+
+    @Test
+    @DisplayName("Default Auto ID strategy is `DISABLED`")
+    fun defaultAutoId() {
+        assertEquals(AutoId.DISABLED, Configuration.autoIdStrategy, "Default Auto ID strategy should be `disabled`")
+    }
+
+    @Test
+    @DisplayName("Default ID string length should be 16")
+    fun defaultIdStringLength() {
+        assertEquals(16, Configuration.idStringLength, "Default ID string length should be 16")
+    }
+}
diff --git a/src/common/src/test/kotlin/QueryTest.kt b/src/common/src/test/kotlin/QueryTest.kt
new file mode 100644
index 0000000..565243d
--- /dev/null
+++ b/src/common/src/test/kotlin/QueryTest.kt
@@ -0,0 +1,181 @@
+package solutions.bitbadger.documents.common
+
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import kotlin.test.assertEquals
+
+class QueryTest {
+
+    /** Test table name */
+    private val tbl = "test_table"
+
+    @Test
+    @DisplayName("statementWhere generates correctly")
+    fun statementWhere() {
+        assertEquals("x WHERE y", Query.statementWhere("x", "y"), "Statements not combined correctly")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureTableFor generates correctly")
+    fun ensureTableFor() {
+        assertEquals("CREATE TABLE IF NOT EXISTS my.table (data JSONB NOT NULL)",
+            Query.Definition.ensureTableFor("my.table", "JSONB"), "CREATE TABLE statement not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureKey generates correctly with schema")
+    fun ensureKeyWithSchema() {
+        assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_table_key ON test.table ((data->>'id'))",
+            Query.Definition.ensureKey("test.table", Dialect.POSTGRESQL),
+            "CREATE INDEX for key statement with schema not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureKey generates correctly without schema")
+    fun ensureKeyWithoutSchema() {
+        assertEquals("CREATE UNIQUE INDEX IF NOT EXISTS idx_${tbl}_key ON $tbl ((data->>'id'))",
+            Query.Definition.ensureKey(tbl, Dialect.SQLITE),
+            "CREATE INDEX for key statement without schema not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureIndexOn generates multiple fields and directions")
+    fun ensureIndexOnMultipleFields() {
+        assertEquals(
+            "CREATE INDEX IF NOT EXISTS idx_table_gibberish ON test.table ((data->>'taco'), (data->>'guac') DESC, (data->>'salsa') ASC)",
+            Query.Definition.ensureIndexOn("test.table", "gibberish", listOf("taco", "guac DESC", "salsa ASC"),
+                Dialect.POSTGRESQL),
+            "CREATE INDEX for multiple field statement not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureIndexOn generates nested PostgreSQL field")
+    fun ensureIndexOnNestedPostgres() {
+        assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data#>>'{a,b,c}'))",
+            Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.POSTGRESQL),
+            "CREATE INDEX for nested PostgreSQL field incorrect")
+    }
+
+    @Test
+    @DisplayName("Definition.ensureIndexOn generates nested SQLite field")
+    fun ensureIndexOnNestedSQLite() {
+        assertEquals("CREATE INDEX IF NOT EXISTS idx_${tbl}_nest ON $tbl ((data->'a'->'b'->>'c'))",
+            Query.Definition.ensureIndexOn(tbl, "nest", listOf("a.b.c"), Dialect.SQLITE),
+            "CREATE INDEX for nested SQLite field incorrect")
+    }
+
+    @Test
+    @DisplayName("insert generates correctly")
+    fun insert() {
+        assertEquals("INSERT INTO $tbl VALUES (:data)", Query.insert(tbl), "INSERT statement not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("save generates correctly")
+    fun save() {
+        assertEquals("INSERT INTO $tbl VALUES (:data) ON CONFLICT ((data->>'id')) DO UPDATE SET data = EXCLUDED.data",
+            Query.save(tbl), "INSERT ON CONFLICT UPDATE statement not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("count generates correctly")
+    fun count() {
+        assertEquals("SELECT COUNT(*) AS it FROM $tbl", Query.count(tbl), "Count query not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("exists generates correctly")
+    fun exists() {
+        assertEquals("SELECT EXISTS (SELECT 1 FROM $tbl WHERE turkey) AS it", Query.exists(tbl, "turkey"),
+            "Exists query not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("find generates correctly")
+    fun find() {
+        assertEquals("SELECT data FROM $tbl", Query.find(tbl), "Find query not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("update generates successfully")
+    fun update() {
+        assertEquals("UPDATE $tbl SET data = :data", Query.update(tbl), "Update query not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("delete generates successfully")
+    fun delete() {
+        assertEquals("DELETE FROM $tbl", Query.delete(tbl), "Delete query not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates for no fields")
+    fun orderByNone() {
+        assertEquals("", Query.orderBy(listOf(), Dialect.POSTGRESQL), "ORDER BY should have been blank (PostgreSQL)")
+        assertEquals("", Query.orderBy(listOf(), Dialect.SQLITE), "ORDER BY should have been blank (SQLite)")
+    }
+
+    @Test
+    @DisplayName("orderBy generates single, no direction for PostgreSQL")
+    fun orderBySinglePostgres() {
+        assertEquals(" ORDER BY data->>'TestField'",
+            Query.orderBy(listOf(Field.named("TestField")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates single, no direction for SQLite")
+    fun orderBySingleSQLite() {
+        assertEquals(" ORDER BY data->>'TestField'", Query.orderBy(listOf(Field.named("TestField")), Dialect.SQLITE),
+            "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates multiple with direction for PostgreSQL")
+    fun orderByMultiplePostgres() {
+        assertEquals(" ORDER BY data#>>'{Nested,Test,Field}' DESC, data->>'AnotherField', data->>'It' DESC",
+            Query.orderBy(
+                listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
+                Dialect.POSTGRESQL),
+            "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates multiple with direction for SQLite")
+    fun orderByMultipleSQLite() {
+        assertEquals(" ORDER BY data->'Nested'->'Test'->>'Field' DESC, data->>'AnotherField', data->>'It' DESC",
+            Query.orderBy(
+                listOf(Field.named("Nested.Test.Field DESC"), Field.named("AnotherField"), Field.named("It DESC")),
+                Dialect.SQLITE),
+            "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates numeric ordering PostgreSQL")
+    fun orderByNumericPostgres() {
+        assertEquals(" ORDER BY (data->>'Test')::numeric",
+            Query.orderBy(listOf(Field.named("n:Test")), Dialect.POSTGRESQL), "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates numeric ordering for SQLite")
+    fun orderByNumericSQLite() {
+        assertEquals(" ORDER BY data->>'Test'", Query.orderBy(listOf(Field.named("n:Test")), Dialect.SQLITE),
+            "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates case-insensitive ordering for PostgreSQL")
+    fun orderByCIPostgres() {
+        assertEquals(" ORDER BY LOWER(data#>>'{Test,Field}') DESC NULLS FIRST",
+            Query.orderBy(listOf(Field.named("i:Test.Field DESC NULLS FIRST")), Dialect.POSTGRESQL),
+            "ORDER BY not constructed correctly")
+    }
+
+    @Test
+    @DisplayName("orderBy generates case-insensitive ordering for SQLite")
+    fun orderByCISQLite() {
+        assertEquals(" ORDER BY data->'Test'->>'Field' COLLATE NOCASE ASC NULLS LAST",
+            Query.orderBy(listOf(Field.named("i:Test.Field ASC NULLS LAST")), Dialect.SQLITE),
+            "ORDER BY not constructed correctly")
+    }
+}