test(sql): cover DirectoryEntryCodecTest
This commit is contained in:
1 parent
17a9f92eef
commit
910c0761fa
1 file changed
+167
@@ -0,0 +1,167 @@
|
|||||||
|
package kestrel.store.sql
|
||||||
|
|
||||||
|
import kestrel.catalog.Column
|
||||||
|
import kestrel.catalog.UniqueConstraint
|
||||||
|
import kestrel.storage.ColumnType
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class DirectoryEntryCodecTest {
|
||||||
|
@Test
|
||||||
|
fun roundTripsATableWithNoIndexes() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "widgets",
|
||||||
|
columns = listOf(Column("id", ColumnType.Integer, notNull = true, primaryKey = false)),
|
||||||
|
dataHeaderPageId = 3L,
|
||||||
|
primaryKeyIndexRootPageId = null,
|
||||||
|
secondaryIndexes = emptyList()
|
||||||
|
)
|
||||||
|
assertEquals(entry, DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripsATableWithAPrimaryKeyAndSecondaryIndexes() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "orders",
|
||||||
|
columns = listOf(
|
||||||
|
Column("id", ColumnType.Integer, notNull = true, primaryKey = true),
|
||||||
|
Column("customer", ColumnType.Text, notNull = false, primaryKey = false),
|
||||||
|
Column("total", ColumnType.Real, notNull = true, primaryKey = false),
|
||||||
|
Column("shipped", ColumnType.Boolean, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
dataHeaderPageId = 10L,
|
||||||
|
primaryKeyIndexRootPageId = 11L,
|
||||||
|
secondaryIndexes = listOf(
|
||||||
|
SecondaryIndexEntry("idx_customer", listOf("customer"), 12L),
|
||||||
|
SecondaryIndexEntry("idx_total", listOf("total"), 13L)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assertEquals(entry, DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripsDecimalColumnsWithAndWithoutExplicitPrecisionAndDateColumns() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "priced",
|
||||||
|
columns = listOf(
|
||||||
|
Column("id", ColumnType.Integer, notNull = true, primaryKey = true),
|
||||||
|
Column("bare", ColumnType.Decimal, notNull = false, primaryKey = false),
|
||||||
|
Column("scaled", ColumnType.Decimal, notNull = false, primaryKey = false, precision = 12, scale = 4),
|
||||||
|
Column("placed", ColumnType.Date, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
dataHeaderPageId = 4L,
|
||||||
|
primaryKeyIndexRootPageId = 5L,
|
||||||
|
secondaryIndexes = emptyList()
|
||||||
|
)
|
||||||
|
val decoded = DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry))
|
||||||
|
assertEquals(entry, decoded)
|
||||||
|
assertEquals(null, decoded.columns[1].precision)
|
||||||
|
assertEquals(12, decoded.columns[2].precision)
|
||||||
|
assertEquals(4, decoded.columns[2].scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripsATableWithACompositePrimaryKeyAndEmptyOnesDoNotAddAnyBytes() {
|
||||||
|
val withComposite = TableDirectoryEntry(
|
||||||
|
tableName = "order_lines",
|
||||||
|
columns = listOf(
|
||||||
|
Column("order_id", ColumnType.Integer, notNull = true, primaryKey = false),
|
||||||
|
Column("line_no", ColumnType.Integer, notNull = true, primaryKey = false),
|
||||||
|
Column("sku", ColumnType.Text, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
dataHeaderPageId = 7L,
|
||||||
|
primaryKeyIndexRootPageId = 8L,
|
||||||
|
secondaryIndexes = emptyList(),
|
||||||
|
compositePrimaryKey = listOf("order_id", "line_no")
|
||||||
|
)
|
||||||
|
val decoded = DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(withComposite))
|
||||||
|
assertEquals(withComposite, decoded)
|
||||||
|
assertEquals(listOf("order_id", "line_no"), decoded.compositePrimaryKey)
|
||||||
|
|
||||||
|
val withoutComposite = withComposite.copy(compositePrimaryKey = emptyList())
|
||||||
|
assertEquals(withoutComposite, DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(withoutComposite)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripsAUniqueSecondaryIndexFlagAndDeclarativeUniqueConstraints() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "users",
|
||||||
|
columns = listOf(
|
||||||
|
Column("id", ColumnType.Integer, notNull = true, primaryKey = true),
|
||||||
|
Column("email", ColumnType.Text, notNull = false, primaryKey = false),
|
||||||
|
Column("event_id", ColumnType.Integer, notNull = false, primaryKey = false),
|
||||||
|
Column("user_id", ColumnType.Integer, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
dataHeaderPageId = 20L,
|
||||||
|
primaryKeyIndexRootPageId = 21L,
|
||||||
|
secondaryIndexes = listOf(SecondaryIndexEntry("idx_email", listOf("email"), 22L, unique = true)),
|
||||||
|
uniqueConstraints = listOf(
|
||||||
|
UniqueConstraint("uq_email_named", listOf("email")),
|
||||||
|
UniqueConstraint(null, listOf("event_id", "user_id"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val decoded = DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry))
|
||||||
|
assertEquals(entry, decoded)
|
||||||
|
assertTrue(decoded.secondaryIndexes.single().unique)
|
||||||
|
assertEquals(listOf("event_id", "user_id"), decoded.uniqueConstraints[1].columns)
|
||||||
|
assertEquals(null, decoded.uniqueConstraints[1].name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun aNonUniqueSecondaryIndexAndEmptyUniqueConstraintsDoNotAddSpuriousFlagsOnDecode() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "widgets",
|
||||||
|
columns = listOf(Column("id", ColumnType.Integer, notNull = true, primaryKey = false)),
|
||||||
|
dataHeaderPageId = 1L,
|
||||||
|
primaryKeyIndexRootPageId = null,
|
||||||
|
secondaryIndexes = listOf(SecondaryIndexEntry("idx_id", listOf("id"), 2L))
|
||||||
|
)
|
||||||
|
val decoded = DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry))
|
||||||
|
assertEquals(false, decoded.secondaryIndexes.single().unique)
|
||||||
|
assertTrue(decoded.uniqueConstraints.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun roundTripsACompositeMultiColumnSecondaryIndex() {
|
||||||
|
val entry = TableDirectoryEntry(
|
||||||
|
tableName = "order_lines",
|
||||||
|
columns = listOf(
|
||||||
|
Column("order_id", ColumnType.Integer, notNull = true, primaryKey = false),
|
||||||
|
Column("sku", ColumnType.Text, notNull = false, primaryKey = false)
|
||||||
|
),
|
||||||
|
dataHeaderPageId = 5L,
|
||||||
|
primaryKeyIndexRootPageId = null,
|
||||||
|
secondaryIndexes = listOf(SecondaryIndexEntry("idx_order_sku", listOf("order_id", "sku"), 6L, unique = true))
|
||||||
|
)
|
||||||
|
val decoded = DirectoryEntryCodec.decode(DirectoryEntryCodec.encode(entry))
|
||||||
|
assertEquals(entry, decoded)
|
||||||
|
assertEquals(listOf("order_id", "sku"), decoded.secondaryIndexes.single().columnNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun decodingAnUnknownColumnTypeFails() {
|
||||||
|
val entry = TableDirectoryEntry("t", listOf(Column("c", ColumnType.Integer, false, false)), 0L, null, emptyList())
|
||||||
|
val bytes = DirectoryEntryCodec.encode(entry).copyOf()
|
||||||
|
// Corrupt the type-keyword length byte to force an unrecognised keyword.
|
||||||
|
var offset = 0
|
||||||
|
// skip table name: length-prefix varint then bytes
|
||||||
|
val nameLength = bytes[offset].toInt() and 0x7F
|
||||||
|
offset += 1 + nameLength
|
||||||
|
offset += 1 // column count varint (small)
|
||||||
|
val colNameLength = bytes[offset].toInt() and 0x7F
|
||||||
|
offset += 1 + colNameLength
|
||||||
|
val typeLength = bytes[offset].toInt() and 0x7F
|
||||||
|
for (i in 0 until typeLength) bytes[offset + 1 + i] = 'Z'.code.toByte()
|
||||||
|
assertThrowsCodecException { DirectoryEntryCodec.decode(bytes) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertThrowsCodecException(block: () -> Unit) {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
throw AssertionError("expected decode to fail")
|
||||||
|
} catch (e: kestrel.store.codec.CodecException) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user