Initial commit: Crafter.DB
Header-only C++26 module library for typed, file-backed tables with schema reflection, migrations, queries, and crash-safe commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
bfd0cc261b
34 changed files with 7506 additions and 0 deletions
317
implementations/Crafter.DB-File.cpp
Normal file
317
implementations/Crafter.DB-File.cpp
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
module;
|
||||
|
||||
#define _GNU_SOURCE 1
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
module Crafter.DB:File_impl;
|
||||
import :File;
|
||||
import :Error;
|
||||
import std;
|
||||
|
||||
using namespace Crafter::DB;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
std::error_code Errno() noexcept {
|
||||
return std::error_code(errno, std::generic_category());
|
||||
}
|
||||
|
||||
[[noreturn]] void Fail(std::string_view what, const fs::path& path) {
|
||||
throw IOError(what, Errno(), path);
|
||||
}
|
||||
}
|
||||
|
||||
// --- File -------------------------------------------------------------------
|
||||
|
||||
File::File(File&& other) noexcept : descriptor(other.descriptor), path(std::move(other.path)) {
|
||||
other.descriptor = -1;
|
||||
}
|
||||
|
||||
File& File::operator=(File&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Close();
|
||||
descriptor = other.descriptor;
|
||||
path = std::move(other.path);
|
||||
other.descriptor = -1;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
Close();
|
||||
}
|
||||
|
||||
File File::Open(const fs::path& path, bool writable) {
|
||||
File file;
|
||||
file.path = path;
|
||||
// O_NOFOLLOW: a private cache file must not be swapped for a symlink to
|
||||
// somewhere we would then happily write 0600 data into.
|
||||
const std::int32_t flags = (writable ? O_RDWR : O_RDONLY) | O_CLOEXEC | O_NOFOLLOW;
|
||||
file.descriptor = ::open(path.c_str(), flags);
|
||||
if (file.descriptor == -1) Fail("cannot open", path);
|
||||
return file;
|
||||
}
|
||||
|
||||
File File::CreateExclusive(const fs::path& path, fs::perms permissions) {
|
||||
File file;
|
||||
file.path = path;
|
||||
file.descriptor = ::open(path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, static_cast<mode_t>(permissions));
|
||||
if (file.descriptor == -1) Fail("cannot create", path);
|
||||
return file;
|
||||
}
|
||||
|
||||
void File::LockExclusive() {
|
||||
if (::flock(descriptor, LOCK_EX | LOCK_NB) == -1) {
|
||||
if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string()));
|
||||
Fail("cannot lock", path);
|
||||
}
|
||||
}
|
||||
|
||||
void File::LockShared() {
|
||||
if (::flock(descriptor, LOCK_SH | LOCK_NB) == -1) {
|
||||
if (errno == EWOULDBLOCK) throw LockedError(std::format("{}: another Table holds this file open for writing", path.string()));
|
||||
Fail("cannot lock", path);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t File::Size() const {
|
||||
struct stat info{};
|
||||
if (::fstat(descriptor, &info) == -1) Fail("cannot stat", path);
|
||||
return static_cast<std::uint64_t>(info.st_size);
|
||||
}
|
||||
|
||||
void File::Truncate(std::uint64_t length) {
|
||||
if (::ftruncate(descriptor, static_cast<off_t>(length)) == -1) Fail("cannot truncate", path);
|
||||
}
|
||||
|
||||
void File::Allocate(std::uint64_t offset, std::uint64_t length) {
|
||||
if (::fallocate(descriptor, 0, static_cast<off_t>(offset), static_cast<off_t>(length)) == 0) return;
|
||||
if (errno != EOPNOTSUPP && errno != ENOSYS) Fail("cannot allocate space", path);
|
||||
// Filesystems without fallocate (FUSE, 9p, exFAT): posix_fallocate falls
|
||||
// back to writing zeros, which equally guarantees the blocks exist.
|
||||
const std::int32_t result = ::posix_fallocate(descriptor, static_cast<off_t>(offset), static_cast<off_t>(length));
|
||||
if (result != 0) throw IOError("cannot allocate space", std::error_code(result, std::generic_category()), path);
|
||||
}
|
||||
|
||||
void File::ReadAt(std::uint64_t offset, std::span<std::byte> out) const {
|
||||
while (!out.empty()) {
|
||||
const ssize_t got = ::pread(descriptor, out.data(), out.size(), static_cast<off_t>(offset));
|
||||
if (got == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
Fail("read failed", path);
|
||||
}
|
||||
if (got == 0) throw CorruptError(std::format("{}: file ends {} bytes short", path.string(), out.size()));
|
||||
out = out.subspan(static_cast<std::size_t>(got));
|
||||
offset += static_cast<std::uint64_t>(got);
|
||||
}
|
||||
}
|
||||
|
||||
void File::WriteAt(std::uint64_t offset, std::span<const std::byte> in) {
|
||||
while (!in.empty()) {
|
||||
const ssize_t put = ::pwrite(descriptor, in.data(), in.size(), static_cast<off_t>(offset));
|
||||
if (put == -1) {
|
||||
if (errno == EINTR) continue;
|
||||
Fail("write failed", path);
|
||||
}
|
||||
in = in.subspan(static_cast<std::size_t>(put));
|
||||
offset += static_cast<std::uint64_t>(put);
|
||||
}
|
||||
}
|
||||
|
||||
void File::SyncData() {
|
||||
if (::fdatasync(descriptor) == -1) Fail("fdatasync failed", path);
|
||||
}
|
||||
|
||||
void File::Sync() {
|
||||
if (::fsync(descriptor) == -1) Fail("fsync failed", path);
|
||||
}
|
||||
|
||||
void File::Close() noexcept {
|
||||
if (descriptor != -1) {
|
||||
::close(descriptor);
|
||||
descriptor = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MappedRegion -----------------------------------------------------------
|
||||
|
||||
MappedRegion::MappedRegion(MappedRegion&& other) noexcept : base(other.base), reserved(other.reserved), mapped(other.mapped), dontDump(other.dontDump) {
|
||||
other.base = nullptr;
|
||||
other.reserved = 0;
|
||||
other.mapped = 0;
|
||||
}
|
||||
|
||||
MappedRegion& MappedRegion::operator=(MappedRegion&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Release();
|
||||
base = other.base;
|
||||
reserved = other.reserved;
|
||||
mapped = other.mapped;
|
||||
dontDump = other.dontDump;
|
||||
other.base = nullptr;
|
||||
other.reserved = 0;
|
||||
other.mapped = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MappedRegion::~MappedRegion() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void MappedRegion::Reserve(std::uint64_t bytes) {
|
||||
Release();
|
||||
// PROT_NONE + MAP_NORESERVE costs no memory and no commit charge: it only
|
||||
// claims the addresses, so later MAP_FIXED calls cannot collide with
|
||||
// anything else the process maps.
|
||||
void* address = ::mmap(nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||
if (address == MAP_FAILED) throw IOError(std::format("cannot reserve {} bytes of address space", bytes), Errno(), fs::path());
|
||||
base = static_cast<std::byte*>(address);
|
||||
reserved = bytes;
|
||||
mapped = 0;
|
||||
}
|
||||
|
||||
void MappedRegion::Map(const File& file, std::uint64_t fileOffset, std::uint64_t length, bool writable) {
|
||||
if (mapped + length > reserved) {
|
||||
throw IOError(std::format("table needs more than the {} bytes of address space reserved per region; reopen with a larger OpenOptions::reserveBytes", reserved), std::make_error_code(std::errc::not_enough_memory), file.Path());
|
||||
}
|
||||
void* address = ::mmap(base + mapped, length, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED | MAP_FIXED, file.Descriptor(), static_cast<off_t>(fileOffset));
|
||||
if (address == MAP_FAILED) Fail("cannot map", file.Path());
|
||||
if (dontDump) ::madvise(address, length, MADV_DONTDUMP);
|
||||
mapped += length;
|
||||
}
|
||||
|
||||
// MADV_DONTDUMP only. MADV_DONTFORK was considered and rejected: it turns any
|
||||
// touch of an inherited table in a forked child into SIGSEGV. The mapping is
|
||||
// MAP_SHARED, so a child reads consistently; writes are refused by the
|
||||
// owner-process check in TableFile.
|
||||
void MappedRegion::ExcludeFromCoreDumps() {
|
||||
dontDump = true;
|
||||
if (mapped != 0) ::madvise(base, mapped, MADV_DONTDUMP);
|
||||
}
|
||||
|
||||
void MappedRegion::WillNeed(std::uint64_t length) {
|
||||
if (length != 0) ::madvise(base, std::min(length, mapped), MADV_WILLNEED);
|
||||
}
|
||||
|
||||
void MappedRegion::Release() noexcept {
|
||||
if (base != nullptr) {
|
||||
::munmap(base, reserved);
|
||||
base = nullptr;
|
||||
reserved = 0;
|
||||
mapped = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- MappedPages ------------------------------------------------------------
|
||||
|
||||
MappedPages::MappedPages(MappedPages&& other) noexcept : data(other.data), length(other.length) {
|
||||
other.data = nullptr;
|
||||
other.length = 0;
|
||||
}
|
||||
|
||||
MappedPages& MappedPages::operator=(MappedPages&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Release();
|
||||
data = other.data;
|
||||
length = other.length;
|
||||
other.data = nullptr;
|
||||
other.length = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MappedPages::~MappedPages() {
|
||||
Release();
|
||||
}
|
||||
|
||||
void MappedPages::Map(const File& file, std::uint64_t fileOffset, std::uint64_t mapLength, bool writable) {
|
||||
Release();
|
||||
void* address = ::mmap(nullptr, mapLength, writable ? PROT_READ | PROT_WRITE : PROT_READ, MAP_SHARED, file.Descriptor(), static_cast<off_t>(fileOffset));
|
||||
if (address == MAP_FAILED) Fail("cannot map header", file.Path());
|
||||
data = static_cast<std::byte*>(address);
|
||||
length = mapLength;
|
||||
}
|
||||
|
||||
void MappedPages::Sync() {
|
||||
if (data != nullptr && ::msync(data, length, MS_SYNC) == -1) throw IOError("msync failed", Errno(), fs::path());
|
||||
}
|
||||
|
||||
void MappedPages::Release() noexcept {
|
||||
if (data != nullptr) {
|
||||
::munmap(data, length);
|
||||
data = nullptr;
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Os ---------------------------------------------------------------------
|
||||
|
||||
void Os::CreateDirectories(const fs::path& directory, fs::perms permissions) {
|
||||
if (directory.empty()) return;
|
||||
fs::path current;
|
||||
for (const fs::path& part : directory) {
|
||||
current /= part;
|
||||
if (current == directory.root_path() || current.empty()) continue;
|
||||
if (::mkdir(current.c_str(), static_cast<mode_t>(permissions)) == -1 && errno != EEXIST) Fail("cannot create directory", current);
|
||||
}
|
||||
}
|
||||
|
||||
void Os::Rename(const fs::path& from, const fs::path& to) {
|
||||
if (::rename(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot rename to {}", to.string()), from);
|
||||
}
|
||||
|
||||
bool Os::RenameNoReplace(const fs::path& from, const fs::path& to) {
|
||||
if (::renameat2(AT_FDCWD, from.c_str(), AT_FDCWD, to.c_str(), RENAME_NOREPLACE) == 0) return true;
|
||||
if (errno == EEXIST) return false;
|
||||
Fail(std::format("cannot rename to {}", to.string()), from);
|
||||
}
|
||||
|
||||
void Os::HardLink(const fs::path& from, const fs::path& to) {
|
||||
if (::link(from.c_str(), to.c_str()) == -1) Fail(std::format("cannot link to {}", to.string()), from);
|
||||
}
|
||||
|
||||
bool Os::Remove(const fs::path& path) noexcept {
|
||||
return ::unlink(path.c_str()) == 0;
|
||||
}
|
||||
|
||||
void Os::SyncDirectory(const fs::path& directory) {
|
||||
const fs::path dir = directory.empty() ? fs::path(".") : directory;
|
||||
const std::int32_t fd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
||||
if (fd == -1) Fail("cannot open directory", dir);
|
||||
const std::int32_t result = ::fsync(fd);
|
||||
::close(fd);
|
||||
if (result == -1) Fail("fsync of directory failed", dir);
|
||||
}
|
||||
|
||||
void Os::RandomBytes(std::span<std::byte> out) {
|
||||
std::random_device device;
|
||||
for (std::size_t i = 0; i < out.size(); i += 4) {
|
||||
const std::uint32_t value = device();
|
||||
std::memcpy(out.data() + i, &value, std::min<std::size_t>(4, out.size() - i));
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t Os::UnixNanos() noexcept {
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::system_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
std::uint64_t Os::PageSize() noexcept {
|
||||
const long size = ::sysconf(_SC_PAGESIZE);
|
||||
return size > 0 ? static_cast<std::uint64_t>(size) : 4096;
|
||||
}
|
||||
|
||||
std::int64_t Os::ProcessId() noexcept {
|
||||
return static_cast<std::int64_t>(::getpid());
|
||||
}
|
||||
615
implementations/Crafter.DB-Storage.cpp
Normal file
615
implementations/Crafter.DB-Storage.cpp
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
// 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<std::uint64_t>(pageSize, Os::PageSize());
|
||||
}
|
||||
|
||||
std::span<const std::byte> 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<std::uint32_t, regionSlots>& 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<std::size_t> 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<std::byte> pages(2 * pageSize);
|
||||
file.ReadAt(0, pages);
|
||||
std::memcpy(&header, pages.data(), sizeof(header));
|
||||
ValidateHeader(header, size, file.Path());
|
||||
const std::optional<std::size_t> slot = ChooseCommit(header);
|
||||
if (!slot) throw CorruptError(std::format("{}: both commit records are damaged", file.Path().string()));
|
||||
chosen = *slot;
|
||||
std::span<const std::byte> blob;
|
||||
std::vector<std::byte> 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<ScalarValue> 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<class A, class B>
|
||||
bool CmpLessPartial(const A& x, const B& y) noexcept {
|
||||
if constexpr (std::is_floating_point_v<A> || std::is_floating_point_v<B>) return static_cast<double>(x) < static_cast<double>(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<std::byte, 6> random{};
|
||||
Os::RandomBytes(random);
|
||||
std::string suffix;
|
||||
for (const std::byte b : random) suffix += std::format("{:02x}", static_cast<std::uint8_t>(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<std::byte> blob = params.schema.Serialize();
|
||||
const std::uint64_t granule = Granule();
|
||||
FileHeader header{};
|
||||
header.magic = fileMagic;
|
||||
header.formatVersion = formatVersion;
|
||||
header.headerPages = static_cast<std::uint16_t>(1 + RoundUp(blob.size(), pageSize) / pageSize);
|
||||
header.formatFlags = formatFlagLittleEndian;
|
||||
header.pageSize = static_cast<std::uint32_t>(granule);
|
||||
header.rowSize = params.schema.rowSize;
|
||||
header.rowAlign = params.schema.rowAlign;
|
||||
header.columnCount = static_cast<std::uint16_t>(params.schema.columns.size());
|
||||
header.orderedColumn = params.orderedColumn;
|
||||
header.regionCount = regionCount;
|
||||
header.schemaVersion = params.schemaVersion;
|
||||
header.schemaFingerprint = params.fingerprint;
|
||||
header.schemaBlobLength = static_cast<std::uint32_t>(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<std::uint32_t, regionSlots> 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<std::uint64_t>(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<std::uint64_t>(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<std::uint8_t>(*const_cast<std::uint8_t*>(reinterpret_cast<const std::uint8_t*>(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<std::uint8_t>(*reinterpret_cast<std::uint8_t*>(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<const std::byte> 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<ScalarValue> 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<ScalarValue> 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<std::size_t> 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<ScalarValue> DynamicRow::Scalar(std::size_t index) const noexcept {
|
||||
return ReadScalar(row, schema->columns[index]);
|
||||
}
|
||||
|
||||
std::optional<std::span<const std::byte>> DynamicRow::Bytes(std::size_t index) const {
|
||||
const ColumnDescriptor& column = schema->columns[index];
|
||||
if (column.kind == Kind::FixedBytes) return std::span<const std::byte>(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<const std::byte>(row + column.offset + 4, var.length);
|
||||
return heap->Read(var.tailOrOffset, var.length);
|
||||
}
|
||||
|
||||
std::optional<std::string_view> 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<std::span<const std::byte>> bytes = Bytes(index);
|
||||
if (!bytes) return std::nullopt;
|
||||
std::string_view text(reinterpret_cast<const char*>(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<std::string_view> text = Text(i)) {
|
||||
out += std::format("\"{}\"", *text);
|
||||
} else if (const std::optional<ScalarValue> scalar = Scalar(i)) {
|
||||
std::visit([&](const auto& v) { out += std::format("{}", v); }, *scalar);
|
||||
} else if (const std::optional<std::span<const std::byte>> bytes = Bytes(i)) {
|
||||
out += std::format("<{} bytes>", bytes->size());
|
||||
} else {
|
||||
out += '?';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Loading…
Reference in a new issue