QodeAssist/context/RAGStorage.cpp
2025-02-21 18:19:16 +01:00

1048 lines
28 KiB
C++

/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// RAGStorage.cpp
#include "RAGStorage.hpp"
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QSqlError>
#include <QSqlQuery>
#include <QUuid>
namespace QodeAssist::Context {
RAGStorage::RAGStorage(const QString &dbPath, const StorageOptions &options, QObject *parent)
: QObject(parent)
, m_dbPath(dbPath)
, m_options(options)
, m_status(Status::Ok)
{}
RAGStorage::~RAGStorage()
{
if (m_db.isOpen()) {
m_db.close();
}
QSqlDatabase::removeDatabase(m_db.connectionName());
}
bool RAGStorage::init()
{
QMutexLocker locker(&m_mutex);
qDebug() << "Initializing RAGStorage at path:" << m_dbPath;
if (!openDatabase()) {
qDebug() << "Failed to open database";
return false;
}
qDebug() << "Database opened successfully";
if (!createTables()) {
qDebug() << "Failed to create tables";
return false;
}
qDebug() << "Tables created successfully";
if (!createIndices()) {
qDebug() << "Failed to create indices";
return false;
}
qDebug() << "Indices created successfully";
int version = getStorageVersion();
qDebug() << "Current storage version:" << version;
if (version < CURRENT_VERSION) {
qDebug() << "Upgrading storage from version" << version << "to" << CURRENT_VERSION;
if (!upgradeStorage(version)) {
qDebug() << "Failed to upgrade storage";
return false;
}
qDebug() << "Storage upgraded successfully";
}
if (!prepareStatements()) {
qDebug() << "Failed to prepare statements";
return false;
}
qDebug() << "Statements prepared successfully";
m_status = Status::Ok;
qDebug() << "RAGStorage initialized successfully";
return true;
}
bool RAGStorage::openDatabase()
{
qDebug() << "Opening database at:" << m_dbPath;
QDir dir(QFileInfo(m_dbPath).absolutePath());
if (!dir.exists() && !dir.mkpath(".")) {
setError("Failed to create database directory", Status::DatabaseError);
return false;
}
QString connectionName = QString("rag_storage_%1").arg(QUuid::createUuid().toString());
m_db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
m_db.setDatabaseName(m_dbPath);
if (!m_db.open()) {
setError("Failed to open database: " + m_db.lastError().text(), Status::ConnectionError);
return false;
}
QSqlQuery query(m_db);
if (!query.exec("PRAGMA journal_mode=WAL")) {
qDebug() << "Failed to set journal mode:" << query.lastError().text();
}
if (!query.exec("PRAGMA synchronous=NORMAL")) {
qDebug() << "Failed to set synchronous mode:" << query.lastError().text();
}
qDebug() << "Database opened successfully";
return true;
}
bool RAGStorage::createTables()
{
qDebug() << "Creating tables...";
if (!createVersionTable()) {
qDebug() << "Failed to create version table";
return false;
}
qDebug() << "Version table created";
if (!createVectorsTable()) {
qDebug() << "Failed to create vectors table";
return false;
}
qDebug() << "Vectors table created";
if (!createChunksTable()) {
qDebug() << "Failed to create chunks table";
return false;
}
qDebug() << "Chunks table created";
return true;
}
bool RAGStorage::createIndices()
{
QSqlQuery query(m_db);
QStringList indices
= {"CREATE INDEX IF NOT EXISTS idx_file_chunks_file_path ON file_chunks(file_path)",
"CREATE INDEX IF NOT EXISTS idx_file_vectors_file_path ON file_vectors(file_path)",
"CREATE INDEX IF NOT EXISTS idx_file_chunks_updated_at ON file_chunks(updated_at)"};
for (const QString &sql : indices) {
if (!query.exec(sql)) {
setError("Failed to create index: " + query.lastError().text());
return false;
}
}
return true;
}
bool RAGStorage::createVersionTable()
{
qDebug() << "Creating version table...";
QSqlQuery query(m_db);
bool success = query.exec("CREATE TABLE IF NOT EXISTS storage_version ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"version INTEGER NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")");
if (!success) {
qDebug() << "Failed to create version table:" << query.lastError().text();
return false;
}
query.exec("SELECT COUNT(*) FROM storage_version");
if (query.next() && query.value(0).toInt() == 0) {
qDebug() << "Inserting initial version record";
QSqlQuery insertQuery(m_db);
success = insertQuery.exec(
QString("INSERT INTO storage_version (version) VALUES (%1)").arg(CURRENT_VERSION));
if (!success) {
qDebug() << "Failed to insert initial version:" << insertQuery.lastError().text();
return false;
}
}
qDebug() << "Version table ready";
return true;
}
bool RAGStorage::createVectorsTable()
{
QSqlQuery query(m_db);
return query.exec("CREATE TABLE IF NOT EXISTS file_vectors ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"file_path TEXT UNIQUE NOT NULL,"
"vector_data BLOB NOT NULL,"
"last_modified DATETIME NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP"
")");
}
bool RAGStorage::createChunksTable()
{
QSqlQuery query(m_db);
return query.exec("CREATE TABLE IF NOT EXISTS file_chunks ("
"id INTEGER PRIMARY KEY AUTOINCREMENT,"
"file_path TEXT NOT NULL,"
"start_line INTEGER NOT NULL CHECK (start_line >= 0),"
"end_line INTEGER NOT NULL CHECK (end_line >= start_line),"
"content TEXT NOT NULL,"
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,"
"UNIQUE(file_path, start_line, end_line)"
")");
}
bool RAGStorage::prepareStatements()
{
qDebug() << "Preparing SQL statements...";
m_insertChunkQuery = QSqlQuery(m_db);
if (!m_insertChunkQuery.prepare(
"INSERT INTO file_chunks (file_path, start_line, end_line, content) "
"VALUES (:path, :start, :end, :content)")) {
setError("Failed to prepare insert chunk query");
return false;
}
m_updateChunkQuery = QSqlQuery(m_db);
if (!m_updateChunkQuery.prepare(
"UPDATE file_chunks SET content = :content, updated_at = CURRENT_TIMESTAMP "
"WHERE file_path = :path AND start_line = :start AND end_line = :end")) {
setError("Failed to prepare update chunk query");
return false;
}
m_insertVectorQuery = QSqlQuery(m_db);
if (!m_insertVectorQuery.prepare(
"INSERT INTO file_vectors (file_path, vector_data, last_modified) "
"VALUES (:path, :vector, :modified)")) {
setError("Failed to prepare insert vector query: " + m_insertVectorQuery.lastError().text());
return false;
}
m_updateVectorQuery = QSqlQuery(m_db);
if (!m_updateVectorQuery.prepare(
"UPDATE file_vectors SET vector_data = :vector, last_modified = :modified, "
"updated_at = CURRENT_TIMESTAMP WHERE file_path = :path")) {
setError("Failed to prepare update vector query: " + m_updateVectorQuery.lastError().text());
return false;
}
return true;
}
bool RAGStorage::storeChunk(const FileChunkData &chunk)
{
QMutexLocker locker(&m_mutex);
auto validation = validateChunk(chunk);
if (!validation.isValid) {
setError(validation.errorMessage, validation.errorStatus);
return false;
}
if (!beginTransaction()) {
return false;
}
m_insertChunkQuery.bindValue(":path", chunk.filePath);
m_insertChunkQuery.bindValue(":start", chunk.startLine);
m_insertChunkQuery.bindValue(":end", chunk.endLine);
m_insertChunkQuery.bindValue(":content", chunk.content);
if (!m_insertChunkQuery.exec()) {
rollbackTransaction();
setError("Failed to store chunk: " + m_insertChunkQuery.lastError().text());
return false;
}
return commitTransaction();
}
bool RAGStorage::storeChunks(const QList<FileChunkData> &chunks)
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
for (const auto &chunk : chunks) {
auto validation = validateChunk(chunk);
if (!validation.isValid) {
setError(validation.errorMessage, validation.errorStatus);
rollbackTransaction();
return false;
}
m_insertChunkQuery.bindValue(":path", chunk.filePath);
m_insertChunkQuery.bindValue(":start", chunk.startLine);
m_insertChunkQuery.bindValue(":end", chunk.endLine);
m_insertChunkQuery.bindValue(":content", chunk.content);
if (!m_insertChunkQuery.exec()) {
rollbackTransaction();
setError("Failed to store chunks: " + m_insertChunkQuery.lastError().text());
return false;
}
}
return commitTransaction();
}
RAGStorage::ValidationResult RAGStorage::validateChunk(const FileChunkData &chunk) const
{
if (!chunk.isValid()) {
return {false, "Invalid chunk data", Status::ValidationError};
}
if (chunk.content.size() > m_options.maxChunkSize) {
return {false, "Chunk content exceeds maximum size", Status::ValidationError};
}
return {true, QString(), Status::Ok};
}
RAGStorage::ValidationResult RAGStorage::validateVector(const RAGVector &vector) const
{
if (vector.empty()) {
return {false, "Empty vector data", Status::ValidationError};
}
if (vector.size() > m_options.maxVectorSize) {
return {false, "Vector size exceeds maximum limit", Status::ValidationError};
}
return {true, QString(), Status::Ok};
}
bool RAGStorage::beginTransaction()
{
return m_db.transaction();
}
bool RAGStorage::commitTransaction()
{
return m_db.commit();
}
bool RAGStorage::rollbackTransaction()
{
return m_db.rollback();
}
bool RAGStorage::storeVector(const QString &filePath, const RAGVector &vector)
{
QMutexLocker locker(&m_mutex);
qDebug() << "Storing vector for file:" << filePath;
auto validation = validateVector(vector);
if (!validation.isValid) {
setError(validation.errorMessage, validation.errorStatus);
return false;
}
if (!beginTransaction()) {
return false;
}
QDateTime lastModified = getFileLastModified(filePath);
QByteArray blob = vectorToBlob(vector);
qDebug() << "Vector converted to blob, size:" << blob.size() << "bytes";
m_updateVectorQuery.bindValue(":path", filePath);
m_updateVectorQuery.bindValue(":vector", blob);
m_updateVectorQuery.bindValue(":modified", lastModified);
if (m_updateVectorQuery.exec()) {
if (m_updateVectorQuery.numRowsAffected() > 0) {
qDebug() << "Vector updated successfully";
return commitTransaction();
}
}
m_insertVectorQuery.bindValue(":path", filePath);
m_insertVectorQuery.bindValue(":vector", blob);
m_insertVectorQuery.bindValue(":modified", lastModified);
if (!m_insertVectorQuery.exec()) {
qDebug() << "Failed to store vector:" << m_insertVectorQuery.lastError().text();
rollbackTransaction();
setError("Failed to store vector: " + m_insertVectorQuery.lastError().text());
return false;
}
qDebug() << "Vector stored successfully";
return commitTransaction();
}
bool RAGStorage::updateVector(const QString &filePath, const RAGVector &vector)
{
QMutexLocker locker(&m_mutex);
auto validation = validateVector(vector);
if (!validation.isValid) {
setError(validation.errorMessage, validation.errorStatus);
return false;
}
if (!beginTransaction()) {
return false;
}
QDateTime lastModified = getFileLastModified(filePath);
QByteArray blob = vectorToBlob(vector);
m_updateVectorQuery.bindValue(":path", filePath);
m_updateVectorQuery.bindValue(":vector", blob);
m_updateVectorQuery.bindValue(":modified", lastModified);
if (!m_updateVectorQuery.exec()) {
rollbackTransaction();
setError("Failed to update vector: " + m_updateVectorQuery.lastError().text());
return false;
}
return commitTransaction();
}
std::optional<RAGVector> RAGStorage::getVector(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
query.prepare("SELECT vector_data FROM file_vectors WHERE file_path = :path");
query.bindValue(":path", filePath);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
QByteArray blob = query.value(0).toByteArray();
return blobToVector(blob);
}
bool RAGStorage::needsUpdate(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
query.prepare("SELECT last_modified FROM file_vectors WHERE file_path = :path");
query.bindValue(":path", filePath);
if (!query.exec() || !query.next()) {
return true;
}
QDateTime storedModified = query.value(0).toDateTime();
QDateTime fileModified = getFileLastModified(filePath);
return fileModified > storedModified;
}
QDateTime RAGStorage::getFileLastModified(const QString &filePath)
{
QFileInfo fileInfo(filePath);
return fileInfo.lastModified();
}
RAGVector RAGStorage::blobToVector(const QByteArray &blob)
{
RAGVector vector;
QDataStream stream(blob);
stream.setVersion(QDataStream::Qt_6_0);
stream.setFloatingPointPrecision(QDataStream::DoublePrecision);
qint32 size;
stream >> size;
vector.resize(size);
for (int i = 0; i < size; ++i) {
double value;
stream >> value;
vector[i] = value;
}
qDebug() << "Vector restored from blob, size:" << vector.size();
return vector;
}
QByteArray RAGStorage::vectorToBlob(const RAGVector &vector)
{
QByteArray blob;
QDataStream stream(&blob, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_6_0);
stream.setFloatingPointPrecision(QDataStream::DoublePrecision);
stream << static_cast<qint32>(vector.size());
for (double value : vector) {
stream << value;
}
qDebug() << "Vector converted to blob, vector size:" << vector.size()
<< "blob size:" << blob.size();
return blob;
}
void RAGStorage::setError(const QString &message, Status status)
{
m_lastError = Error{message, m_db.lastError().text(), m_db.lastError().databaseText(), status};
m_status = status;
emit errorOccurred(m_lastError);
}
void RAGStorage::clearError()
{
m_lastError = Error();
m_status = Status::Ok;
}
RAGStorage::Status RAGStorage::status() const
{
return m_status;
}
RAGStorage::Error RAGStorage::lastError() const
{
return m_lastError;
}
bool RAGStorage::isReady() const
{
return m_db.isOpen() && m_status == Status::Ok;
}
QString RAGStorage::dbPath() const
{
return m_dbPath;
}
bool RAGStorage::deleteChunksForFile(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM file_chunks WHERE file_path = :path");
query.bindValue(":path", filePath);
if (!query.exec()) {
rollbackTransaction();
setError("Failed to delete chunks: " + query.lastError().text());
return false;
}
return commitTransaction();
}
bool RAGStorage::vacuum()
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
if (!query.exec("VACUUM")) {
setError("Failed to vacuum database: " + query.lastError().text());
return false;
}
return true;
}
bool RAGStorage::backup(const QString &backupPath)
{
QMutexLocker locker(&m_mutex);
if (!m_db.isOpen()) {
setError("Database is not open");
return false;
}
QFile::copy(m_dbPath, backupPath);
return true;
}
StorageStatistics RAGStorage::getStatistics() const
{
QMutexLocker locker(&m_mutex);
StorageStatistics stats;
QSqlQuery query(m_db);
if (query.exec("SELECT COUNT(*), SUM(LENGTH(content)) FROM file_chunks")) {
if (query.next()) {
stats.totalChunks = query.value(0).toInt();
stats.totalSize = query.value(1).toLongLong();
}
}
if (query.exec("SELECT COUNT(*) FROM file_vectors")) {
if (query.next()) {
stats.totalVectors = query.value(0).toInt();
}
}
if (query.exec("SELECT COUNT(DISTINCT file_path) FROM ("
"SELECT file_path FROM file_chunks "
"UNION "
"SELECT file_path FROM file_vectors)")) {
if (query.next()) {
stats.totalFiles = query.value(0).toInt();
}
}
if (query.exec("SELECT MAX(updated_at) FROM ("
"SELECT updated_at FROM file_chunks "
"UNION "
"SELECT updated_at FROM file_vectors)")) {
if (query.next()) {
stats.lastUpdate = query.value(0).toDateTime();
}
}
return stats;
}
bool RAGStorage::deleteOldChunks(const QString &filePath, const QDateTime &olderThan)
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
QSqlQuery query(m_db);
query.prepare("DELETE FROM file_chunks WHERE file_path = :path AND updated_at < :date");
query.bindValue(":path", filePath);
query.bindValue(":date", olderThan);
if (!query.exec()) {
rollbackTransaction();
setError("Failed to delete old chunks: " + query.lastError().text());
return false;
}
return commitTransaction();
}
bool RAGStorage::deleteAllChunks()
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
QSqlQuery query(m_db);
if (!query.exec("DELETE FROM file_chunks")) {
rollbackTransaction();
setError("Failed to delete all chunks: " + query.lastError().text());
return false;
}
return commitTransaction();
}
QStringList RAGStorage::getFilesWithChunks()
{
QMutexLocker locker(&m_mutex);
QStringList files;
QSqlQuery query(m_db);
if (query.exec("SELECT DISTINCT file_path FROM file_chunks")) {
while (query.next()) {
files.append(query.value(0).toString());
}
}
return files;
}
QStringList RAGStorage::getAllFiles()
{
QMutexLocker locker(&m_mutex);
QStringList files;
QSqlQuery query(m_db);
if (query.exec("SELECT DISTINCT file_path FROM ("
"SELECT file_path FROM file_chunks "
"UNION "
"SELECT file_path FROM file_vectors)")) {
while (query.next()) {
files.append(query.value(0).toString());
}
}
return files;
}
bool RAGStorage::updateChunk(const FileChunkData &chunk)
{
QMutexLocker locker(&m_mutex);
auto validation = validateChunk(chunk);
if (!validation.isValid) {
return false;
}
if (!beginTransaction()) {
return false;
}
m_updateChunkQuery.bindValue(":path", chunk.filePath);
m_updateChunkQuery.bindValue(":start", chunk.startLine);
m_updateChunkQuery.bindValue(":end", chunk.endLine);
m_updateChunkQuery.bindValue(":content", chunk.content);
if (!m_updateChunkQuery.exec()) {
rollbackTransaction();
setError("Failed to update chunk: " + m_updateChunkQuery.lastError().text());
return false;
}
return commitTransaction();
}
bool RAGStorage::updateChunks(const QList<FileChunkData> &chunks)
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
for (const auto &chunk : chunks) {
auto validation = validateChunk(chunk);
if (!validation.isValid) {
rollbackTransaction();
return false;
}
m_updateChunkQuery.bindValue(":path", chunk.filePath);
m_updateChunkQuery.bindValue(":start", chunk.startLine);
m_updateChunkQuery.bindValue(":end", chunk.endLine);
m_updateChunkQuery.bindValue(":content", chunk.content);
if (!m_updateChunkQuery.exec()) {
rollbackTransaction();
setError("Failed to update chunks: " + m_updateChunkQuery.lastError().text());
return false;
}
}
return commitTransaction();
}
std::optional<FileChunkData> RAGStorage::getChunk(const QString &filePath, int startLine, int endLine)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
query.prepare("SELECT file_path, start_line, end_line, content, created_at, updated_at "
"FROM file_chunks "
"WHERE file_path = :path AND start_line = :start AND end_line = :end");
query.bindValue(":path", filePath);
query.bindValue(":start", startLine);
query.bindValue(":end", endLine);
if (!query.exec() || !query.next()) {
return std::nullopt;
}
FileChunkData chunk;
chunk.filePath = query.value(0).toString();
chunk.startLine = query.value(1).toInt();
chunk.endLine = query.value(2).toInt();
chunk.content = query.value(3).toString();
chunk.createdAt = query.value(4).toDateTime();
chunk.updatedAt = query.value(5).toDateTime();
return chunk;
}
QList<FileChunkData> RAGStorage::getChunksForFile(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
QList<FileChunkData> chunks;
QSqlQuery query(m_db);
query.prepare("SELECT file_path, start_line, end_line, content, created_at, updated_at "
"FROM file_chunks "
"WHERE file_path = :path "
"ORDER BY start_line");
query.bindValue(":path", filePath);
if (query.exec()) {
while (query.next()) {
FileChunkData chunk;
chunk.filePath = query.value(0).toString();
chunk.startLine = query.value(1).toInt();
chunk.endLine = query.value(2).toInt();
chunk.content = query.value(3).toString();
chunk.createdAt = query.value(4).toDateTime();
chunk.updatedAt = query.value(5).toDateTime();
chunks.append(chunk);
}
}
return chunks;
}
bool RAGStorage::chunkExists(const QString &filePath, int startLine, int endLine)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
query.prepare("SELECT COUNT(*) FROM file_chunks "
"WHERE file_path = :path AND start_line = :start AND end_line = :end");
query.bindValue(":path", filePath);
query.bindValue(":start", startLine);
query.bindValue(":end", endLine);
if (query.exec() && query.next()) {
return query.value(0).toInt() > 0;
}
return false;
}
int RAGStorage::getStorageVersion() const
{
qDebug() << "Getting storage version...";
QSqlQuery query(m_db);
qDebug() << "Created query object";
if (!query.exec("SELECT version FROM storage_version ORDER BY id DESC LIMIT 1")) {
qDebug() << "Failed to execute version query:" << query.lastError().text();
return 0;
}
qDebug() << "Version query executed";
if (query.next()) {
int version = query.value(0).toInt();
qDebug() << "Current version:" << version;
return version;
}
qDebug() << "No version found, assuming version 0";
return 0;
}
bool RAGStorage::isVersionCompatible() const
{
int version = getStorageVersion();
return version > 0 && version <= CURRENT_VERSION;
}
bool RAGStorage::upgradeStorage(int fromVersion)
{
QMutexLocker locker(&m_mutex);
if (!beginTransaction()) {
return false;
}
for (int version = fromVersion + 1; version <= CURRENT_VERSION; ++version) {
if (!applyMigration(version)) {
rollbackTransaction();
setError(QString("Failed to upgrade to version %1").arg(version));
return false;
}
}
QSqlQuery query(m_db);
query.prepare("INSERT INTO storage_version (version) VALUES (:version)");
query.bindValue(":version", CURRENT_VERSION);
if (!query.exec()) {
rollbackTransaction();
setError("Failed to update storage version");
return false;
}
return commitTransaction();
}
bool RAGStorage::applyMigration(int version)
{
QSqlQuery query(m_db);
switch (version) {
case 1:
if (!query.exec("ALTER TABLE file_chunks ADD COLUMN metadata TEXT")) {
return false;
}
break;
// case 2:
// //
// break;
default:
setError(QString("Unknown version for migration: %1").arg(version));
return false;
}
return true;
}
bool RAGStorage::validateSchema() const
{
QMutexLocker locker(&m_mutex);
QStringList requiredTables = {"storage_version", "file_vectors", "file_chunks"};
QSqlQuery query(m_db);
query.exec("SELECT name FROM sqlite_master WHERE type='table'");
QStringList existingTables;
while (query.next()) {
existingTables << query.value(0).toString();
}
for (const QString &table : requiredTables) {
if (!existingTables.contains(table)) {
return false;
}
}
struct ColumnInfo
{
QString name;
QString type;
bool notNull;
};
QMap<QString, QList<ColumnInfo>> expectedSchema
= {{"file_chunks",
{{"id", "INTEGER", true},
{"file_path", "TEXT", true},
{"start_line", "INTEGER", true},
{"end_line", "INTEGER", true},
{"content", "TEXT", true},
{"created_at", "DATETIME", false},
{"updated_at", "DATETIME", false}}},
{"file_vectors",
{{"id", "INTEGER", true},
{"file_path", "TEXT", true},
{"vector_data", "BLOB", true},
{"last_modified", "DATETIME", true},
{"created_at", "DATETIME", false},
{"updated_at", "DATETIME", false}}}};
for (auto it = expectedSchema.begin(); it != expectedSchema.end(); ++it) {
QString tableName = it.key();
query.exec(QString("PRAGMA table_info(%1)").arg(tableName));
QList<ColumnInfo> actualColumns;
while (query.next()) {
ColumnInfo col;
col.name = query.value(1).toString();
col.type = query.value(2).toString();
col.notNull = query.value(3).toBool();
actualColumns << col;
}
if (actualColumns.size() != it.value().size()) {
return false;
}
for (int i = 0; i < actualColumns.size(); ++i) {
const auto &expected = it.value()[i];
const auto &actual = actualColumns[i];
if (expected.name != actual.name || expected.type != actual.type
|| expected.notNull != actual.notNull) {
return false;
}
}
}
return true;
}
bool RAGStorage::restore(const QString &backupPath)
{
QMutexLocker locker(&m_mutex);
if (m_db.isOpen()) {
m_db.close();
}
if (!QFile::remove(m_dbPath) || !QFile::copy(backupPath, m_dbPath)) {
setError("Failed to restore from backup");
return false;
}
if (!openDatabase()) {
return false;
}
if (!validateSchema()) {
setError("Invalid schema in backup file");
return false;
}
return true;
}
int RAGStorage::getChunkCount(const QString &filePath)
{
QMutexLocker locker(&m_mutex);
QSqlQuery query(m_db);
query.prepare("SELECT COUNT(*) FROM file_chunks WHERE file_path = :path");
query.bindValue(":path", filePath);
if (query.exec() && query.next()) {
return query.value(0).toInt();
}
return 0;
}
} // namespace QodeAssist::Context