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