module; #include #include module cargoxx.linkdb; import std; import cargoxx.util; namespace cargoxx::linkdb { namespace { constexpr const char* SCHEMA_SQL = R"( CREATE TABLE IF NOT EXISTS recipes ( package TEXT NOT NULL, version_range TEXT NOT NULL, nixpkgs_attr TEXT NOT NULL, find_package TEXT NOT NULL, targets TEXT NOT NULL, components TEXT, source TEXT NOT NULL, verified_at INTEGER NOT NULL, PRIMARY KEY (package, version_range, source) ); CREATE TABLE IF NOT EXISTS resolution_failures ( package TEXT NOT NULL, version TEXT NOT NULL, last_attempt INTEGER NOT NULL, error TEXT NOT NULL, PRIMARY KEY (package, version) ); )"; constexpr std::int64_t THIRTY_DAYS_SECONDS = 30LL * 24 * 60 * 60; auto sqlite_error(sqlite3* db, std::string_view ctx) -> util::Error { return util::Error{ util::ErrorCode::LinkdbCorrupt, std::format("{}: {}", ctx, sqlite3_errmsg(db)), "", std::nullopt, std::nullopt, }; } auto column_text(sqlite3_stmt* stmt, int idx) -> std::string { const auto* p = sqlite3_column_text(stmt, idx); if (!p) { return {}; } return std::string{reinterpret_cast(p)}; } } // namespace namespace detail { auto overlay_open(const std::filesystem::path& path) -> util::Result> { std::error_code ec; if (path.has_parent_path()) { std::filesystem::create_directories(path.parent_path(), ec); // ec is non-fatal; sqlite3_open will report a clearer error if it matters } sqlite3* db = nullptr; auto rc = sqlite3_open(path.string().c_str(), &db); if (rc != SQLITE_OK) { std::string msg = std::format("cannot open overlay '{}': {}", path.string(), db ? sqlite3_errmsg(db) : sqlite3_errstr(rc)); if (db) { sqlite3_close(db); } return std::unexpected(util::Error{ util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt, }); } auto state = std::make_unique(db); char* errmsg = nullptr; rc = sqlite3_exec(state->handle(), SCHEMA_SQL, nullptr, nullptr, &errmsg); if (rc != SQLITE_OK) { std::string msg = std::format("cannot create overlay schema: {}", errmsg ? errmsg : "?"); sqlite3_free(errmsg); return std::unexpected(util::Error{ util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt, }); } // Schema migrations. SQLite ADD COLUMN errors when the column // already exists; treat "duplicate column" as success. auto add_column = [&](const char* sql) -> util::Result { char* mig_err = nullptr; if (sqlite3_exec(state->handle(), sql, nullptr, nullptr, &mig_err) != SQLITE_OK) { if (mig_err && std::string_view{mig_err}.find("duplicate column") == std::string_view::npos) { std::string msg = std::format("cannot migrate overlay schema: {}", mig_err ? mig_err : "?"); sqlite3_free(mig_err); return std::unexpected(util::Error{ util::ErrorCode::LinkdbCorrupt, std::move(msg), "", path, std::nullopt, }); } sqlite3_free(mig_err); } return {}; }; if (auto r = add_column( "ALTER TABLE recipes ADD COLUMN pkg_config_module TEXT"); !r) { return std::unexpected(r.error()); } if (auto r = add_column( "ALTER TABLE recipes ADD COLUMN brute_force_libs TEXT"); !r) { return std::unexpected(r.error()); } if (auto r = add_column( "ALTER TABLE recipes ADD COLUMN brute_force_includes TEXT"); !r) { return std::unexpected(r.error()); } return state; } auto overlay_insert_manual(OverlayState& state, const std::string& package, const std::string& version_range, const Recipe& r) -> util::Result { constexpr const char* SQL = "INSERT OR REPLACE INTO recipes " "(package, version_range, nixpkgs_attr, find_package, targets, components, source, " " verified_at, pkg_config_module, brute_force_libs, brute_force_includes) " "VALUES (?, ?, ?, ?, ?, NULL, 'manual', ?, ?, ?, ?)"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare insert")); } auto targets_str = nlohmann::json(r.targets).dump(); auto libs_str = nlohmann::json(r.brute_force_libs).dump(); auto incs_str = nlohmann::json(r.brute_force_includes).dump(); auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, r.nixpkgs_attr.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 6, now); if (r.pkg_config_module) { sqlite3_bind_text(stmt, 7, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT); } else { sqlite3_bind_null(stmt, 7); } sqlite3_bind_text(stmt, 8, libs_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 9, incs_str.c_str(), -1, SQLITE_TRANSIENT); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step insert")); } return {}; } namespace { auto overlay_insert(OverlayState& state, const std::string& package, const std::string& version_range, const Recipe& r, const std::string& source, std::int64_t verified_at) -> util::Result { constexpr const char* SQL = "INSERT OR REPLACE INTO recipes " "(package, version_range, nixpkgs_attr, find_package, targets, components, source, " " verified_at, pkg_config_module, brute_force_libs, brute_force_includes) " "VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare insert")); } auto targets_str = nlohmann::json(r.targets).dump(); auto libs_str = nlohmann::json(r.brute_force_libs).dump(); auto incs_str = nlohmann::json(r.brute_force_includes).dump(); sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, r.nixpkgs_attr.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, r.find_package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, targets_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, source.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 7, verified_at); if (r.pkg_config_module) { sqlite3_bind_text(stmt, 8, r.pkg_config_module->c_str(), -1, SQLITE_TRANSIENT); } else { sqlite3_bind_null(stmt, 8); } sqlite3_bind_text(stmt, 9, libs_str.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 10, incs_str.c_str(), -1, SQLITE_TRANSIENT); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step insert")); } return {}; } } // namespace auto overlay_insert_provisional(OverlayState& state, const std::string& package, const std::string& version_range, const Recipe& r, const std::string& source) -> util::Result { return overlay_insert(state, package, version_range, r, source, /*verified_at=*/0); } auto overlay_confirm_provisional(OverlayState& state, const std::string& package, const std::string& version_range, const std::string& source) -> util::Result { constexpr const char* SQL = "UPDATE recipes SET verified_at = ? " "WHERE package = ? AND version_range = ? AND source = ?"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare update")); } auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); sqlite3_bind_int64(stmt, 1, now); sqlite3_bind_text(stmt, 2, package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, version_range.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 4, source.c_str(), -1, SQLITE_TRANSIENT); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step update")); } return {}; } auto overlay_delete_recipe(OverlayState& state, const std::string& package, const std::string& version_range, const std::string& source) -> util::Result { constexpr const char* SQL = "DELETE FROM recipes WHERE package = ? AND version_range = ? AND source = ?"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare delete")); } sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, version_range.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 3, source.c_str(), -1, SQLITE_TRANSIENT); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step delete")); } return {}; } auto overlay_evict_auto(OverlayState& state, const std::string& package) -> util::Result { // `manual` rows are user-curated via `cargoxx linkdb add`; everything // else (`nix-probe`, `conan`, `vcpkg`, …) was synthesized by the // resolver from the current cargoxx logic and is safe to drop — // the next add will re-discover with the latest scanner. constexpr const char* SQL = "DELETE FROM recipes WHERE package = ? AND source != 'manual'"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare evict")); } sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT); auto rc = sqlite3_step(stmt); sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step evict")); } return {}; } auto overlay_query(OverlayState& state, const std::string& package) -> util::Result> { constexpr const char* SQL = "SELECT version_range, nixpkgs_attr, find_package, targets, source, verified_at, " " pkg_config_module, brute_force_libs, brute_force_includes " "FROM recipes WHERE package = ?"; sqlite3* db = state.handle(); sqlite3_stmt* stmt = nullptr; if (sqlite3_prepare_v2(db, SQL, -1, &stmt, nullptr) != SQLITE_OK) { return std::unexpected(sqlite_error(db, "prepare select")); } sqlite3_bind_text(stmt, 1, package.c_str(), -1, SQLITE_TRANSIENT); std::vector out; int rc; while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { OverlayRow row; row.version_range = column_text(stmt, 0); row.nixpkgs_attr = column_text(stmt, 1); row.find_package = column_text(stmt, 2); auto targets_text = column_text(stmt, 3); try { row.targets = nlohmann::json::parse(targets_text).get>(); } catch (const nlohmann::json::exception&) { sqlite3_finalize(stmt); return std::unexpected(util::Error{ util::ErrorCode::LinkdbCorrupt, std::format("overlay row for '{}' has malformed targets JSON", package), "", std::nullopt, std::nullopt, }); } row.source = column_text(stmt, 4); row.verified_at = sqlite3_column_int64(stmt, 5); if (sqlite3_column_type(stmt, 6) != SQLITE_NULL) { row.pkg_config_module = column_text(stmt, 6); } auto parse_str_array = [&](int col, std::vector& out_arr) { if (sqlite3_column_type(stmt, col) == SQLITE_NULL) { return; } try { auto txt = column_text(stmt, col); if (txt.empty()) { return; } out_arr = nlohmann::json::parse(txt).get>(); } catch (const nlohmann::json::exception&) { // legacy/manual rows may have stored garbage; ignore } }; parse_str_array(7, row.brute_force_libs); parse_str_array(8, row.brute_force_includes); out.push_back(std::move(row)); } sqlite3_finalize(stmt); if (rc != SQLITE_DONE) { return std::unexpected(sqlite_error(db, "step select")); } return out; } auto overlay_is_fresh(const OverlayRow& row, std::int64_t now) -> bool { if (row.source == "manual" || row.source == "curated") { return true; } // verified_at == 0 marks a provisional row — written by // resolver::verify_link before its build runs and either confirmed // (verified_at = now) or deleted afterwards. The verifying build // itself must see this row to surface the candidate recipe to its // codegen step. if (row.verified_at == 0) { return true; } return (now - row.verified_at) < THIRTY_DAYS_SECONDS; } } // namespace detail auto Database::add_manual(const std::string& package, const std::string& version_range, const Recipe& r) -> util::Result { if (!overlay_) { return std::unexpected(util::Error{ util::ErrorCode::Internal, "no overlay database is open", "", std::nullopt, std::nullopt, }); } return detail::overlay_insert_manual(*overlay_, package, version_range, r); } namespace { auto require_overlay(const std::unique_ptr& o) -> util::Result { if (!o) { return std::unexpected(util::Error{ util::ErrorCode::Internal, "no overlay database is open", "", std::nullopt, std::nullopt, }); } return {}; } } // namespace auto Database::insert_provisional(const std::string& package, const std::string& version_range, const Recipe& r, const std::string& source) -> util::Result { if (auto ok = require_overlay(overlay_); !ok) { return std::unexpected(ok.error()); } return detail::overlay_insert_provisional(*overlay_, package, version_range, r, source); } auto Database::confirm_provisional(const std::string& package, const std::string& version_range, const std::string& source) -> util::Result { if (auto ok = require_overlay(overlay_); !ok) { return std::unexpected(ok.error()); } return detail::overlay_confirm_provisional(*overlay_, package, version_range, source); } auto Database::abort_provisional(const std::string& package, const std::string& version_range, const std::string& source) -> util::Result { if (auto ok = require_overlay(overlay_); !ok) { return std::unexpected(ok.error()); } return detail::overlay_delete_recipe(*overlay_, package, version_range, source); } auto Database::evict_auto_recipes(const std::string& package) -> util::Result { if (auto ok = require_overlay(overlay_); !ok) { return std::unexpected(ok.error()); } return detail::overlay_evict_auto(*overlay_, package); } } // namespace cargoxx::linkdb