From d04335c63ae239b7ef401e29c4dd2b7ce9950df6 Mon Sep 17 00:00:00 2001 From: Jay Phan Date: Sun, 26 Apr 2026 17:32:25 -0400 Subject: [PATCH 1/3] tuple encode decode --- Makefile | 4 +- src/sql/tuple.cpp | 265 +++++++++++++++++++++++++++++++++++ src/sql/tuple.h | 81 +++++++++++ tests/sql/test_tuple.cpp | 290 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 src/sql/tuple.cpp create mode 100644 src/sql/tuple.h create mode 100644 tests/sql/test_tuple.cpp diff --git a/Makefile b/Makefile index c77ad2a..8bb70bb 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,13 @@ TEST_OBJS = $(BUILD_DIR)/tests/test_parser.o \ $(BUILD_DIR)/tests/storage/test_slotted_page.o \ $(BUILD_DIR)/tests/storage/test_heap_file.o \ $(BUILD_DIR)/tests/storage/test_integration.o \ + $(BUILD_DIR)/tests/sql/test_tuple.o \ $(BUILD_DIR)/src/parser.o \ $(BUILD_DIR)/src/storage/disk_manager.o \ $(BUILD_DIR)/src/storage/buffer_pool.o \ $(BUILD_DIR)/src/storage/slotted_page.o \ - $(BUILD_DIR)/src/storage/heap_file.o + $(BUILD_DIR)/src/storage/heap_file.o \ + $(BUILD_DIR)/src/sql/tuple.o dbms: $(DBMS_OBJS) $(CXX) $(CXXFLAGS) -o $@ $^ diff --git a/src/sql/tuple.cpp b/src/sql/tuple.cpp new file mode 100644 index 0000000..16ed9db --- /dev/null +++ b/src/sql/tuple.cpp @@ -0,0 +1,265 @@ +#include "src/sql/tuple.h" + +#include +#include +#include +#include + +namespace { + +// Byte width of a fixed-width column type. Text is variable-length and is +// rejected here; tupleSize() (the only caller) is documented as fixed-only. +size_t widthOf(Type t) { + switch (t) { + case Type::Int32: return 4; + case Type::Int64: return 8; + case Type::Bool: return 1; + case Type::Text: + throw std::runtime_error("tuple codec: tupleSize() is undefined for " + "schemas containing Text columns"); + } + throw std::runtime_error("tuple codec: unknown type"); +} + +} // namespace + +size_t Schema::indexOf(const std::string& name) const { + for (size_t i = 0; i < columns.size(); ++i) { + if (columns[i].name == name) return i; + } + return kNotFound; +} + +size_t Schema::tupleSize() const { + const size_t bitmap = (columns.size() + 7) / 8; + size_t data = 0; + for (const auto& c : columns) data += widthOf(c.type); + return bitmap + data; +} + +Value Value::Int32(int32_t v) { + Value r; + r.type = Type::Int32; + r.is_null = false; + r.i32 = v; + return r; +} + +Value Value::Int64(int64_t v) { + Value r; + r.type = Type::Int64; + r.is_null = false; + r.i64 = v; + return r; +} + +Value Value::Bool(bool v) { + Value r; + r.type = Type::Bool; + r.is_null = false; + r.b = v; + return r; +} + +Value Value::Text(std::string v) { + Value r; + r.type = Type::Text; + r.is_null = false; + r.text = std::move(v); + return r; +} + +Value Value::Null(Type t) { + Value r; + r.type = t; + r.is_null = true; + return r; +} + +bool operator==(const Value& a, const Value& b) { + if (a.type != b.type) return false; + if (a.is_null != b.is_null) return false; + if (a.is_null) return true; + switch (a.type) { + case Type::Int32: return a.i32 == b.i32; + case Type::Int64: return a.i64 == b.i64; + case Type::Bool: return a.b == b.b; + case Type::Text: return a.text == b.text; + } + return false; +} + +std::vector TupleCodec::encode(const Schema& s, const std::vector& vals) { + if (vals.size() != s.columns.size()) { + throw std::runtime_error( + "TupleCodec::encode: column count mismatch (got " + + std::to_string(vals.size()) + ", expected " + + std::to_string(s.columns.size()) + ")"); + } + + const size_t n = s.columns.size(); + const size_t bitmap_bytes = (n + 7) / 8; + + // Validate everything before allocating so encode either fully succeeds + // or fully fails with a clear error. + for (size_t i = 0; i < n; ++i) { + const auto& col = s.columns[i]; + const auto& v = vals[i]; + if (v.is_null && !col.nullable) { + throw std::runtime_error( + "TupleCodec::encode: column '" + col.name + "' is not nullable"); + } + if (!v.is_null && v.type != col.type) { + throw std::runtime_error( + "TupleCodec::encode: column '" + col.name + "' type mismatch"); + } + if (col.type == Type::Text && !v.is_null && + v.text.size() > std::numeric_limits::max()) { + throw std::runtime_error( + "TupleCodec::encode: Text value exceeds 4 GB length limit"); + } + } + + // Compute total size once, so we allocate exactly. + size_t total = bitmap_bytes; + for (size_t i = 0; i < n; ++i) { + const auto& col = s.columns[i]; + const auto& v = vals[i]; + switch (col.type) { + case Type::Int32: total += 4; break; + case Type::Int64: total += 8; break; + case Type::Bool: total += 1; break; + case Type::Text: total += 4 + (v.is_null ? 0u : v.text.size()); break; + } + } + + // Zero-init so null slots have deterministic bytes. + std::vector out(total, 0); + + // Null bitmap. + for (size_t i = 0; i < n; ++i) { + if (vals[i].is_null) { + out[i / 8] |= static_cast(1u << (i % 8)); + } + } + + // Payload. + char* p = out.data() + bitmap_bytes; + for (size_t i = 0; i < n; ++i) { + const auto& col = s.columns[i]; + const auto& v = vals[i]; + switch (col.type) { + case Type::Int32: { + if (!v.is_null) std::memcpy(p, &v.i32, 4); + p += 4; + break; + } + case Type::Int64: { + if (!v.is_null) std::memcpy(p, &v.i64, 8); + p += 8; + break; + } + case Type::Bool: { + if (!v.is_null) { + uint8_t x = v.b ? 1 : 0; + std::memcpy(p, &x, 1); + } + p += 1; + break; + } + case Type::Text: { + const uint32_t text_len = v.is_null + ? 0u + : static_cast(v.text.size()); + std::memcpy(p, &text_len, 4); + p += 4; + if (!v.is_null && text_len > 0) { + std::memcpy(p, v.text.data(), text_len); + p += text_len; + } + break; + } + } + } + return out; +} + +std::vector TupleCodec::decode(const Schema& s, const char* bytes, size_t len) { + const size_t n = s.columns.size(); + const size_t bitmap_bytes = (n + 7) / 8; + + if (len < bitmap_bytes) { + throw std::runtime_error( + "TupleCodec::decode: input shorter than null bitmap"); + } + + const char* end = bytes + len; + const char* p = bytes + bitmap_bytes; + + std::vector out; + out.reserve(n); + + for (size_t i = 0; i < n; ++i) { + const auto& col = s.columns[i]; + const bool is_null = + (static_cast(bytes[i / 8]) >> (i % 8)) & 1u; + + Value v; + v.type = col.type; + v.is_null = is_null; + + switch (col.type) { + case Type::Int32: { + if (end - p < 4) { + throw std::runtime_error("TupleCodec::decode: truncated Int32"); + } + if (!is_null) std::memcpy(&v.i32, p, 4); + p += 4; + break; + } + case Type::Int64: { + if (end - p < 8) { + throw std::runtime_error("TupleCodec::decode: truncated Int64"); + } + if (!is_null) std::memcpy(&v.i64, p, 8); + p += 8; + break; + } + case Type::Bool: { + if (end - p < 1) { + throw std::runtime_error("TupleCodec::decode: truncated Bool"); + } + if (!is_null) { + uint8_t x; + std::memcpy(&x, p, 1); + v.b = (x != 0); + } + p += 1; + break; + } + case Type::Text: { + if (end - p < 4) { + throw std::runtime_error( + "TupleCodec::decode: truncated Text length prefix"); + } + uint32_t text_len; + std::memcpy(&text_len, p, 4); + p += 4; + if (static_cast(end - p) < text_len) { + throw std::runtime_error( + "TupleCodec::decode: truncated Text payload"); + } + if (!is_null) v.text.assign(p, text_len); + p += text_len; + break; + } + } + out.push_back(std::move(v)); + } + + if (p != end) { + throw std::runtime_error( + "TupleCodec::decode: trailing bytes after schema"); + } + return out; +} diff --git a/src/sql/tuple.h b/src/sql/tuple.h new file mode 100644 index 0000000..b9ded37 --- /dev/null +++ b/src/sql/tuple.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include + +// SQL column types we know how to serialize. +// Int32 / Int64 / Bool — fixed width. +// Text — variable width, length-prefixed (uint32 + bytes). +enum class Type { Int32, Int64, Bool, Text }; + +struct Column { + std::string name; + Type type; + bool nullable; +}; + +// A row's schema: an ordered list of columns. Pure value type; no I/O. +struct Schema { + std::vector columns; + + // Sentinel returned by indexOf when the name isn't present. Mirrors + // std::string::npos's "size_t with all bits set" convention. + static constexpr size_t kNotFound = static_cast(-1); + + // Find a column by name. Returns kNotFound if absent. + size_t indexOf(const std::string& name) const; + + // Bytes a tuple of this schema occupies on disk *if every column is + // fixed-width*. Throws when the schema contains a Text column, since + // the encoded size then depends on the actual values. Use the size of + // TupleCodec::encode(...) instead for variable-length schemas. + size_t tupleSize() const; +}; + +// A runtime value. The union is only safe to read for the discriminator's +// type when is_null is false; otherwise the payload is unset. +struct Value { + Type type; + bool is_null; + union { + int32_t i32; + int64_t i64; + bool b; + }; + std::string text; // out-of-union for simplicity (Text reserved) + + // Convenience builders. Make tests and call-sites read cleanly. + static Value Int32(int32_t v); + static Value Int64(int64_t v); + static Value Bool(bool v); + static Value Text(std::string v); + static Value Null(Type t); +}; + +bool operator==(const Value& a, const Value& b); +inline bool operator!=(const Value& a, const Value& b) { return !(a == b); } + +// Stateless codec converting a row of Values to/from the byte sequence +// stored in a SlottedPage's tuple area. Layout: +// +// [ null bitmap : ceil(N/8) bytes ][ col0 bytes ][ col1 bytes ] ... +// +// Each column's slot: +// Int32/Int64/Bool — full fixed width regardless of null status; the +// bitmap is authoritative for nullness. +// Text — uint32_t length prefix followed by `length` bytes. +// Null is encoded as length 0 with no payload (and +// the bitmap bit set); empty-but-non-null Text is +// length 0 with the bitmap bit clear. +class TupleCodec { +public: + // Throws std::runtime_error on column-count mismatch, type mismatch, + // or null in a non-nullable column. + static std::vector encode(const Schema& s, const std::vector& vals); + + // Throws if the bytes are truncated mid-column, contain trailing bytes + // after the schema is satisfied, or are too short for the null bitmap. + static std::vector decode(const Schema& s, const char* bytes, size_t len); +}; diff --git a/tests/sql/test_tuple.cpp b/tests/sql/test_tuple.cpp new file mode 100644 index 0000000..b2a1e72 --- /dev/null +++ b/tests/sql/test_tuple.cpp @@ -0,0 +1,290 @@ +#include "tests/vendor/doctest.h" + +#include "src/sql/tuple.h" + +#include +#include +#include +#include +#include + +TEST_CASE("Schema::indexOf finds columns by name") { + Schema s{{ + {"id", Type::Int32, false}, + {"name", Type::Bool, true}, + {"age", Type::Int64, true}, + }}; + CHECK(s.indexOf("id") == 0); + CHECK(s.indexOf("name") == 1); + CHECK(s.indexOf("age") == 2); + CHECK(s.indexOf("missing") == Schema::kNotFound); +} + +TEST_CASE("Schema::tupleSize matches the layout: bitmap + sum of widths") { + Schema s{{ + {"a", Type::Int32, false}, // 4 bytes + {"b", Type::Int64, true}, // 8 bytes + {"c", Type::Bool, false}, // 1 byte + }}; + // bitmap: ceil(3/8) = 1 + // payload: 4 + 8 + 1 = 13 + CHECK(s.tupleSize() == 1 + 13); +} + +TEST_CASE("Schema::tupleSize counts a full bitmap byte every 8 columns") { + Schema s; + for (int i = 0; i < 9; ++i) { + s.columns.push_back({"c" + std::to_string(i), Type::Bool, true}); + } + // 9 columns → bitmap = 2 bytes; 9 bools → 9 bytes payload. + CHECK(s.tupleSize() == 2 + 9); +} + +TEST_CASE("Value factories build values of the expected kind") { + auto i = Value::Int32(42); + CHECK(i.type == Type::Int32); + CHECK_FALSE(i.is_null); + CHECK(i.i32 == 42); + + auto j = Value::Int64(0x0123456789ABCDEFLL); + CHECK(j.type == Type::Int64); + CHECK(j.i64 == 0x0123456789ABCDEFLL); + + auto t = Value::Bool(true); + CHECK(t.type == Type::Bool); + CHECK(t.b == true); + + auto n = Value::Null(Type::Int32); + CHECK(n.type == Type::Int32); + CHECK(n.is_null); +} + +TEST_CASE("Value equality compares type, null, and payload") { + CHECK(Value::Int32(1) == Value::Int32(1)); + CHECK(Value::Int32(1) != Value::Int32(2)); + CHECK(Value::Int32(1) != Value::Int64(1)); // different type + CHECK(Value::Bool(true) != Value::Bool(false)); + CHECK(Value::Null(Type::Int32) == Value::Null(Type::Int32)); + CHECK(Value::Null(Type::Int32) != Value::Null(Type::Bool)); + CHECK(Value::Null(Type::Int32) != Value::Int32(0)); // null vs zero +} + +TEST_CASE("encode/decode round trips a non-null tuple") { + Schema s{{ + {"id", Type::Int32, false}, + {"big", Type::Int64, false}, + {"flag", Type::Bool, false}, + }}; + std::vector vals = { + Value::Int32(0x12345678), + Value::Int64(static_cast(0xCAFEBABEDEADBEEFLL)), + Value::Bool(true), + }; + + auto bytes = TupleCodec::encode(s, vals); + REQUIRE(bytes.size() == s.tupleSize()); + + auto back = TupleCodec::decode(s, bytes.data(), bytes.size()); + REQUIRE(back.size() == vals.size()); + for (size_t i = 0; i < vals.size(); ++i) { + CHECK(back[i] == vals[i]); + } +} + +TEST_CASE("encode/decode round trip with mixed null and non-null columns") { + Schema s{{ + {"a", Type::Int32, true}, + {"b", Type::Int64, true}, + {"c", Type::Bool, false}, + {"d", Type::Int32, true}, + }}; + std::vector vals = { + Value::Null(Type::Int32), + Value::Int64(99), + Value::Bool(false), + Value::Null(Type::Int32), + }; + auto bytes = TupleCodec::encode(s, vals); + auto back = TupleCodec::decode(s, bytes.data(), bytes.size()); + CHECK(back == vals); +} + +TEST_CASE("empty schema round trips an empty tuple") { + Schema s{}; + auto bytes = TupleCodec::encode(s, {}); + CHECK(bytes.empty()); + // decode with empty bytes works because tupleSize() == 0. + auto back = TupleCodec::decode(s, bytes.data(), 0); + CHECK(back.empty()); +} + +TEST_CASE("encode rejects column count mismatch") { + Schema s{{{"a", Type::Int32, false}, {"b", Type::Bool, false}}}; + CHECK_THROWS_AS(TupleCodec::encode(s, {Value::Int32(1)}), std::runtime_error); +} + +TEST_CASE("encode rejects mismatched value type") { + Schema s{{{"a", Type::Int32, false}}}; + CHECK_THROWS_AS(TupleCodec::encode(s, {Value::Int64(1)}), std::runtime_error); + CHECK_THROWS_AS(TupleCodec::encode(s, {Value::Bool(false)}), std::runtime_error); +} + +TEST_CASE("encode rejects null in a non-nullable column") { + Schema s{{{"a", Type::Int32, false}}}; + CHECK_THROWS_AS(TupleCodec::encode(s, {Value::Null(Type::Int32)}), std::runtime_error); +} + +TEST_CASE("tupleSize is undefined when the schema contains Text columns") { + // tupleSize is for fixed-only schemas; Text is variable-length, so its + // size depends on the actual values. Throw rather than guess. + Schema s{{{"a", Type::Text, true}}}; + CHECK_THROWS_AS(s.tupleSize(), std::runtime_error); +} + +TEST_CASE("Text round trips a non-null value of varying lengths") { + Schema s{{{"name", Type::Text, false}}}; + + for (const std::string& sample : {std::string(""), + std::string("hi"), + std::string("a longer text value"), + std::string(1000, 'x')}) { + std::vector vals = {Value::Text(sample)}; + auto bytes = TupleCodec::encode(s, vals); + // Bitmap (1 byte) + length prefix (4 bytes) + payload. + REQUIRE(bytes.size() == 1 + 4 + sample.size()); + auto back = TupleCodec::decode(s, bytes.data(), bytes.size()); + REQUIRE(back.size() == 1); + CHECK_FALSE(back[0].is_null); + CHECK(back[0].text == sample); + } +} + +TEST_CASE("Text distinguishes null from empty string via the bitmap") { + Schema s{{{"name", Type::Text, true}}}; + + // Empty string: not null, length 0. + auto e_bytes = TupleCodec::encode(s, {Value::Text("")}); + auto e_back = TupleCodec::decode(s, e_bytes.data(), e_bytes.size()); + CHECK_FALSE(e_back[0].is_null); + CHECK(e_back[0].text == ""); + + // Null: bitmap bit set, length still 0. + auto n_bytes = TupleCodec::encode(s, {Value::Null(Type::Text)}); + auto n_back = TupleCodec::decode(s, n_bytes.data(), n_bytes.size()); + CHECK(n_back[0].is_null); + + // Same byte length, different bitmap. + REQUIRE(e_bytes.size() == n_bytes.size()); + CHECK(e_bytes[0] != n_bytes[0]); +} + +TEST_CASE("mixed schema with fixed and Text columns round-trips") { + Schema s{{ + {"id", Type::Int32, false}, + {"name", Type::Text, false}, + {"score", Type::Int64, true}, + {"label", Type::Text, true}, + {"active", Type::Bool, false}, + }}; + std::vector vals = { + Value::Int32(7), + Value::Text("alice"), + Value::Null(Type::Int64), + Value::Text(""), // non-null empty text + Value::Bool(true), + }; + + auto bytes = TupleCodec::encode(s, vals); + auto back = TupleCodec::decode(s, bytes.data(), bytes.size()); + CHECK(back == vals); +} + +TEST_CASE("decode rejects truncated Text length prefix or payload") { + Schema s{{{"name", Type::Text, false}}}; + auto good = TupleCodec::encode(s, {Value::Text("hello")}); + + // Lop off the payload — length says 5 but only 3 bytes follow. + auto truncated_payload = good; + truncated_payload.resize(truncated_payload.size() - 2); + CHECK_THROWS_AS(TupleCodec::decode(s, truncated_payload.data(), + truncated_payload.size()), + std::runtime_error); + + // Also lop off most of the length prefix itself. + auto truncated_prefix = good; + truncated_prefix.resize(2); // bitmap byte + 1 byte of length + CHECK_THROWS_AS(TupleCodec::decode(s, truncated_prefix.data(), + truncated_prefix.size()), + std::runtime_error); +} + +TEST_CASE("decode rejects wrong byte length") { + Schema s{{{"a", Type::Int32, false}}}; + auto good = TupleCodec::encode(s, {Value::Int32(1)}); + CHECK_THROWS_AS(TupleCodec::decode(s, good.data(), good.size() - 1), std::runtime_error); + CHECK_THROWS_AS(TupleCodec::decode(s, good.data(), good.size() + 1), std::runtime_error); +} + +TEST_CASE("randomized round-trip stress: 200 random schemas/tuples") { + std::mt19937 rng(0xBEEFu); + std::uniform_int_distribution n_cols_dist(1, 8); + std::uniform_int_distribution type_dist(0, 3); // includes Text + std::uniform_int_distribution coin(0, 1); + std::uniform_int_distribution text_len_dist(0, 40); + std::uniform_int_distribution byte_dist(0, 255); + + auto type_for = [](int t) { + switch (t) { + case 0: return Type::Int32; + case 1: return Type::Int64; + case 2: return Type::Bool; + default: return Type::Text; + } + }; + + for (int trial = 0; trial < 200; ++trial) { + Schema s; + const int n_cols = n_cols_dist(rng); + for (int i = 0; i < n_cols; ++i) { + s.columns.push_back({"c" + std::to_string(i), + type_for(type_dist(rng)), + coin(rng) != 0}); + } + + std::vector vals; + for (const auto& col : s.columns) { + const bool make_null = col.nullable && coin(rng) == 0; + if (make_null) { + vals.push_back(Value::Null(col.type)); + continue; + } + switch (col.type) { + case Type::Int32: + vals.push_back(Value::Int32(static_cast(rng()))); + break; + case Type::Int64: { + const int64_t hi = static_cast(rng()) << 32; + const int64_t lo = static_cast(rng()); + vals.push_back(Value::Int64(hi | lo)); + break; + } + case Type::Bool: + vals.push_back(Value::Bool(coin(rng) != 0)); + break; + case Type::Text: { + std::string t(text_len_dist(rng), '\0'); + for (auto& c : t) c = static_cast(byte_dist(rng)); + vals.push_back(Value::Text(std::move(t))); + break; + } + } + } + + auto bytes = TupleCodec::encode(s, vals); + auto back = TupleCodec::decode(s, bytes.data(), bytes.size()); + REQUIRE(back.size() == vals.size()); + for (size_t i = 0; i < vals.size(); ++i) { + REQUIRE(back[i] == vals[i]); + } + } +} From 89e4ff24f122e4a39fb506e9293b776503e88366 Mon Sep 17 00:00:00 2001 From: Jay Phan Date: Mon, 27 Apr 2026 16:48:53 -0400 Subject: [PATCH 2/3] add readme --- src/storage/README.md | 4 ++++ src/storage/image.png | Bin 0 -> 53523 bytes 2 files changed, 4 insertions(+) create mode 100644 src/storage/image.png diff --git a/src/storage/README.md b/src/storage/README.md index 5a1bd08..6a3da1b 100644 --- a/src/storage/README.md +++ b/src/storage/README.md @@ -62,3 +62,7 @@ Each layer has its own test file under `tests/storage/`. The end-to-end persistence test lives in `tests/storage/test_integration.cpp` (loads 1000 rows, simulates a program restart by destroying every storage object, then scans the rows back). + + +## Data flow +![alt text](image.png) \ No newline at end of file diff --git a/src/storage/image.png b/src/storage/image.png new file mode 100644 index 0000000000000000000000000000000000000000..722f94e4a435a1372b3462dde4f35a6c72a26865 GIT binary patch literal 53523 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 literal 0 HcmV?d00001 From 7e628c8416445a9cf872235f80bed6664c3adf2c Mon Sep 17 00:00:00 2001 From: Jay Phan Date: Mon, 27 Apr 2026 17:39:21 -0400 Subject: [PATCH 3/3] catalog --- Makefile | 13 +- main.cpp | 122 +++++++++++++- src/sql/catalog.cpp | 161 ++++++++++++++++++ src/sql/catalog.h | 88 ++++++++++ src/sql/tuple.cpp | 22 +++ src/sql/tuple.h | 7 + tests/sql/test_catalog.cpp | 334 +++++++++++++++++++++++++++++++++++++ tests/sql/test_tuple.cpp | 8 + 8 files changed, 747 insertions(+), 8 deletions(-) create mode 100644 src/sql/catalog.cpp create mode 100644 src/sql/catalog.h create mode 100644 tests/sql/test_catalog.cpp diff --git a/Makefile b/Makefile index 8bb70bb..6c586a9 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,14 @@ CXXFLAGS = -std=c++17 -Wall -Wextra -O2 -I. BUILD_DIR = build -DBMS_OBJS = $(BUILD_DIR)/main.o $(BUILD_DIR)/src/parser.o +DBMS_OBJS = $(BUILD_DIR)/main.o \ + $(BUILD_DIR)/src/parser.o \ + $(BUILD_DIR)/src/storage/disk_manager.o \ + $(BUILD_DIR)/src/storage/buffer_pool.o \ + $(BUILD_DIR)/src/storage/slotted_page.o \ + $(BUILD_DIR)/src/storage/heap_file.o \ + $(BUILD_DIR)/src/sql/tuple.o \ + $(BUILD_DIR)/src/sql/catalog.o TEST_OBJS = $(BUILD_DIR)/tests/test_parser.o \ $(BUILD_DIR)/tests/storage/test_disk_manager.o \ $(BUILD_DIR)/tests/storage/test_buffer_pool.o \ @@ -11,12 +18,14 @@ TEST_OBJS = $(BUILD_DIR)/tests/test_parser.o \ $(BUILD_DIR)/tests/storage/test_heap_file.o \ $(BUILD_DIR)/tests/storage/test_integration.o \ $(BUILD_DIR)/tests/sql/test_tuple.o \ + $(BUILD_DIR)/tests/sql/test_catalog.o \ $(BUILD_DIR)/src/parser.o \ $(BUILD_DIR)/src/storage/disk_manager.o \ $(BUILD_DIR)/src/storage/buffer_pool.o \ $(BUILD_DIR)/src/storage/slotted_page.o \ $(BUILD_DIR)/src/storage/heap_file.o \ - $(BUILD_DIR)/src/sql/tuple.o + $(BUILD_DIR)/src/sql/tuple.o \ + $(BUILD_DIR)/src/sql/catalog.o dbms: $(DBMS_OBJS) $(CXX) $(CXXFLAGS) -o $@ $^ diff --git a/main.cpp b/main.cpp index 784c7d9..67be420 100644 --- a/main.cpp +++ b/main.cpp @@ -1,11 +1,22 @@ #include "src/parser.h" +#include "src/sql/catalog.h" +#include "src/sql/tuple.h" +#include "src/storage/buffer_pool.h" +#include "src/storage/disk_manager.h" +#include "src/storage/heap_file.h" +#include +#include #include #include #include +#include #include -// Pretty-print a parsed SelectQuery to stdout in a stable, debuggable format. +// ============================================================================= +// Parser demo — parse a handful of SQL strings and print the resulting AST. +// ============================================================================= + static void printQuery(const SelectQuery& q) { std::cout << " columns: "; if (q.select_all) { @@ -34,9 +45,8 @@ static void printQuery(const SelectQuery& q) { } } -// Driver: parse a handful of example queries and print the resulting AST. -// The last query is intentionally malformed to exercise the error path. -int main() { +static void runParserDemo() { + std::cout << "=== Parser demo ==========================================\n\n"; const std::vector queries = { "SELECT id, name FROM users WHERE age > 18", "SELECT * FROM products", @@ -46,7 +56,6 @@ int main() { "SELECT * FROM a JOIN b ON a.x = b.x JOIN c ON b.y = c.y WHERE c.z > 0", "SELECT FROM users", }; - for (const auto& sql : queries) { std::cout << "SQL: " << sql << "\n"; try { @@ -54,11 +63,112 @@ int main() { SelectQuery q = p.parse(); printQuery(q); } catch (const std::exception& e) { - // Lex or parse error — keep going so remaining examples still run. std::cout << " error: " << e.what() << "\n"; } std::cout << "\n"; } +} + +// ============================================================================= +// Storage demo — exercise the full storage + catalog stack end-to-end: +// +// 1. open a brand-new database file +// 2. create the catalog (allocates __tables and __columns at pages 0/1) +// 3. createTable("users", schema) +// 4. insert a few rows via TupleCodec → HeapFile +// 5. flush, drop the in-memory state, reopen +// 6. open the catalog with no extra information, look up "users", +// open its heap file, scan and print every row +// ============================================================================= + +namespace { + +void seedUsers(BufferPool& bp, const Catalog::TableInfo& info) { + const std::vector> rows = { + {1, "alice", 30}, + {2, "bob", 25}, + {3, "carol", 40}, + {4, "dave", 19}, + {5, "eve", 33}, + }; + HeapFile hf(&bp, info.root_page); + for (const auto& [id, name, age] : rows) { + const auto bytes = TupleCodec::encode(info.schema, { + Value::Int32(id), + Value::Text(name), + Value::Int32(age), + }); + hf.insert(bytes.data(), bytes.size()); + } + std::cout << " inserted " << rows.size() << " rows into 'users'\n"; +} + +void scanUsers(BufferPool& bp, const Catalog::TableInfo& info) { + HeapFile hf(&bp, info.root_page); + for (const auto& [rid, bytes] : hf) { + const auto vals = TupleCodec::decode(info.schema, bytes.data(), bytes.size()); + std::cout << " rid=(" << rid.page_id << "," << rid.slot_id << ")" + << " id=" << vals[0].i32 + << " name=" << vals[1].text + << " age=" << vals[2].i32 << "\n"; + } +} + +} // namespace +static void runStorageDemo() { + std::cout << "=== Storage demo =========================================\n\n"; + const std::string path = "/tmp/dbms_demo.db"; + + // Start from a clean slate so the demo is reproducible. + std::error_code ec; + std::filesystem::remove(path, ec); + + // ----- Phase 1: create + populate ------------------------------------ + { + DiskManager dm(path); + BufferPool bp(8, &dm); + Catalog cat = Catalog::create(&bp); + + const Schema users_schema{{ + {"id", Type::Int32, false}, + {"name", Type::Text, false}, + {"age", Type::Int32, true}, + }}; + cat.createTable("users", users_schema); + + std::cout << "[phase 1] created catalog + table 'users'\n"; + std::cout << " __tables is at page " << Catalog::TABLES_ROOT << "\n"; + std::cout << " __columns is at page " << Catalog::COLUMNS_ROOT << "\n"; + std::cout << " users heap is at page " + << cat.getTable("users")->root_page << "\n"; + + seedUsers(bp, *cat.getTable("users")); + bp.flushAll(); + std::cout << " flushed; file size = " + << std::filesystem::file_size(path) << " bytes\n\n"; + } + + // ----- Phase 2: cold reopen + scan ----------------------------------- + DiskManager dm(path); + BufferPool bp(8, &dm); + Catalog cat(&bp); // bootstrap from pages 0 + 1 + + std::cout << "[phase 2] reopened database; tables in catalog:"; + for (const auto& n : cat.tableNames()) std::cout << " " << n; + std::cout << "\n"; + + const auto* info = cat.getTable("users"); + std::cout << " scanning 'users':\n"; + scanUsers(bp, *info); + std::cout << "\n"; + + // Tidy up so successive runs always start clean. + std::filesystem::remove(path, ec); +} + +int main() { + // runParserDemo(); + runStorageDemo(); return 0; } diff --git a/src/sql/catalog.cpp b/src/sql/catalog.cpp new file mode 100644 index 0000000..753527e --- /dev/null +++ b/src/sql/catalog.cpp @@ -0,0 +1,161 @@ +#include "src/sql/catalog.h" + +#include +#include +#include +#include +#include + +Schema Catalog::tablesSchema() { + return Schema{{ + {"table_id", Type::Int32, false}, + {"name", Type::Text, false}, + {"first_page_id", Type::Int64, false}, + }}; +} + +Schema Catalog::columnsSchema() { + return Schema{{ + {"table_id", Type::Int32, false}, + {"position", Type::Int32, false}, + {"name", Type::Text, false}, + {"type", Type::Int32, false}, + {"nullable", Type::Bool, false}, + }}; +} + +Catalog::Catalog(BufferPool* bp) + : bp_(bp), + tables_hf_(bp, TABLES_ROOT), + columns_hf_(bp, COLUMNS_ROOT), + next_table_id_(0) { + loadFromDisk(); +} + +Catalog Catalog::create(BufferPool* bp) { + // Allocating __tables must yield page 0; __columns must yield page 1. + // Anything else means the database already had data, in which case the + // bootstrap convention is broken and we'd silently mis-read on reopen. + HeapFile tables_hf = HeapFile::create(bp); + if (tables_hf.firstPageId() != TABLES_ROOT) { + throw std::runtime_error( + "Catalog::create: __tables did not land at page 0; " + "is the database already initialized?"); + } + HeapFile columns_hf = HeapFile::create(bp); + if (columns_hf.firstPageId() != COLUMNS_ROOT) { + throw std::runtime_error( + "Catalog::create: __columns did not land at page 1; " + "is the database already initialized?"); + } + return Catalog(bp); +} + +void Catalog::loadFromDisk() { + const Schema tables_s = tablesSchema(); + const Schema columns_s = columnsSchema(); + + // Pass 1: pull every row out of __tables. Schemas are filled in in pass 2. + std::vector infos; + for (auto it = tables_hf_.begin(); it != tables_hf_.end(); ++it) { + const auto& bytes = it->second; + auto vals = TupleCodec::decode(tables_s, bytes.data(), bytes.size()); + TableInfo info; + info.table_id = vals[0].i32; + info.name = vals[1].text; + info.root_page = static_cast(vals[2].i64); + infos.push_back(std::move(info)); + } + + // Pass 2: collect column rows from __columns, group by table_id, sort by + // position. Sorting by position lets us reconstruct the user's column + // order even if the catalog was edited out of order across crashes. + struct ColRow { + int32_t position; + std::string name; + Type type; + bool nullable; + }; + std::map> by_table; + for (auto it = columns_hf_.begin(); it != columns_hf_.end(); ++it) { + const auto& bytes = it->second; + auto vals = TupleCodec::decode(columns_s, bytes.data(), bytes.size()); + const int32_t tid = vals[0].i32; + ColRow c{vals[1].i32, vals[2].text, typeFromCode(vals[3].i32), vals[4].b}; + by_table[tid].push_back(std::move(c)); + } + + // Pass 3: stitch each table's columns into its Schema, install in cache, + // and track the next free table_id. + int32_t max_id = -1; + for (auto& info : infos) { + auto cols_it = by_table.find(info.table_id); + if (cols_it != by_table.end()) { + auto& cols = cols_it->second; + std::sort(cols.begin(), cols.end(), + [](const ColRow& a, const ColRow& b) { + return a.position < b.position; + }); + info.schema.columns.reserve(cols.size()); + for (auto& c : cols) { + info.schema.columns.push_back({std::move(c.name), c.type, c.nullable}); + } + } + max_id = std::max(max_id, info.table_id); + tables_.emplace(info.name, std::move(info)); + } + next_table_id_ = max_id + 1; +} + +void Catalog::createTable(const std::string& name, Schema schema) { + if (hasTable(name)) { + throw std::runtime_error("Catalog: table '" + name + "' already exists"); + } + + // Allocate the user heap file first. If we crash before recording in + // the catalog, the worst case is one orphan page; the alternative + // (record-first, allocate-after) leaves dangling rows pointing at + // nothing. + HeapFile new_hf = HeapFile::create(bp_); + const PageId root = new_hf.firstPageId(); + const int32_t table_id = next_table_id_++; + + // Append to __tables. + { + const auto bytes = TupleCodec::encode(tablesSchema(), { + Value::Int32(table_id), + Value::Text(name), + Value::Int64(static_cast(root)), + }); + tables_hf_.insert(bytes.data(), bytes.size()); + } + + // Append one row per column to __columns. Position is the column's + // index in the user's schema and is what we sort by on reload. + const Schema cs = columnsSchema(); + for (size_t i = 0; i < schema.columns.size(); ++i) { + const auto& col = schema.columns[i]; + const auto bytes = TupleCodec::encode(cs, { + Value::Int32(table_id), + Value::Int32(static_cast(i)), + Value::Text(col.name), + Value::Int32(typeToCode(col.type)), + Value::Bool(col.nullable), + }); + columns_hf_.insert(bytes.data(), bytes.size()); + } + + tables_.emplace(name, TableInfo{table_id, name, std::move(schema), root}); +} + +const Catalog::TableInfo* Catalog::getTable(const std::string& name) const { + auto it = tables_.find(name); + return it == tables_.end() ? nullptr : &it->second; +} + +std::vector Catalog::tableNames() const { + std::vector out; + out.reserve(tables_.size()); + for (const auto& kv : tables_) out.push_back(kv.first); + return out; +} diff --git a/src/sql/catalog.h b/src/sql/catalog.h new file mode 100644 index 0000000..4f33fd5 --- /dev/null +++ b/src/sql/catalog.h @@ -0,0 +1,88 @@ +#pragma once + +#include "src/sql/tuple.h" +#include "src/storage/buffer_pool.h" +#include "src/storage/disk_manager.h" +#include "src/storage/heap_file.h" + +#include +#include +#include +#include +#include + +// Persistent table catalog, stored as two ordinary heap files: +// +// __tables (table_id, name, first_page_id) +// __columns(table_id, position, name, type, nullable) +// +// One row per user table in __tables, one row per column in __columns. +// These system tables live at hard-coded page ids — page 0 is __tables, +// page 1 is __columns. That hard-coding is the bootstrap: opening a +// database is "open those two heap files at known offsets and walk them." +// +// On startup the catalog is fully reconstructed in memory and serves as +// a read-through cache. Mutations (createTable) update both system +// tables on disk and the in-memory cache. +// +// __tables and __columns are not themselves listed in __tables — they +// live below the catalog. This avoids the chicken-and-egg of needing +// the catalog to read the catalog. +// +// Single-process. If you have two Catalog instances open on the same +// database file they will both load on construction but won't see each +// other's edits. +class Catalog { +public: + // Hard-coded bootstrap pages. They are the first two pages allocated + // on a brand-new database file by Catalog::create. + static constexpr PageId TABLES_ROOT = 0; + static constexpr PageId COLUMNS_ROOT = 1; + + struct TableInfo { + int32_t table_id; + std::string name; + Schema schema; + PageId root_page; + }; + + // Open an existing catalog. Reads __tables and __columns at the + // bootstrap page ids and rebuilds the in-memory cache. + explicit Catalog(BufferPool* bp); + + // Allocate the bootstrap pages on a brand-new database and return a + // ready-to-use Catalog. Throws if the disk already has pages + // allocated, since that would leave __tables and __columns at the + // wrong page ids. + static Catalog create(BufferPool* bp); + + // Allocate a heap file for `name`, append one row to __tables and + // schema.columns.size() rows to __columns, update the cache. + // Throws if `name` already exists. + void createTable(const std::string& name, Schema schema); + + bool hasTable(const std::string& name) const { + return tables_.count(name) != 0; + } + + // Returns nullptr when absent. The pointer is stable across later + // createTable calls (unordered_map insertions don't invalidate + // references) and remains valid for this Catalog's lifetime. + const TableInfo* getTable(const std::string& name) const; + + std::vector tableNames() const; + +private: + BufferPool* bp_; + HeapFile tables_hf_; + HeapFile columns_hf_; + std::unordered_map tables_; + int32_t next_table_id_; + + // Hard-coded schemas of the two system tables. + static Schema tablesSchema(); + static Schema columnsSchema(); + + // Walk __tables + __columns and populate `tables_` and `next_table_id_`. + void loadFromDisk(); +}; diff --git a/src/sql/tuple.cpp b/src/sql/tuple.cpp index 16ed9db..f963a45 100644 --- a/src/sql/tuple.cpp +++ b/src/sql/tuple.cpp @@ -76,6 +76,28 @@ Value Value::Null(Type t) { return r; } +int32_t typeToCode(Type t) { + switch (t) { + case Type::Int32: return 0; + case Type::Int64: return 1; + case Type::Bool: return 2; + case Type::Text: return 3; + } + throw std::runtime_error("typeToCode: unknown Type"); +} + +Type typeFromCode(int32_t c) { + switch (c) { + case 0: return Type::Int32; + case 1: return Type::Int64; + case 2: return Type::Bool; + case 3: return Type::Text; + default: + throw std::runtime_error("typeFromCode: invalid type code " + + std::to_string(c)); + } +} + bool operator==(const Value& a, const Value& b) { if (a.type != b.type) return false; if (a.is_null != b.is_null) return false; diff --git a/src/sql/tuple.h b/src/sql/tuple.h index b9ded37..01c263d 100644 --- a/src/sql/tuple.h +++ b/src/sql/tuple.h @@ -57,6 +57,13 @@ struct Value { bool operator==(const Value& a, const Value& b); inline bool operator!=(const Value& a, const Value& b) { return !(a == b); } +// Stable on-disk integer encoding of a Type, used anywhere the type +// itself needs to be persisted (the catalog's __columns table is the +// current consumer). The numeric values are part of the file format — +// never reorder or reuse them. +int32_t typeToCode(Type t); +Type typeFromCode(int32_t c); + // Stateless codec converting a row of Values to/from the byte sequence // stored in a SlottedPage's tuple area. Layout: // diff --git a/tests/sql/test_catalog.cpp b/tests/sql/test_catalog.cpp new file mode 100644 index 0000000..bca5e70 --- /dev/null +++ b/tests/sql/test_catalog.cpp @@ -0,0 +1,334 @@ +#include "tests/vendor/doctest.h" + +#include "src/sql/catalog.h" +#include "src/sql/tuple.h" +#include "src/storage/buffer_pool.h" +#include "src/storage/disk_manager.h" +#include "src/storage/heap_file.h" +#include "tests/test_util.h" + +#include +#include +#include +#include + +namespace { + +Schema makePersonSchema() { + return Schema{{ + {"id", Type::Int32, false}, + {"name", Type::Text, false}, + {"age", Type::Int32, true}, + }}; +} + +Schema makeOrderSchema() { + return Schema{{ + {"id", Type::Int64, false}, + {"customer", Type::Text, false}, + {"total_cents", Type::Int32, false}, + {"shipped", Type::Bool, false}, + }}; +} + +} // namespace + +TEST_CASE("Catalog::create allocates pages 0 and 1 as the bootstrap heap files") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + CHECK(Catalog::TABLES_ROOT == 0); + CHECK(Catalog::COLUMNS_ROOT == 1); + // Both system tables have been allocated. + CHECK(dm.numPages() == 2); +} + +TEST_CASE("Catalog::create on a non-empty disk throws") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + + // Pretend something else allocated page 0 already. + dm.allocatePage(); + CHECK_THROWS_AS(Catalog::create(&bp), std::runtime_error); +} + +TEST_CASE("a fresh catalog reports no user tables") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + CHECK_FALSE(cat.hasTable("anything")); + CHECK(cat.getTable("anything") == nullptr); + CHECK(cat.tableNames().empty()); +} + +TEST_CASE("createTable registers a table that hasTable / getTable can find") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("people", makePersonSchema()); + + CHECK(cat.hasTable("people")); + const auto* info = cat.getTable("people"); + REQUIRE(info != nullptr); + CHECK(info->name == "people"); + CHECK(info->table_id == 0); + REQUIRE(info->schema.columns.size() == 3); + CHECK(info->schema.columns[0].name == "id"); + CHECK(info->schema.columns[0].type == Type::Int32); + CHECK_FALSE(info->schema.columns[0].nullable); + CHECK(info->schema.columns[1].type == Type::Text); + CHECK(info->schema.columns[2].nullable); + // Brand-new table sits past the bootstrap pages. + CHECK(info->root_page > Catalog::COLUMNS_ROOT); +} + +TEST_CASE("createTable with a duplicate name throws") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("people", makePersonSchema()); + CHECK_THROWS_AS(cat.createTable("people", makePersonSchema()), + std::runtime_error); +} + +TEST_CASE("table_ids are assigned monotonically in creation order") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("a", makePersonSchema()); + cat.createTable("b", makeOrderSchema()); + cat.createTable("c", makePersonSchema()); + + CHECK(cat.getTable("a")->table_id == 0); + CHECK(cat.getTable("b")->table_id == 1); + CHECK(cat.getTable("c")->table_id == 2); +} + +TEST_CASE("multiple tables coexist with distinct heap files") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("people", makePersonSchema()); + cat.createTable("orders", makeOrderSchema()); + + auto names = cat.tableNames(); + REQUIRE(names.size() == 2); + std::sort(names.begin(), names.end()); + CHECK(names[0] == "orders"); + CHECK(names[1] == "people"); + + const auto* p = cat.getTable("people"); + const auto* o = cat.getTable("orders"); + REQUIRE(p); + REQUIRE(o); + CHECK(p->root_page != o->root_page); + CHECK(p->root_page != Catalog::TABLES_ROOT); + CHECK(p->root_page != Catalog::COLUMNS_ROOT); + CHECK(o->root_page != Catalog::TABLES_ROOT); + CHECK(o->root_page != Catalog::COLUMNS_ROOT); +} + +TEST_CASE("schema with all four column types round-trips through the catalog") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + Schema mixed{{ + {"a", Type::Int32, true}, + {"b", Type::Int64, false}, + {"c", Type::Bool, true}, + {"d", Type::Text, false}, + }}; + cat.createTable("mixed", mixed); + + const auto* info = cat.getTable("mixed"); + REQUIRE(info); + REQUIRE(info->schema.columns.size() == 4); + CHECK(info->schema.columns[0].type == Type::Int32); + CHECK(info->schema.columns[0].nullable); + CHECK(info->schema.columns[1].type == Type::Int64); + CHECK_FALSE(info->schema.columns[1].nullable); + CHECK(info->schema.columns[2].type == Type::Bool); + CHECK(info->schema.columns[2].nullable); + CHECK(info->schema.columns[3].type == Type::Text); + CHECK_FALSE(info->schema.columns[3].nullable); +} + +TEST_CASE("catalog persists across DiskManager / BufferPool restart") { + TempFile tf; + { + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + cat.createTable("people", makePersonSchema()); + cat.createTable("orders", makeOrderSchema()); + bp.flushAll(); + } + + // Cold reopen: only `tf.path()` survives across the boundary. + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat(&bp); + + CHECK(cat.hasTable("people")); + CHECK(cat.hasTable("orders")); + + const auto* p = cat.getTable("people"); + REQUIRE(p); + CHECK(p->table_id == 0); + REQUIRE(p->schema.columns.size() == 3); + CHECK(p->schema.columns[1].name == "name"); + CHECK(p->schema.columns[1].type == Type::Text); + CHECK(p->schema.columns[2].nullable); + + const auto* o = cat.getTable("orders"); + REQUIRE(o); + CHECK(o->table_id == 1); + REQUIRE(o->schema.columns.size() == 4); + CHECK(o->schema.columns[3].type == Type::Bool); +} + +TEST_CASE("table_ids do not collide with new tables created after restart") { + TempFile tf; + { + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + cat.createTable("a", makePersonSchema()); + cat.createTable("b", makePersonSchema()); + bp.flushAll(); + } + + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat(&bp); + cat.createTable("c", makePersonSchema()); + + CHECK(cat.getTable("a")->table_id == 0); + CHECK(cat.getTable("b")->table_id == 1); + CHECK(cat.getTable("c")->table_id == 2); +} + +TEST_CASE("a registered table's heap file is independently usable") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + cat.createTable("people", makePersonSchema()); + + const auto* info = cat.getTable("people"); + REQUIRE(info); + + HeapFile people(&bp, info->root_page); + + auto bytes = TupleCodec::encode(info->schema, { + Value::Int32(1), + Value::Text("alice"), + Value::Int32(30), + }); + RID r = people.insert(bytes.data(), bytes.size()); + + std::string out; + REQUIRE(people.get(r, &out)); + auto vals = TupleCodec::decode(info->schema, out.data(), out.size()); + CHECK(vals[0].i32 == 1); + CHECK(vals[1].text == "alice"); + CHECK(vals[2].i32 == 30); +} + +TEST_CASE("end-to-end: catalog + 100 rows, restart, full scan") { + TempFile tf; + { + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + cat.createTable("people", makePersonSchema()); + + const auto* info = cat.getTable("people"); + REQUIRE(info); + HeapFile people(&bp, info->root_page); + for (int i = 0; i < 100; ++i) { + auto bytes = TupleCodec::encode(info->schema, { + Value::Int32(i), + Value::Text("name_" + std::to_string(i)), + Value::Int32(20 + (i % 50)), + }); + people.insert(bytes.data(), bytes.size()); + } + bp.flushAll(); + } + + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat(&bp); + + const auto* info = cat.getTable("people"); + REQUIRE(info); + HeapFile people(&bp, info->root_page); + + int count = 0; + int sum_ids = 0; + for (const auto& [rid, bytes] : people) { + (void)rid; + auto vals = TupleCodec::decode(info->schema, bytes.data(), bytes.size()); + REQUIRE(vals.size() == 3); + sum_ids += vals[0].i32; + ++count; + } + CHECK(count == 100); + CHECK(sum_ids == 100 * 99 / 2); +} + +TEST_CASE("getTable pointer remains valid after subsequent createTable calls") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("first", makePersonSchema()); + const auto* first = cat.getTable("first"); + REQUIRE(first); + const PageId first_root = first->root_page; + + cat.createTable("second", makeOrderSchema()); + cat.createTable("third", makePersonSchema()); + + CHECK(first->name == "first"); + CHECK(first->root_page == first_root); +} + +TEST_CASE("__tables and __columns hold the expected number of rows") { + TempFile tf; + DiskManager dm(tf.path()); + BufferPool bp(4, &dm); + Catalog cat = Catalog::create(&bp); + + cat.createTable("people", makePersonSchema()); // 3 columns + cat.createTable("orders", makeOrderSchema()); // 4 columns + + // Reach into the system tables directly to confirm row counts. + HeapFile tables_hf(&bp, Catalog::TABLES_ROOT); + HeapFile columns_hf(&bp, Catalog::COLUMNS_ROOT); + + int n_tables = 0; + for (auto it = tables_hf.begin(); it != tables_hf.end(); ++it) ++n_tables; + CHECK(n_tables == 2); + + int n_columns = 0; + for (auto it = columns_hf.begin(); it != columns_hf.end(); ++it) ++n_columns; + CHECK(n_columns == 3 + 4); +} diff --git a/tests/sql/test_tuple.cpp b/tests/sql/test_tuple.cpp index b2a1e72..0bf910e 100644 --- a/tests/sql/test_tuple.cpp +++ b/tests/sql/test_tuple.cpp @@ -134,6 +134,14 @@ TEST_CASE("encode rejects null in a non-nullable column") { CHECK_THROWS_AS(TupleCodec::encode(s, {Value::Null(Type::Int32)}), std::runtime_error); } +TEST_CASE("typeToCode and typeFromCode round trip every Type and reject garbage") { + for (Type t : {Type::Int32, Type::Int64, Type::Bool, Type::Text}) { + CHECK(typeFromCode(typeToCode(t)) == t); + } + CHECK_THROWS_AS(typeFromCode(99), std::runtime_error); + CHECK_THROWS_AS(typeFromCode(-1), std::runtime_error); +} + TEST_CASE("tupleSize is undefined when the schema contains Text columns") { // tupleSize is for fixed-only schemas; Text is variable-length, so its // size depends on the actual values. Throw rather than guess.