// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module Crafter.DB:Storage_impl; import :Storage; import :Error; import :Format; import :Formatter; import :File; import std; using namespace Crafter::DB; namespace fs = std::filesystem; namespace { constexpr std::uint64_t maxExtentBytes = std::uint64_t(1) << 30; constexpr std::uint64_t willNeedLimit = std::uint64_t(256) << 20; constexpr std::uint32_t regionRows = 0; constexpr std::uint32_t regionHeap = 1; constexpr std::uint64_t RoundUp(std::uint64_t bytes, std::uint64_t granule) noexcept { return (bytes + granule - 1) / granule * granule; } // Extents are aligned to the larger of the format page and the running // kernel's page size, so they can be mmapped on 16K and 64K kernels too. std::uint64_t Granule() noexcept { return std::max(pageSize, Os::PageSize()); } std::span Bytes(const auto& object) noexcept { return std::as_bytes(std::span(&object, 1)); } void FillCommit(CommitRecord& record, std::uint64_t seq, std::uint64_t rowCount, std::uint64_t liveCount, std::uint64_t heapUsed, std::uint64_t heapGarbage, const std::array& extentCount, std::uint64_t fileLength, std::uint64_t flags) noexcept { record = CommitRecord{}; record.commitSeq = seq; record.rowCount = rowCount; record.liveCount = liveCount; record.heapUsed = heapUsed; record.heapGarbage = heapGarbage; record.extentCount = extentCount; record.fileLength = fileLength; record.modifiedUnixNanos = Os::UnixNanos(); record.flags = flags; record.crc32c = CommitCrc(record); } // Picks the newest valid commit slot; nullopt when both are damaged. std::optional ChooseCommit(const FileHeader& header) noexcept { const bool valid0 = CommitValid(header.commits[0]); const bool valid1 = CommitValid(header.commits[1]); if (valid0 && valid1) return header.commits[0].commitSeq >= header.commits[1].commitSeq ? 0 : 1; if (valid0) return 0; if (valid1) return 1; return std::nullopt; } void ValidateHeader(const FileHeader& header, std::uint64_t fileSize, const fs::path& path) { if (header.magic != fileMagic) throw CorruptError(std::format("{}: not a Crafter.DB table (bad magic)", path.string())); if (header.formatVersion != formatVersion) throw CorruptError(std::format("{}: file format {} is not supported by this build (format {})", path.string(), header.formatVersion, formatVersion)); if ((header.formatFlags & formatFlagLittleEndian) == 0) throw CorruptError(std::format("{}: unsupported byte order", path.string())); if (header.pageSize < pageSize || header.pageSize % pageSize != 0) throw CorruptError(std::format("{}: extent granule {} is invalid", path.string(), header.pageSize)); if (header.pageSize % Os::PageSize() != 0) throw IOError(std::format("table was created with {}-byte extents, which cannot be mapped on a system with {}-byte pages", header.pageSize, Os::PageSize()), std::make_error_code(std::errc::not_supported), path); if (header.columnCount == 0 || (header.orderedColumn != noColumn && header.orderedColumn >= header.columnCount)) throw CorruptError(std::format("{}: column count or ordered column is invalid", path.string())); if (header.headerPages < 2 || std::uint64_t(header.headerPages) * pageSize > fileSize) throw CorruptError(std::format("{}: header page count {} is invalid", path.string(), header.headerPages)); if (header.regionCount == 0 || header.regionCount > regionSlots) throw CorruptError(std::format("{}: region count {} is invalid", path.string(), header.regionCount)); if (header.rowSize == 0 || header.rowAlign == 0 || header.rowAlign > 16 || header.rowSize % header.rowAlign != 0) throw CorruptError(std::format("{}: row size {} / alignment {} is invalid", path.string(), header.rowSize, header.rowAlign)); if (std::uint64_t(header.schemaBlobLength) + pageSize > std::uint64_t(header.headerPages) * pageSize) throw CorruptError(std::format("{}: schema blob does not fit the header pages", path.string())); } // Reads header, commit and schema blob; shared by Inspect and open. The // header and the first schema page are fetched in one pread, which is the // whole description for every table with fewer than ~170 columns. void ReadDescription(const File& file, std::uint64_t size, FileHeader& header, std::size_t& chosen, SchemaInfo& schema) { if (size < 2 * pageSize) throw CorruptError(std::format("{}: file is smaller than the header", file.Path().string())); std::vector pages(2 * pageSize); file.ReadAt(0, pages); std::memcpy(&header, pages.data(), sizeof(header)); ValidateHeader(header, size, file.Path()); const std::optional slot = ChooseCommit(header); if (!slot) throw CorruptError(std::format("{}: both commit records are damaged", file.Path().string())); chosen = *slot; std::span blob; std::vector rest; if (header.schemaBlobLength <= pageSize) { blob = std::span(pages).subspan(pageSize, header.schemaBlobLength); } else { rest.resize(header.schemaBlobLength); file.ReadAt(pageSize, rest); blob = rest; } if (!SchemaInfo::Parse(blob, header.rowAlign, schema)) throw CorruptError(std::format("{}: schema blob is malformed", file.Path().string())); if (schema.rowSize != header.rowSize || schema.columns.size() != header.columnCount) throw CorruptError(std::format("{}: schema blob disagrees with the header", file.Path().string())); } // Reads a scalar column as the variant alternative its kind implies. std::optional ReadScalar(const std::byte* row, const ColumnDescriptor& column) noexcept { const std::byte* at = row + column.offset; switch (column.kind) { case Kind::Flags: case Kind::Bool: case Kind::UInt8: { std::uint8_t v; std::memcpy(&v, at, 1); return ScalarValue(std::uint64_t(v)); } case Kind::UInt16: { std::uint16_t v; std::memcpy(&v, at, 2); return ScalarValue(std::uint64_t(v)); } case Kind::UInt32: { std::uint32_t v; std::memcpy(&v, at, 4); return ScalarValue(std::uint64_t(v)); } case Kind::UInt64: { std::uint64_t v; std::memcpy(&v, at, 8); return ScalarValue(v); } case Kind::Int8: { std::int8_t v; std::memcpy(&v, at, 1); return ScalarValue(std::int64_t(v)); } case Kind::Int16: { std::int16_t v; std::memcpy(&v, at, 2); return ScalarValue(std::int64_t(v)); } case Kind::Int32: { std::int32_t v; std::memcpy(&v, at, 4); return ScalarValue(std::int64_t(v)); } case Kind::Int64: case Kind::TimePoint: case Kind::Duration: { if (column.size == 8) { std::int64_t v; std::memcpy(&v, at, 8); return ScalarValue(v); } if (column.size == 4) { std::int32_t v; std::memcpy(&v, at, 4); return ScalarValue(std::int64_t(v)); } return std::nullopt; } case Kind::Float32: { float v; std::memcpy(&v, at, 4); return ScalarValue(double(v)); } case Kind::Float64: { double v; std::memcpy(&v, at, 8); return ScalarValue(v); } case Kind::String: case Kind::Blob: case Kind::FixedBytes: return std::nullopt; } return std::nullopt; } // cmp_less that also accepts doubles (std::cmp_less is integers only). template bool CmpLessPartial(const A& x, const B& y) noexcept { if constexpr (std::is_floating_point_v || std::is_floating_point_v) return static_cast(x) < static_cast(y); else return std::cmp_less(x, y); } bool ScalarLess(const ScalarValue& a, const ScalarValue& b) noexcept { return std::visit([](const auto& x, const auto& y) { return CmpLessPartial(x, y); }, a, b); } std::uint32_t VarAlign(const ColumnDescriptor& column) noexcept { return column.kind == Kind::Blob && column.paramNum != 0 ? column.paramNum : 1; } } // --- creation --------------------------------------------------------------- fs::path TableFile::TempPath(const fs::path& path) { std::array random{}; Os::RandomBytes(random); std::string suffix; for (const std::byte b : random) suffix += std::format("{:02x}", static_cast(b)); return fs::path(std::format("{}.tmp-{}", path.string(), suffix)); } fs::path TableFile::BackupPath(const fs::path& path, std::uint32_t version) { return fs::path(std::format("{}.v{}.bak", path.string(), version)); } void TableFile::Replace(const fs::path& path, const fs::path& temp) { Os::Rename(temp, path); Os::SyncDirectory(path.parent_path()); } void TableFile::Create(const fs::path& path, const CreateParams& params, const OpenOptions& options) { const std::vector blob = params.schema.Serialize(); const std::uint64_t granule = Granule(); FileHeader header{}; header.magic = fileMagic; header.formatVersion = formatVersion; header.headerPages = static_cast(1 + RoundUp(blob.size(), pageSize) / pageSize); header.formatFlags = formatFlagLittleEndian; header.pageSize = static_cast(granule); header.rowSize = params.schema.rowSize; header.rowAlign = params.schema.rowAlign; header.columnCount = static_cast(params.schema.columns.size()); header.orderedColumn = params.orderedColumn; header.regionCount = regionCount; header.schemaVersion = params.schemaVersion; header.schemaFingerprint = params.fingerprint; header.schemaBlobLength = static_cast(blob.size()); header.minExtentBytes = minExtentBytes; header.createdUnixNanos = Os::UnixNanos(); Os::RandomBytes(std::as_writable_bytes(std::span(header.fileUuid))); header.migratedFromVersion = params.migratedFromVersion; std::array extentCount{}; const std::uint64_t headerBytes = std::uint64_t(header.headerPages) * pageSize; std::uint64_t fileLength = RoundUp(headerBytes, granule); header.extents[regionRows][0] = { fileLength, std::max(RoundUp(minExtentBytes, granule), RoundUp(params.initialRowBytes, granule)) }; extentCount[regionRows] = 1; fileLength += header.extents[regionRows][0].length; if (params.schema.HasHeap()) { header.extents[regionHeap][0] = { fileLength, std::max(RoundUp(minExtentBytes, granule), RoundUp(params.initialHeapBytes, granule)) }; extentCount[regionHeap] = 1; fileLength += header.extents[regionHeap][0].length; } FillCommit(header.commits[0], 1, 0, 0, 0, 0, extentCount, fileLength, commitFlagCleanClose); if (options.createDirectories && !path.parent_path().empty()) Os::CreateDirectories(path.parent_path(), options.directoryPermissions); const fs::path temp = TempPath(path); try { File file = File::CreateExclusive(temp, options.filePermissions); file.WriteAt(0, Bytes(header)); file.WriteAt(pageSize, blob); file.Allocate(headerBytes, fileLength - headerBytes); file.Sync(); file.Close(); // Never replace: two processes creating the same table at once must // end up sharing one file, not each writing to its own inode. if (!Os::RenameNoReplace(temp, path)) { Os::Remove(temp); throw IOError("table already exists", std::make_error_code(std::errc::file_exists), path); } Os::SyncDirectory(path.parent_path()); } catch (...) { Os::Remove(temp); throw; } } Inspection TableFile::Inspect(const fs::path& path) { const File file = File::Open(path, false); Inspection result; std::size_t chosen = 0; ReadDescription(file, file.Size(), result.header, chosen, result.schema); result.commit = result.header.commits[chosen]; return result; } void TableFile::RemoveStaleTemps(const fs::path& path) noexcept { try { const std::string prefix = std::format("{}.tmp-", path.filename().string()); const fs::path directory = path.parent_path().empty() ? fs::path(".") : path.parent_path(); for (const fs::directory_entry& entry : fs::directory_iterator(directory)) { if (!entry.path().filename().string().starts_with(prefix)) continue; try { File probe = File::Open(entry.path(), true); probe.LockExclusive(); // held by a live writer -> LockedError -> keep it Os::Remove(entry.path()); } catch (...) { } } } catch (...) { } } // --- open ------------------------------------------------------------------- TableFile::TableFile(const fs::path& path, const OpenOptions& openOptions) : options(openOptions), writable(!openOptions.readOnly), ownerProcess(Os::ProcessId()) { file = File::Open(path, writable); if (writable) file.LockExclusive(); else file.LockShared(); const std::uint64_t size = file.Size(); ReadDescription(file, size, header, activeSlot, schema); current = header.commits[activeSlot]; if (current.fileLength > size) throw CorruptError(std::format("{}: committed length {} exceeds the file size {}", path.string(), current.fileLength, size)); if (current.fileLength < std::uint64_t(header.headerPages) * pageSize) throw CorruptError(std::format("{}: committed length is inside the header", path.string())); for (std::size_t r = 0; r < regionSlots; r++) { if (current.extentCount[r] > maxExtents || (r >= header.regionCount && current.extentCount[r] != 0)) throw CorruptError(std::format("{}: extent count of region {} is invalid", path.string(), r)); } rows.Reserve(options.reserveBytes); heapRegion.Reserve(options.reserveBytes); const auto mapRegion = [&](MappedRegion& region, std::uint32_t index) { for (std::uint32_t e = 0; e < current.extentCount[index]; e++) { const Extent extent = header.extents[index][e]; if (extent.length == 0 || extent.length % header.pageSize != 0 || extent.fileOffset % header.pageSize != 0 || extent.fileOffset < std::uint64_t(header.headerPages) * pageSize || extent.length > current.fileLength || extent.fileOffset > current.fileLength - extent.length) { throw CorruptError(std::format("{}: extent {} of region {} lies outside the committed file", path.string(), e, index)); } region.Map(file, extent.fileOffset, extent.length, writable); } }; mapRegion(rows, regionRows); mapRegion(heapRegion, regionHeap); // One madvise pair per region now; extents added later are advised as // they are mapped. if (options.excludeFromCoreDumps) { rows.ExcludeFromCoreDumps(); heapRegion.ExcludeFromCoreDumps(); } if (current.rowCount > rows.Mapped() / header.rowSize) throw CorruptError(std::format("{}: {} committed rows do not fit the mapped row extents", path.string(), current.rowCount)); if (current.heapUsed > heapRegion.Mapped()) throw CorruptError(std::format("{}: committed heap length exceeds the mapped heap extents", path.string())); extentCount = current.extentCount; fileLength = current.fileLength; publishedRows.store(current.rowCount, std::memory_order_relaxed); publishedLive.store(current.liveCount, std::memory_order_relaxed); publishedHeap.store(current.heapUsed, std::memory_order_relaxed); heapGarbage = current.heapGarbage; heap = Heap{ heapRegion.Base(), &publishedHeap }; writer.Attach(this, heapRegion.Base(), current.heapUsed, heapRegion.Mapped()); for (std::size_t c = 0; c < schema.columns.size(); c++) { if (schema.columns[c].kind == Kind::String || schema.columns[c].kind == Kind::Blob) varColumns.push_back(c); } // An unclean close means tombstones (and orphaned heap bytes) may have // reached the disk after the last commit; recount so the "no tombstones" // scan fast path and the garbage figure stay honest. if ((current.flags & commitFlagCleanClose) == 0) RecountLive(); tombstones.store(publishedLive.load(std::memory_order_relaxed) != publishedRows.load(std::memory_order_relaxed), std::memory_order_relaxed); if (options.prefetch) { const std::uint64_t rowBytes = current.rowCount * header.rowSize; if (rowBytes < willNeedLimit) rows.WillNeed(rowBytes); if (current.heapUsed < willNeedLimit) heapRegion.WillNeed(current.heapUsed); } if (writable) { // Uncommitted growth from an earlier crash: drop it so extents we add // start exactly at the committed end. if (size > current.fileLength) file.Truncate(current.fileLength); headerPages.Map(file, 0, pageSize, true); // Mark the file open: a crash from here on must recount on next open. WriteCommit(false); RemoveStaleTemps(path); } } TableFile::~TableFile() { try { Close(); } catch (...) { } } void TableFile::Close() { if (closed) return; closed = true; // A forked child inherited this object; its counters describe the parent's // state at fork time and must never reach the file. if (writable && file.IsOpen() && OwnedByThisProcess()) Flush(true); headerPages.Release(); rows.Release(); heapRegion.Release(); file.Close(); } bool TableFile::OwnedByThisProcess() const noexcept { return Os::ProcessId() == ownerProcess; } void TableFile::RequireWritable() const { if (!writable) throw ReadOnlyError(std::format("{}: table is open read-only", file.Path().string())); if (closed) throw Error(std::format("{}: table is closed", file.Path().string())); if (!OwnedByThisProcess()) throw Error(std::format("{}: table was opened by another process (inherited through fork)", file.Path().string())); } TableStats TableFile::Stats() const noexcept { return { SlotCount(), LiveCount(), HeapUsed(), heapGarbage, fileLength, current.commitSeq }; } bool TableFile::Deleted(const std::byte* row) noexcept { return (std::atomic_ref(*const_cast(reinterpret_cast(row))).load(std::memory_order_relaxed) & rowFlagDeleted) != 0; } void TableFile::RecountLive() noexcept { const std::uint64_t count = publishedRows.load(std::memory_order_relaxed); std::uint64_t live = 0; std::uint64_t referenced = 0; for (std::uint64_t i = 0; i < count; i++) { const std::byte* row = Slot(i); if (Deleted(row)) continue; live++; referenced += HeapBytesOf(row); } publishedLive.store(live, std::memory_order_relaxed); // Everything in the heap that no live row references is garbage: this // also covers bytes orphaned by Update/Set before the crash. const std::uint64_t used = publishedHeap.load(std::memory_order_relaxed); heapGarbage = used > referenced ? used - referenced : 0; } // --- growth ----------------------------------------------------------------- void TableFile::AddExtent(std::uint32_t region, std::uint64_t minimumBytes) { RequireWritable(); const std::uint32_t count = extentCount[region]; if (count >= maxExtents) throw IOError("table has reached its maximum number of extents; Compact() it", std::make_error_code(std::errc::file_too_large), file.Path()); const std::uint64_t granule = header.pageSize; const std::uint64_t previous = count == 0 ? 0 : header.extents[region][count - 1].length; const std::uint64_t length = std::max({ RoundUp(minExtentBytes, granule), std::min(previous * 2, maxExtentBytes), RoundUp(minimumBytes, granule) }); const Extent extent{ fileLength, length }; file.Allocate(extent.fileOffset, extent.length); MappedRegion& target = region == regionRows ? rows : heapRegion; target.Map(file, extent.fileOffset, extent.length, true); header.extents[region][count] = extent; std::memcpy(headerPages.Data() + __builtin_offsetof(FileHeader, extents) + (region * maxExtents + count) * sizeof(Extent), &extent, sizeof(extent)); extentCount[region] = count + 1; fileLength = extent.fileOffset + extent.length; } void TableFile::EnsureHeapCapacity(std::uint64_t bytes) { while (heapRegion.Mapped() < bytes) AddExtent(regionHeap, bytes - heapRegion.Mapped()); writer.SetCapacity(heapRegion.Mapped()); } void TableFile::Arena::Grow(std::uint64_t neededCapacity) { owner->EnsureHeapCapacity(neededCapacity); } // --- writes ----------------------------------------------------------------- std::byte* TableFile::BeginAppend(std::uint64_t count) { RequireWritable(); const std::uint64_t slots = publishedRows.load(std::memory_order_relaxed); const std::uint64_t needed = (slots + count) * header.rowSize; while (rows.Mapped() < needed) AddExtent(regionRows, needed - rows.Mapped()); return rows.Base() + slots * header.rowSize; } void TableFile::CommitAppend(std::uint64_t count) noexcept { publishedHeap.store(writer.Cursor(), std::memory_order_release); publishedLive.fetch_add(count, std::memory_order_relaxed); publishedRows.fetch_add(count, std::memory_order_release); } void TableFile::AbortAppend() noexcept { writer.Rewind(publishedHeap.load(std::memory_order_relaxed)); } std::byte* TableFile::MutableSlot(std::uint64_t index) { RequireWritable(); return rows.Base() + index * header.rowSize; } bool TableFile::MarkDeleted(std::uint64_t index) { RequireWritable(); std::byte* row = rows.Base() + index * header.rowSize; // The flag goes up before the byte, so a scan that starts after this call // checks every row. tombstones.store(true, std::memory_order_release); const std::uint8_t previous = std::atomic_ref(*reinterpret_cast(row)).fetch_or(rowFlagDeleted, std::memory_order_relaxed); if ((previous & rowFlagDeleted) != 0) return false; publishedLive.fetch_sub(1, std::memory_order_relaxed); heapGarbage += HeapBytesOf(row); return true; } std::uint64_t TableFile::HeapBytesOf(const std::byte* row) const noexcept { std::uint64_t bytes = 0; for (const std::size_t c : varColumns) { const ColumnDescriptor& column = schema.columns[c]; Var var; std::memcpy(&var, row + column.offset, sizeof(var)); if (!VarInline(var.length, VarAlign(column))) bytes += var.length; } return bytes; } void TableFile::WriteCommit(bool cleanClose) { const std::size_t slot = activeSlot == 0 ? 1 : 0; CommitRecord record; FillCommit(record, current.commitSeq + 1, publishedRows.load(std::memory_order_relaxed), publishedLive.load(std::memory_order_relaxed), publishedHeap.load(std::memory_order_relaxed), heapGarbage, extentCount, fileLength, cleanClose ? commitFlagCleanClose : 0); std::memcpy(headerPages.Data() + __builtin_offsetof(FileHeader, commits) + slot * sizeof(CommitRecord), &record, sizeof(record)); headerPages.Sync(); current = record; header.commits[slot] = record; activeSlot = slot; } void TableFile::Flush(bool cleanClose) { if (!writable || (closed && !file.IsOpen())) return; if (!OwnedByThisProcess()) throw Error(std::format("{}: table was opened by another process (inherited through fork)", file.Path().string())); // Data first: the commit record must never describe rows or heap bytes // that are not yet on disk. file.SyncData(); WriteCommit(cleanClose); } // --- compaction ------------------------------------------------------------- void TableFile::CompactInto(TableFile& target) const { target.RequireWritable(); if (target.RowSize() != RowSize() || target.Schema().columns.size() != schema.columns.size()) throw Error("CompactInto: target table has a different schema"); const std::uint64_t slots = SlotCount(); const std::uint64_t live = LiveCount(); if (live == 0) return; std::byte* out = target.BeginAppend(live); HeapWriter& arena = target.Writer(); std::uint64_t written = 0; for (std::uint64_t i = 0; i < slots && written < live; i++) { const std::byte* row = Slot(i); if (Deleted(row)) continue; std::memcpy(out, row, header.rowSize); for (const std::size_t c : varColumns) { const ColumnDescriptor& column = schema.columns[c]; if ((column.flags & columnFlagOptional) != 0 && column.presenceOffset != noColumn) { // The payload of a disengaged optional carries no value: write // an empty handle rather than carrying stale bytes forward. std::uint8_t present; std::memcpy(&present, out + column.presenceOffset, 1); if (present == 0) { const Var empty{}; std::memcpy(out + column.offset, &empty, sizeof(empty)); continue; } } Var var; std::memcpy(&var, out + column.offset, sizeof(var)); if (VarInline(var.length, VarAlign(column))) continue; const std::span bytes = heap.Read(var.tailOrOffset, var.length); var.tailOrOffset = arena.Write(bytes, VarAlign(column)); std::memcpy(out + column.offset, &var, sizeof(var)); } out += header.rowSize; written++; } target.CommitAppend(written); } // --- verification ----------------------------------------------------------- VerifyResult TableFile::Verify() const { VerifyResult result; const std::uint64_t slots = SlotCount(); const std::uint64_t heapLimit = HeapUsed(); const std::uint16_t ordered = header.orderedColumn; std::optional previousKey; std::uint64_t live = 0; for (std::uint64_t i = 0; i < slots; i++) { const std::byte* row = Slot(i); std::uint8_t flags; std::memcpy(&flags, row, 1); if ((flags & ~rowFlagDeleted) != 0) result.problems.push_back(std::format("row {}: unknown flag bits {:#x}", i, flags)); if ((flags & rowFlagDeleted) == 0) live++; for (std::size_t c = 1; c < schema.columns.size(); c++) { const ColumnDescriptor& column = schema.columns[c]; if ((column.flags & columnFlagPresence) != 0) { std::uint8_t present; std::memcpy(&present, row + column.offset, 1); if (present > 1) result.problems.push_back(std::format("row {}: presence byte of '{}' is {}", i, schema.Name(c), present)); } if (column.kind == Kind::String || column.kind == Kind::Blob) { Var var; std::memcpy(&var, row + column.offset, sizeof(var)); if (!VarInline(var.length, VarAlign(column)) && (var.tailOrOffset > heapLimit || var.length > heapLimit - var.tailOrOffset)) { result.problems.push_back(std::format("row {}: '{}' points at heap [{}, +{}) beyond the committed {} bytes", i, schema.Name(c), var.tailOrOffset, var.length, heapLimit)); } } } if (ordered != noColumn && ordered < schema.columns.size()) { const std::optional key = ReadScalar(row, schema.columns[ordered]); if (key && previousKey && ScalarLess(*key, *previousKey)) result.problems.push_back(std::format("row {}: '{}' is out of order", i, schema.Name(ordered))); if (key) previousKey = key; } if (result.problems.size() > 1000) { result.problems.push_back("... further problems omitted"); return result; } } if (live != LiveCount()) result.problems.push_back(std::format("live count {} does not match the {} rows without a tombstone", LiveCount(), live)); return result; } // --- DynamicRow ------------------------------------------------------------- bool DynamicRow::Deleted() const noexcept { return TableFile::Deleted(row); } std::optional DynamicRow::Find(std::string_view name) const noexcept { for (std::size_t i = 0; i < schema->columns.size(); i++) { if (schema->Name(i) == name) return i; } return std::nullopt; } bool DynamicRow::Present(std::size_t index) const noexcept { const ColumnDescriptor& column = schema->columns[index]; if ((column.flags & columnFlagOptional) == 0 || column.presenceOffset == noColumn) return true; std::uint8_t present; std::memcpy(&present, row + column.presenceOffset, 1); return present != 0; } std::optional DynamicRow::Scalar(std::size_t index) const noexcept { return ReadScalar(row, schema->columns[index]); } std::optional> DynamicRow::Bytes(std::size_t index) const { const ColumnDescriptor& column = schema->columns[index]; if (column.kind == Kind::FixedBytes) return std::span(row + column.offset, column.size); if (column.kind != Kind::String && column.kind != Kind::Blob) return std::nullopt; Var var; std::memcpy(&var, row + column.offset, sizeof(var)); if (VarInline(var.length, VarAlign(column))) return std::span(row + column.offset + 4, var.length); return heap->Read(var.tailOrOffset, var.length); } std::optional DynamicRow::Text(std::size_t index) const { const ColumnDescriptor& column = schema->columns[index]; if (column.kind != Kind::String && column.kind != Kind::FixedBytes) return std::nullopt; const std::optional> bytes = Bytes(index); if (!bytes) return std::nullopt; std::string_view text(reinterpret_cast(bytes->data()), bytes->size()); if (column.kind == Kind::FixedBytes) text = text.substr(0, text.find('\0')); return text; } std::string DynamicRow::Describe() const { std::string out; for (std::size_t i = 1; i < schema->columns.size(); i++) { const ColumnDescriptor& column = schema->columns[i]; if ((column.flags & columnFlagPresence) != 0) continue; if (!out.empty()) out += ", "; out += schema->Name(i); out += '='; if (!Present(i)) { out += "null"; continue; } if (const std::optional text = Text(i)) { out += std::format("\"{}\"", *text); } else if (const std::optional scalar = Scalar(i)) { std::visit([&](const auto& v) { out += std::format("{}", v); }, *scalar); } else if (const std::optional> bytes = Bytes(i)) { out += std::format("<{} bytes>", bytes->size()); } else { out += '?'; } } return out; }