From 142f8274edfa0d31e8b53e2d544c9acff1c329ab Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 7 Sep 2026 23:11:43 +0200 Subject: [PATCH] feat(sql): add SequenceDirectoryEntryCodec, encode --- .../store/sql/SequenceDirectoryEntryCodec.kt | 38 +++++++++++ .../kestrel/store/sql/TableDirectoryEntry.kt | 68 +++++++++++++++++++ .../store/sql/TriggerDirectoryEntry.kt | 12 ++++ .../store/sql/TriggerDirectoryEntryCodec.kt | 21 ++++++ .../kestrel/store/sql/ViewDirectoryEntry.kt | 12 ++++ .../store/sql/ViewDirectoryEntryCodec.kt | 21 ++++++ 6 files changed, 172 insertions(+) create mode 100644 src/main/kotlin/kestrel/store/sql/SequenceDirectoryEntryCodec.kt create mode 100644 src/main/kotlin/kestrel/store/sql/TableDirectoryEntry.kt create mode 100644 src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntry.kt create mode 100644 src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntryCodec.kt create mode 100644 src/main/kotlin/kestrel/store/sql/ViewDirectoryEntry.kt create mode 100644 src/main/kotlin/kestrel/store/sql/ViewDirectoryEntryCodec.kt diff --git a/src/main/kotlin/kestrel/store/sql/SequenceDirectoryEntryCodec.kt b/src/main/kotlin/kestrel/store/sql/SequenceDirectoryEntryCodec.kt new file mode 100644 index 0000000..901962b --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/SequenceDirectoryEntryCodec.kt @@ -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) + } +} diff --git a/src/main/kotlin/kestrel/store/sql/TableDirectoryEntry.kt b/src/main/kotlin/kestrel/store/sql/TableDirectoryEntry.kt new file mode 100644 index 0000000..6f5de9b --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/TableDirectoryEntry.kt @@ -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, 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, + val dataHeaderPageId: Long, + val primaryKeyIndexRootPageId: Long?, + val secondaryIndexes: List, + /** + * 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 = 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 = emptyList(), + /** Table-level or column-level `CHECK (...)` constraints, mirroring [kestrel.catalog.TableSchema.checks]. */ + val checks: List = 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 = emptyList() +) diff --git a/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntry.kt b/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntry.kt new file mode 100644 index 0000000..c11aa8c --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntry.kt @@ -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) diff --git a/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntryCodec.kt b/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntryCodec.kt new file mode 100644 index 0000000..7e1cefe --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/TriggerDirectoryEntryCodec.kt @@ -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) + } +} diff --git a/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntry.kt b/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntry.kt new file mode 100644 index 0000000..2ecf7e3 --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntry.kt @@ -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) diff --git a/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntryCodec.kt b/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntryCodec.kt new file mode 100644 index 0000000..36a9b89 --- /dev/null +++ b/src/main/kotlin/kestrel/store/sql/ViewDirectoryEntryCodec.kt @@ -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) + } +}