yams/tests/unit/metadata/symbol_metadata_schema_test.cpp

666 lines
22 KiB
C++

// Copyright 2025 YAMS Project
// SPDX-License-Identifier: GPL-3.0-or-later
/**
* @file symbol_metadata_schema_test.cpp
* @brief Schema validation tests for symbol metadata (tree-sitter extraction)
*
* Validates that:
* - Migration v16 creates symbol_metadata table correctly
* - EntityGraphService symbol insertion uses correct column names
* - Symbol queries reference valid schema columns
* - Foreign key constraint to documents table is correct
*/
#include <catch2/catch_test_macros.hpp>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/database.h>
#include <yams/metadata/migration.h>
#include <filesystem>
#include <map>
#include <optional>
#include <string>
#include <vector>
using namespace yams;
using namespace yams::metadata;
namespace {
/**
* @brief Helper class to validate schema after migrations
*/
class SymbolSchemaValidator {
public:
SymbolSchemaValidator() {
// Create temporary database in /tmp
temp_db_path_ = std::filesystem::temp_directory_path() / "yams_symbol_schema_test";
std::filesystem::create_directories(temp_db_path_);
db_file_ = temp_db_path_ / "test.db";
// Create connection pool and initialize
pool_ = std::make_unique<ConnectionPool>(db_file_.string());
auto init_result = pool_->initialize();
if (!init_result) {
throw std::runtime_error("Failed to initialize connection pool: " +
init_result.error().message);
}
// Apply migrations
auto migrateResult = pool_->withConnection([](Database& db) -> Result<void> {
MigrationManager mm(db);
auto initResult = mm.initialize();
if (!initResult)
return initResult.error();
mm.registerMigrations(YamsMetadataMigrations::getAllMigrations());
return mm.migrate();
});
if (!migrateResult) {
throw std::runtime_error("Failed to apply migrations");
}
// Cache column information
cacheColumnInfo();
}
~SymbolSchemaValidator() {
pool_.reset();
try {
std::filesystem::remove_all(temp_db_path_);
} catch (const std::filesystem::filesystem_error& e) {
// Ignore cleanup errors
}
}
const std::vector<std::string>& getSymbolColumns() const { return symbol_columns_; }
const std::vector<std::string>& getDocumentsColumns() const { return documents_columns_; }
// Check if a specific table exists
bool tableExists(const std::string& table_name) {
auto result = pool_->withConnection([&](Database& db) -> Result<bool> {
auto stmt_result =
db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?");
if (!stmt_result) {
return stmt_result.error();
}
auto stmt = std::move(stmt_result.value());
auto bind_result = stmt.bind(1, table_name);
if (!bind_result) {
return bind_result.error();
}
auto step_result = stmt.step();
if (!step_result) {
return step_result.error();
}
return step_result.value();
});
return result && result.value();
}
// Get foreign key information
std::vector<std::map<std::string, std::string>> getForeignKeys(const std::string& table_name) {
std::vector<std::map<std::string, std::string>> fks;
pool_->withConnection([&](Database& db) -> Result<void> {
auto stmt_result = db.prepare("PRAGMA foreign_key_list(" + table_name + ")");
if (!stmt_result) {
return stmt_result.error();
}
auto stmt = std::move(stmt_result.value());
while (true) {
auto stepResult = stmt.step();
if (!stepResult.has_value() || !stepResult.value())
break;
std::map<std::string, std::string> fk;
fk["table"] = stmt.getString(2);
fk["from"] = stmt.getString(3);
fk["to"] = stmt.getString(4);
fks.push_back(fk);
}
return Result<void>();
});
return fks;
}
// Get index information
std::vector<std::string> getIndexes(const std::string& table_name) {
std::vector<std::string> indexes;
auto result = pool_->withConnection([&](Database& db) -> Result<void> {
auto stmt_result =
db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=?");
if (!stmt_result) {
return stmt_result.error();
}
auto stmt = std::move(stmt_result.value());
auto bind_result = stmt.bind(1, table_name);
if (!bind_result) {
return bind_result.error();
}
while (true) {
auto step_result = stmt.step();
if (!step_result) {
return step_result.error();
}
if (!step_result.value()) {
break;
}
auto name = stmt.getString(0);
if (!name.empty()) {
indexes.push_back(name);
}
}
return Result<void>();
});
return indexes;
}
private:
void cacheColumnInfo() {
// Get symbol_metadata columns
auto symbol_result = pool_->withConnection([this](Database& db) -> Result<void> {
auto stmt_result = db.prepare("PRAGMA table_info(symbol_metadata)");
if (!stmt_result) {
return stmt_result.error();
}
auto stmt = std::move(stmt_result.value());
while (true) {
auto step_result = stmt.step();
if (!step_result) {
return step_result.error();
}
if (!step_result.value()) {
break;
}
auto name = stmt.getString(1);
if (!name.empty()) {
symbol_columns_.push_back(name);
}
}
return Result<void>();
});
// Get documents columns
auto docs_result = pool_->withConnection([this](Database& db) -> Result<void> {
auto stmt_result = db.prepare("PRAGMA table_info(documents)");
if (!stmt_result) {
return stmt_result.error();
}
auto stmt = std::move(stmt_result.value());
while (true) {
auto step_result = stmt.step();
if (!step_result) {
return step_result.error();
}
if (!step_result.value()) {
break;
}
auto name = stmt.getString(1);
if (!name.empty()) {
documents_columns_.push_back(name);
}
}
return Result<void>();
});
}
std::filesystem::path temp_db_path_;
std::filesystem::path db_file_;
std::unique_ptr<ConnectionPool> pool_;
std::vector<std::string> symbol_columns_;
std::vector<std::string> documents_columns_;
};
bool columnExists(const std::vector<std::string>& columns, const std::string& name) {
return std::find(columns.begin(), columns.end(), name) != columns.end();
}
// Shared validator instance (created once, reused across all tests)
SymbolSchemaValidator& getValidator() {
static SymbolSchemaValidator instance;
return instance;
}
} // namespace
// ============================================================================
// Migration v16 Validation Tests
// ============================================================================
TEST_CASE("Migration v16 creates symbol_metadata table", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
SECTION("symbol_metadata table exists") {
REQUIRE(validator.tableExists("symbol_metadata"));
}
SECTION("documents table exists (FK target)") {
REQUIRE(validator.tableExists("documents"));
}
}
TEST_CASE("symbol_metadata table has all required columns", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
INFO("symbol_metadata columns: " << columns.size());
for (const auto& col : columns) {
INFO(" - " << col);
}
SECTION("has primary key column") {
REQUIRE(columnExists(columns, "symbol_id"));
}
SECTION("has document reference column (FK)") {
REQUIRE(columnExists(columns, "document_hash"));
}
SECTION("has file path column") {
REQUIRE(columnExists(columns, "file_path"));
}
SECTION("has symbol identification columns") {
REQUIRE(columnExists(columns, "symbol_name"));
REQUIRE(columnExists(columns, "qualified_name"));
REQUIRE(columnExists(columns, "kind"));
}
SECTION("has location columns") {
REQUIRE(columnExists(columns, "start_line"));
REQUIRE(columnExists(columns, "end_line"));
REQUIRE(columnExists(columns, "start_offset"));
REQUIRE(columnExists(columns, "end_offset"));
}
SECTION("has metadata columns") {
REQUIRE(columnExists(columns, "return_type"));
REQUIRE(columnExists(columns, "parameters"));
REQUIRE(columnExists(columns, "documentation"));
}
}
TEST_CASE("symbol_metadata has correct foreign key to documents",
"[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto documents_columns = validator.getDocumentsColumns();
SECTION("documents table has sha256_hash column (FK target)") {
REQUIRE(columnExists(documents_columns, "sha256_hash"));
}
SECTION("foreign key constraint exists") {
auto fks = validator.getForeignKeys("symbol_metadata");
INFO("Foreign keys found: " << fks.size());
for (const auto& fk : fks) {
INFO(" FK: " << fk.at("from") << " -> " << fk.at("table") << "." << fk.at("to"));
}
REQUIRE_FALSE(fks.empty());
bool found_documents_fk = false;
for (const auto& fk : fks) {
if (fk.at("table") == "documents" && fk.at("from") == "document_hash" &&
fk.at("to") == "sha256_hash") {
found_documents_fk = true;
break;
}
}
REQUIRE(found_documents_fk);
}
}
TEST_CASE("symbol_metadata has required indexes", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto indexes = validator.getIndexes("symbol_metadata");
INFO("Indexes on symbol_metadata: " << indexes.size());
for (const auto& idx : indexes) {
INFO(" - " << idx);
}
SECTION("has index on symbol_name") {
bool found = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_name") != std::string::npos) {
found = true;
break;
}
}
REQUIRE(found);
}
SECTION("has index on document_hash") {
bool found = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_document") != std::string::npos) {
found = true;
break;
}
}
REQUIRE(found);
}
SECTION("has index on file_path") {
bool found = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_file_path") != std::string::npos) {
found = true;
break;
}
}
REQUIRE(found);
}
SECTION("has index on kind") {
bool found = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_kind") != std::string::npos) {
found = true;
break;
}
}
REQUIRE(found);
}
SECTION("has index on qualified_name") {
bool found = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_qualified") != std::string::npos) {
found = true;
break;
}
}
REQUIRE(found);
}
}
// ============================================================================
// Symbol Insertion Validation Tests
// ============================================================================
TEST_CASE("Symbol insertion field names match schema", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("tree-sitter SymbolInfo fields map to schema columns") {
// Fields from plugins/symbol_extractor_treesitter/symbol_extractor.h
// SymbolInfo.name -> symbol_name
REQUIRE(columnExists(columns, "symbol_name"));
// SymbolInfo.qualified_name -> qualified_name
REQUIRE(columnExists(columns, "qualified_name"));
// SymbolInfo.kind -> kind
REQUIRE(columnExists(columns, "kind"));
// SymbolInfo.file_path -> file_path
REQUIRE(columnExists(columns, "file_path"));
// SymbolInfo.start_line -> start_line
REQUIRE(columnExists(columns, "start_line"));
// SymbolInfo.end_line -> end_line
REQUIRE(columnExists(columns, "end_line"));
// SymbolInfo.start_offset -> start_offset
REQUIRE(columnExists(columns, "start_offset"));
// SymbolInfo.end_offset -> end_offset
REQUIRE(columnExists(columns, "end_offset"));
// SymbolInfo.return_type -> return_type
REQUIRE(columnExists(columns, "return_type"));
// SymbolInfo.parameters -> parameters (JSON array)
REQUIRE(columnExists(columns, "parameters"));
// SymbolInfo.documentation -> documentation
REQUIRE(columnExists(columns, "documentation"));
}
}
TEST_CASE("Symbol kind values are schema-consistent", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("kind column exists for storing symbol types") {
REQUIRE(columnExists(columns, "kind"));
}
SECTION("expected symbol kinds are valid") {
// From tree-sitter extraction (see symbol_extractor.cpp)
std::vector<std::string> expected_kinds = {"function", "class", "struct", "interface",
"enum", "method", "variable"};
// Schema allows TEXT, so all string kinds are valid
// This test documents expected kinds
INFO("Expected symbol kinds:");
for (const auto& kind : expected_kinds) {
INFO(" - " << kind);
}
REQUIRE(columnExists(columns, "kind"));
}
}
// ============================================================================
// Symbol Query Validation Tests
// ============================================================================
TEST_CASE("Symbol queries use valid column names", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("query by symbol name uses correct column") {
// SELECT * FROM symbol_metadata WHERE symbol_name = ?
REQUIRE(columnExists(columns, "symbol_name"));
}
SECTION("query by qualified name uses correct column") {
// SELECT * FROM symbol_metadata WHERE qualified_name = ?
REQUIRE(columnExists(columns, "qualified_name"));
}
SECTION("query by document uses FK column") {
// SELECT * FROM symbol_metadata WHERE document_hash = ?
REQUIRE(columnExists(columns, "document_hash"));
}
SECTION("query by file path uses correct column") {
// SELECT * FROM symbol_metadata WHERE file_path = ?
REQUIRE(columnExists(columns, "file_path"));
}
SECTION("query by kind uses correct column") {
// SELECT * FROM symbol_metadata WHERE kind = ?
REQUIRE(columnExists(columns, "kind"));
}
}
TEST_CASE("Symbol location queries use valid columns", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("line-based queries use line columns") {
REQUIRE(columnExists(columns, "start_line"));
REQUIRE(columnExists(columns, "end_line"));
}
SECTION("byte-offset queries use offset columns") {
REQUIRE(columnExists(columns, "start_offset"));
REQUIRE(columnExists(columns, "end_offset"));
}
}
// ============================================================================
// Integration with Documents Table
// ============================================================================
TEST_CASE("Symbol metadata integrates with documents table", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto symbol_columns = validator.getSymbolColumns();
auto doc_columns = validator.getDocumentsColumns();
SECTION("FK column exists in symbol_metadata") {
REQUIRE(columnExists(symbol_columns, "document_hash"));
}
SECTION("FK target column exists in documents") {
REQUIRE(columnExists(doc_columns, "sha256_hash"));
}
SECTION("FK constraint is properly defined") {
auto fks = validator.getForeignKeys("symbol_metadata");
bool valid_fk = false;
for (const auto& fk : fks) {
if (fk.at("table") == "documents" && fk.at("from") == "document_hash" &&
fk.at("to") == "sha256_hash") {
valid_fk = true;
break;
}
}
REQUIRE(valid_fk);
}
}
TEST_CASE("Symbol metadata schema prevents common errors", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("prevents 'name' vs 'symbol_name' confusion") {
REQUIRE(columnExists(columns, "symbol_name"));
REQUIRE_FALSE(columnExists(columns, "name"));
}
SECTION("prevents 'doc_hash' vs 'document_hash' confusion") {
REQUIRE(columnExists(columns, "document_hash"));
REQUIRE_FALSE(columnExists(columns, "doc_hash"));
REQUIRE_FALSE(columnExists(columns, "hash"));
}
SECTION("prevents 'type' vs 'kind' confusion") {
REQUIRE(columnExists(columns, "kind"));
REQUIRE_FALSE(columnExists(columns, "type"));
REQUIRE_FALSE(columnExists(columns, "symbol_type"));
}
}
// ============================================================================
// EntityGraphService Integration
// ============================================================================
TEST_CASE("EntityGraphService symbol storage uses correct fields",
"[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("EntityGraphService stores symbols with schema-consistent fields") {
// When EntityGraphService creates symbol nodes from tree-sitter results,
// it should use these field names
INFO("Validating EntityGraphService would use correct INSERT statement");
// Conceptual INSERT (from EntityGraphService::createSymbolNodes):
// INSERT INTO symbol_metadata (
// document_hash, file_path, symbol_name, qualified_name, kind,
// start_line, end_line, start_offset, end_offset,
// return_type, parameters, documentation
// ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
REQUIRE(columnExists(columns, "document_hash"));
REQUIRE(columnExists(columns, "file_path"));
REQUIRE(columnExists(columns, "symbol_name"));
REQUIRE(columnExists(columns, "qualified_name"));
REQUIRE(columnExists(columns, "kind"));
REQUIRE(columnExists(columns, "start_line"));
REQUIRE(columnExists(columns, "end_line"));
REQUIRE(columnExists(columns, "start_offset"));
REQUIRE(columnExists(columns, "end_offset"));
REQUIRE(columnExists(columns, "return_type"));
REQUIRE(columnExists(columns, "parameters"));
REQUIRE(columnExists(columns, "documentation"));
}
}
TEST_CASE("Symbol metadata supports Knowledge Graph queries", "[catch2][unit][metadata][symbol]") {
auto& validator = getValidator();
auto columns = validator.getSymbolColumns();
SECTION("supports finding symbols by name") {
REQUIRE(columnExists(columns, "symbol_name"));
// Indexed for performance
auto indexes = validator.getIndexes("symbol_metadata");
bool has_name_index = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_name") != std::string::npos) {
has_name_index = true;
break;
}
}
REQUIRE(has_name_index);
}
SECTION("supports finding symbols in document") {
REQUIRE(columnExists(columns, "document_hash"));
// Indexed for performance
auto indexes = validator.getIndexes("symbol_metadata");
bool has_doc_index = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_document") != std::string::npos) {
has_doc_index = true;
break;
}
}
REQUIRE(has_doc_index);
}
SECTION("supports finding symbols by type") {
REQUIRE(columnExists(columns, "kind"));
// Indexed for performance (e.g., find all functions)
auto indexes = validator.getIndexes("symbol_metadata");
bool has_kind_index = false;
for (const auto& idx : indexes) {
if (idx.find("idx_symbol_kind") != std::string::npos) {
has_kind_index = true;
break;
}
}
REQUIRE(has_kind_index);
}
}