feat(sql): add SequenceDirectoryEntryCodec, encode
This commit is contained in:
1 parent
b74fda3e6a
commit
142f8274ed
6 files changed
+172
No files matched your search
@@ -0,0 +1,38 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.store.codec.ByteReader
|
||||
import kestrel.store.codec.ByteWriter
|
||||
|
||||
/**
|
||||
* Encodes a [SequenceDirectoryEntry] as a self-contained byte payload, mirroring
|
||||
* [DirectoryEntryCodec]/[ViewDirectoryEntryCodec]. Every numeric field uses [ByteWriter.writeZigZag]
|
||||
* rather than [ByteWriter.writeFixedLong] because MINVALUE/MAXVALUE/START WITH routinely hold
|
||||
* negative values (a descending sequence's default MAXVALUE is `-1`, and either bound can be
|
||||
* [Long.MIN_VALUE]/[Long.MAX_VALUE]) - zigzag keeps the common case (small increments, page ids)
|
||||
* compact while still round-tripping the full `Long` range.
|
||||
*/
|
||||
object SequenceDirectoryEntryCodec {
|
||||
fun encode(entry: SequenceDirectoryEntry): ByteArray {
|
||||
val writer = ByteWriter()
|
||||
writer.writeLengthPrefixed(entry.name.toByteArray(Charsets.UTF_8))
|
||||
writer.writeZigZag(entry.incrementBy)
|
||||
writer.writeZigZag(entry.minValue)
|
||||
writer.writeZigZag(entry.maxValue)
|
||||
writer.writeZigZag(entry.startValue)
|
||||
writer.writeByte(if (entry.cycle) 1 else 0)
|
||||
writer.writeZigZag(entry.positionPageId)
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
fun decode(bytes: ByteArray): SequenceDirectoryEntry {
|
||||
val reader = ByteReader(bytes)
|
||||
val name = String(reader.readLengthPrefixed(), Charsets.UTF_8)
|
||||
val incrementBy = reader.readZigZag()
|
||||
val minValue = reader.readZigZag()
|
||||
val maxValue = reader.readZigZag()
|
||||
val startValue = reader.readZigZag()
|
||||
val cycle = reader.readByte() != 0
|
||||
val positionPageId = reader.readZigZag()
|
||||
return SequenceDirectoryEntry(name, incrementBy, minValue, maxValue, startValue, cycle, positionPageId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.catalog.CheckConstraint
|
||||
import kestrel.catalog.Column
|
||||
import kestrel.catalog.ForeignKeyConstraint
|
||||
import kestrel.catalog.UniqueConstraint
|
||||
import kestrel.persistence.SqlWriter
|
||||
|
||||
/**
|
||||
* One secondary index recorded against a table's directory entry - [columnNames] has more than
|
||||
* one entry for a composite (multi-column) index. [unique] mirrors a `CREATE UNIQUE INDEX`:
|
||||
* [DiskTable] rejects an insert/update that would give two rows the same non-null tuple of
|
||||
* values across [columnNames], the same way it already does for the primary key.
|
||||
*/
|
||||
data class SecondaryIndexEntry(val name: String, val columnNames: List<String>, val rootPageId: Long, val unique: Boolean = false)
|
||||
|
||||
/**
|
||||
* The durable form of a [CheckConstraint]: its expression rendered to SQL text by
|
||||
* [SqlWriter.expressionText] rather than a bespoke expression codec - mirrors [ViewDirectoryEntry]
|
||||
* storing a view's query as `CREATE VIEW ... AS ...;` text, for the same reason (a CHECK expression
|
||||
* can nest arbitrarily deep, so a dedicated binary codec would have to mirror the whole [kestrel.sql.ast.Expression]
|
||||
* hierarchy). [DiskCatalog] re-derives the [Expression][kestrel.sql.ast.Expression] on load by parsing
|
||||
* it back through the same [kestrel.sql.lexer.Lexer]/[kestrel.sql.parser.Parser] pair every other SQL
|
||||
* entry point uses.
|
||||
*/
|
||||
data class CheckConstraintEntry(val name: String?, val expressionText: String) {
|
||||
companion object {
|
||||
fun from(check: CheckConstraint): CheckConstraintEntry = CheckConstraintEntry(check.name, SqlWriter.expressionText(check.expression))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The durable, on-disk description of one table: its schema plus every page
|
||||
* id needed to reopen its storage after a process restart. [DiskCatalog]
|
||||
* keeps one of these per table in its directory heap, re-encoding it (via
|
||||
* [DirectoryEntryCodec]) whenever a page id it references changes - most
|
||||
* often [primaryKeyIndexRootPageId] or a [SecondaryIndexEntry.rootPageId]
|
||||
* after a B+Tree split or merge moves the tree's root.
|
||||
*/
|
||||
data class TableDirectoryEntry(
|
||||
val tableName: String,
|
||||
val columns: List<Column>,
|
||||
val dataHeaderPageId: Long,
|
||||
val primaryKeyIndexRootPageId: Long?,
|
||||
val secondaryIndexes: List<SecondaryIndexEntry>,
|
||||
/**
|
||||
* A table-level `PRIMARY KEY (a, b, ...)`, empty when the table instead declares its
|
||||
* PRIMARY KEY inline on a single [Column] (tracked by [Column.primaryKey]) - mirrors
|
||||
* [kestrel.catalog.TableSchema.compositePrimaryKey].
|
||||
*/
|
||||
val compositePrimaryKey: List<String> = emptyList(),
|
||||
/**
|
||||
* Table-level or inline `UNIQUE`/`UNIQUE (a, b, ...)` constraints, mirroring
|
||||
* [kestrel.catalog.TableSchema.uniqueConstraints]. Unlike [secondaryIndexes] these are not
|
||||
* backed by a B+Tree - [DiskTable] enforces them with a full heap scan per write, exactly like
|
||||
* [kestrel.engine.Executor] does for the in-memory engine, which is what lets a single
|
||||
* constraint span more than one column without a composite-key index format.
|
||||
*/
|
||||
val uniqueConstraints: List<UniqueConstraint> = emptyList(),
|
||||
/** Table-level or column-level `CHECK (...)` constraints, mirroring [kestrel.catalog.TableSchema.checks]. */
|
||||
val checks: List<CheckConstraintEntry> = emptyList(),
|
||||
/**
|
||||
* Column-level `REFERENCES ...` or table-level `FOREIGN KEY (...) REFERENCES ...` constraints,
|
||||
* mirroring [kestrel.catalog.TableSchema.foreignKeys]. Stored as plain typed fields (unlike
|
||||
* [CheckConstraintEntry]) since [ForeignKeyConstraint] holds no arbitrary expression to render.
|
||||
*/
|
||||
val foreignKeys: List<ForeignKeyConstraint> = emptyList()
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
/**
|
||||
* The durable, on-disk description of one trigger: its name plus the exact `CREATE TRIGGER ...;`
|
||||
* text needed to rebuild its [kestrel.catalog.TriggerDefinition] after a process restart - mirrors
|
||||
* [ViewDirectoryEntry]'s own reasoning: a trigger's WHEN clause and body can be arbitrarily deep AST
|
||||
* (any INSERT/UPDATE/DELETE, each with its own WHERE/SET/VALUES), and [kestrel.persistence.SqlWriter.
|
||||
* triggerText] already knows how to render any of that back to text, so re-parsing it with the normal
|
||||
* [kestrel.sql.lexer.Lexer]/[kestrel.sql.parser.Parser] pair on [DiskCatalog.open] reuses
|
||||
* already-tested machinery instead of a second, trigger-specific AST codec.
|
||||
*/
|
||||
data class TriggerDirectoryEntry(val triggerName: String, val statementText: String)
|
||||
@@ -0,0 +1,21 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.store.codec.ByteReader
|
||||
import kestrel.store.codec.ByteWriter
|
||||
|
||||
/** Encodes a [TriggerDirectoryEntry] as a self-contained byte payload, mirroring [ViewDirectoryEntryCodec]. */
|
||||
object TriggerDirectoryEntryCodec {
|
||||
fun encode(entry: TriggerDirectoryEntry): ByteArray {
|
||||
val writer = ByteWriter()
|
||||
writer.writeLengthPrefixed(entry.triggerName.toByteArray(Charsets.UTF_8))
|
||||
writer.writeLengthPrefixed(entry.statementText.toByteArray(Charsets.UTF_8))
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
fun decode(bytes: ByteArray): TriggerDirectoryEntry {
|
||||
val reader = ByteReader(bytes)
|
||||
val triggerName = String(reader.readLengthPrefixed(), Charsets.UTF_8)
|
||||
val statementText = String(reader.readLengthPrefixed(), Charsets.UTF_8)
|
||||
return TriggerDirectoryEntry(triggerName, statementText)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
/**
|
||||
* The durable, on-disk description of one view: its name plus the exact `CREATE VIEW ... AS ...;`
|
||||
* text needed to rebuild its [kestrel.sql.ast.QueryStatement] after a process restart. Unlike
|
||||
* [TableDirectoryEntry] this stores rendered SQL text rather than a structured payload - a view's
|
||||
* defining query can be arbitrarily deep AST (joins, subqueries, set operations, CTEs), and
|
||||
* [kestrel.persistence.SqlWriter] already knows how to render any of that back to text, so
|
||||
* re-parsing that text with the normal [kestrel.sql.lexer.Lexer]/[kestrel.sql.parser.Parser] pair
|
||||
* on [DiskCatalog.open] reuses already-tested machinery instead of a second, view-specific AST codec.
|
||||
*/
|
||||
data class ViewDirectoryEntry(val viewName: String, val statementText: String)
|
||||
@@ -0,0 +1,21 @@
|
||||
package kestrel.store.sql
|
||||
|
||||
import kestrel.store.codec.ByteReader
|
||||
import kestrel.store.codec.ByteWriter
|
||||
|
||||
/** Encodes a [ViewDirectoryEntry] as a self-contained byte payload, mirroring [DirectoryEntryCodec]. */
|
||||
object ViewDirectoryEntryCodec {
|
||||
fun encode(entry: ViewDirectoryEntry): ByteArray {
|
||||
val writer = ByteWriter()
|
||||
writer.writeLengthPrefixed(entry.viewName.toByteArray(Charsets.UTF_8))
|
||||
writer.writeLengthPrefixed(entry.statementText.toByteArray(Charsets.UTF_8))
|
||||
return writer.toByteArray()
|
||||
}
|
||||
|
||||
fun decode(bytes: ByteArray): ViewDirectoryEntry {
|
||||
val reader = ByteReader(bytes)
|
||||
val viewName = String(reader.readLengthPrefixed(), Charsets.UTF_8)
|
||||
val statementText = String(reader.readLengthPrefixed(), Charsets.UTF_8)
|
||||
return ViewDirectoryEntry(viewName, statementText)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user