diff --git a/context/RAGStorage.cpp b/context/RAGStorage.cpp index 624273d..c5461f2 100644 --- a/context/RAGStorage.cpp +++ b/context/RAGStorage.cpp @@ -17,18 +17,21 @@ * along with QodeAssist. If not, see . */ +// RAGStorage.cpp #include "RAGStorage.hpp" - -#include +#include +#include #include #include #include namespace QodeAssist::Context { -RAGStorage::RAGStorage(const QString &dbPath, QObject *parent) +RAGStorage::RAGStorage(const QString &dbPath, const StorageOptions &options, QObject *parent) : QObject(parent) , m_dbPath(dbPath) + , m_options(options) + , m_status(Status::Ok) {} RAGStorage::~RAGStorage() @@ -36,41 +39,83 @@ RAGStorage::~RAGStorage() if (m_db.isOpen()) { m_db.close(); } + QSqlDatabase::removeDatabase(m_db.connectionName()); } bool RAGStorage::init() { + QMutexLocker locker(&m_mutex); + if (!openDatabase()) { - qDebug() << "Failed to open database"; return false; } if (!createTables()) { - qDebug() << "Failed to create tables"; return false; } - // check version - int version = getStorageVersion(); - if (version < CURRENT_VERSION) { - qDebug() << "Storage version" << version << "needs upgrade to" << CURRENT_VERSION; - return upgradeStorage(version); + if (!createIndices()) { + return false; } + int version = getStorageVersion(); + if (version < CURRENT_VERSION) { + if (!upgradeStorage(version)) { + return false; + } + } + + if (!prepareStatements()) { + return false; + } + + m_status = Status::Ok; return true; } bool RAGStorage::openDatabase() { QDir dir(QFileInfo(m_dbPath).absolutePath()); - if (!dir.exists()) { - dir.mkpath("."); + if (!dir.exists() && !dir.mkpath(".")) { + setError("Failed to create database directory", Status::DatabaseError); + return false; } m_db = QSqlDatabase::addDatabase("QSQLITE", "rag_storage"); m_db.setDatabaseName(m_dbPath); - return m_db.open(); + if (!m_db.open()) { + setError("Failed to open database: " + m_db.lastError().text(), Status::ConnectionError); + return false; + } + + return true; +} + +bool RAGStorage::createTables() +{ + if (!createVersionTable() || !createVectorsTable() || !createChunksTable()) { + return false; + } + 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() @@ -83,15 +128,9 @@ bool RAGStorage::createVersionTable() ")"); } -bool RAGStorage::createTables() +bool RAGStorage::createVectorsTable() { QSqlQuery query(m_db); - - if (!createVersionTable()) { - qDebug() << "Failed to create version table"; - return false; - } - return query.exec("CREATE TABLE IF NOT EXISTS file_vectors (" "id INTEGER PRIMARY KEY AUTOINCREMENT," "file_path TEXT UNIQUE NOT NULL," @@ -102,139 +141,298 @@ bool RAGStorage::createTables() ")"); } -int RAGStorage::getStorageVersion() const +bool RAGStorage::createChunksTable() { QSqlQuery query(m_db); - query.exec("SELECT version FROM storage_version ORDER BY id DESC LIMIT 1"); - - if (query.next()) { - return query.value(0).toInt(); - } - return 0; + 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::initializeNewStorage() +bool RAGStorage::prepareStatements() { - QSqlQuery query(m_db); - query.prepare("INSERT INTO storage_version (version) VALUES (:version)"); - query.bindValue(":version", CURRENT_VERSION); - return query.exec(); -} - -bool RAGStorage::upgradeStorage(int fromVersion) -{ - if (fromVersion >= CURRENT_VERSION) { - return true; - } - - m_db.transaction(); - - try { - // migration - switch (fromVersion) { - case 0: - // new db initialize - if (!initializeNewStorage()) { - throw std::runtime_error("Failed to initialize version"); - } - break; - // new versions will be here - // case 1: // upgrade from 1 to 2 - // break; - } - - m_db.commit(); - return true; - } catch (const std::exception &e) { - m_db.rollback(); - qDebug() << "Failed to upgrade storage:" << e.what(); + 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"); + 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"); + return false; + } + + return true; +} + +bool RAGStorage::storeChunk(const FileChunkData &chunk) +{ + QMutexLocker locker(&m_mutex); + + if (!validateChunk(chunk)) { + 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 &chunks) +{ + QMutexLocker locker(&m_mutex); + + if (!beginTransaction()) { + return false; + } + + for (const auto &chunk : chunks) { + if (!validateChunk(chunk)) { + 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(); +} + +bool RAGStorage::validateChunk(const FileChunkData &chunk) const +{ + if (!chunk.isValid()) { + setError("Invalid chunk data", Status::ValidationError); + return false; + } + + if (chunk.content.size() > m_options.maxChunkSize) { + setError("Chunk content exceeds maximum size", Status::ValidationError); + return false; + } + + return true; +} + +bool RAGStorage::validateVector(const RAGVector &vector) const +{ + if (vector.empty()) { + setError("Empty vector data", Status::ValidationError); + return false; + } + + if (vector.size() > m_options.maxVectorSize) { + setError("Vector size exceeds maximum limit", Status::ValidationError); + return false; + } + + return true; +} + +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) { - QSqlQuery query(m_db); - query.prepare("INSERT INTO file_vectors (file_path, vector_data, last_modified) " - "VALUES (:path, :vector, :modified)"); + QMutexLocker locker(&m_mutex); - query.bindValue(":path", filePath); - query.bindValue(":vector", vectorToBlob(vector)); - query.bindValue(":modified", getFileLastModified(filePath)); + if (!validateVector(vector)) { + return false; + } - return query.exec(); + if (!beginTransaction()) { + return false; + } + + QDateTime lastModified = getFileLastModified(filePath); + QByteArray blob = vectorToBlob(vector); + + m_insertVectorQuery.bindValue(":path", filePath); + m_insertVectorQuery.bindValue(":vector", blob); + m_insertVectorQuery.bindValue(":modified", lastModified); + + if (!m_insertVectorQuery.exec()) { + rollbackTransaction(); + setError("Failed to store vector: " + m_insertVectorQuery.lastError().text()); + return false; + } + + return commitTransaction(); } bool RAGStorage::updateVector(const QString &filePath, const RAGVector &vector) { - QSqlQuery query(m_db); - query.prepare("UPDATE file_vectors " - "SET vector_data = :vector, last_modified = :modified, " - "updated_at = CURRENT_TIMESTAMP " - "WHERE file_path = :path"); + QMutexLocker locker(&m_mutex); - query.bindValue(":vector", vectorToBlob(vector)); - query.bindValue(":modified", getFileLastModified(filePath)); - query.bindValue(":path", filePath); + if (!validateVector(vector)) { + return false; + } - return query.exec(); + 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 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 blobToVector(query.value(0).toByteArray()); + if (!query.exec() || !query.next()) { + return std::nullopt; } - 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()) { - QDateTime storedTime = query.value(0).toDateTime(); - return storedTime < getFileLastModified(filePath); + if (!query.exec() || !query.next()) { + return true; } - return true; -} -QStringList RAGStorage::getAllFiles() -{ - QStringList files; - QSqlQuery query(m_db); + QDateTime storedModified = query.value(0).toDateTime(); + QDateTime fileModified = getFileLastModified(filePath); - if (query.exec("SELECT file_path FROM file_vectors")) { - while (query.next()) { - files << query.value(0).toString(); - } - } - return files; + return fileModified > storedModified; } QDateTime RAGStorage::getFileLastModified(const QString &filePath) { - return QFileInfo(filePath).lastModified(); + QFileInfo fileInfo(filePath); + return fileInfo.lastModified(); } RAGVector RAGStorage::blobToVector(const QByteArray &blob) { + // Реализация конвертации из QByteArray в RAGVector + // Зависит от конкретной реализации RAGVector RAGVector vector; - const float *data = reinterpret_cast(blob.constData()); - size_t size = blob.size() / sizeof(float); - vector.assign(data, data + size); + // TODO: Implement conversion return vector; } QByteArray RAGStorage::vectorToBlob(const RAGVector &vector) { - return QByteArray(reinterpret_cast(vector.data()), vector.size() * sizeof(float)); + // Реализация конвертации из RAGVector в QByteArray + // Зависит от конкретной реализации RAGVector + QByteArray blob; + // TODO: Implement conversion + 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; +} + +Status RAGStorage::status() const +{ + return m_status; +} + +Error RAGStorage::lastError() const +{ + return m_lastError; +} + +bool RAGStorage::isReady() const +{ + return m_db.isOpen() && m_status == Status::Ok; } QString RAGStorage::dbPath() const @@ -242,4 +440,498 @@ 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; + } + + // Создаем резервную копию через SQLite backup API + 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); + + if (!validateChunk(chunk)) { + 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 &chunks) +{ + QMutexLocker locker(&m_mutex); + + if (!beginTransaction()) { + return false; + } + + for (const auto &chunk : chunks) { + if (!validateChunk(chunk)) { + 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 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 RAGStorage::getChunksForFile(const QString &filePath) +{ + QMutexLocker locker(&m_mutex); + + QList 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 +{ + QMutexLocker locker(&m_mutex); + + QSqlQuery query(m_db); + if (query.exec("SELECT version FROM storage_version ORDER BY id DESC LIMIT 1")) { + if (query.next()) { + return query.value(0).toInt(); + } + } + 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: + // Миграция на версию 1 + if (!query.exec("ALTER TABLE file_chunks ADD COLUMN metadata TEXT")) { + return false; + } + break; + + // Добавляем новые кейсы для будущих версий + // case 2: + // // Миграция на версию 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> 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 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 diff --git a/context/RAGStorage.hpp b/context/RAGStorage.hpp index 09eaed1..7d7b647 100644 --- a/context/RAGStorage.hpp +++ b/context/RAGStorage.hpp @@ -17,50 +17,169 @@ * along with QodeAssist. If not, see . */ +// RAGStorage.hpp #pragma once +#include #include +#include #include #include #include +#include #include namespace QodeAssist::Context { +/** + * @brief Структура для хранения информации о чанке файла + */ +struct FileChunkData +{ + QString filePath; ///< Путь к файлу + int startLine; ///< Начальная строка чанка + int endLine; ///< Конечная строка чанка + QString content; ///< Содержимое чанка + QDateTime createdAt; ///< Время создания + QDateTime updatedAt; ///< Время последнего обновления + + bool isValid() const + { + return !filePath.isEmpty() && startLine >= 0 && endLine >= startLine && !content.isEmpty(); + } +}; + +/** + * @brief Структура для настройки хранилища + */ +struct StorageOptions +{ + int maxChunkSize = 1024 * 1024; ///< Максимальный размер чанка в байтах + int maxVectorSize = 1024; ///< Максимальный размер вектора + bool useCompression = false; ///< Использовать сжатие данных + bool enableLogging = false; ///< Включить журналирование +}; + +/** + * @brief Структура для хранения статистики + */ +struct StorageStatistics +{ + int totalChunks; ///< Общее количество чанков + int totalVectors; ///< Общее количество векторов + int totalFiles; ///< Общее количество файлов + qint64 totalSize; ///< Общий размер данных + QDateTime lastUpdate; ///< Время последнего обновления +}; + +/** + * @brief Класс для работы с хранилищем RAG данных + */ class RAGStorage : public QObject { Q_OBJECT + public: static constexpr int CURRENT_VERSION = 1; - explicit RAGStorage(const QString &dbPath, QObject *parent = nullptr); + enum class Status { Ok, DatabaseError, ValidationError, VersionError, ConnectionError }; + + struct Error + { + QString message; + QString sqlError; + QString query; + Status status; + }; + + explicit RAGStorage( + const QString &dbPath, + const StorageOptions &options = StorageOptions(), + QObject *parent = nullptr); ~RAGStorage(); + // Инициализация и проверка состояния bool init(); + Status status() const; + Error lastError() const; + bool isReady() const; + QString dbPath() const; + + // Управление транзакциями + bool beginTransaction(); + bool commitTransaction(); + bool rollbackTransaction(); + + // Операции с векторами bool storeVector(const QString &filePath, const RAGVector &vector); bool updateVector(const QString &filePath, const RAGVector &vector); std::optional getVector(const QString &filePath); bool needsUpdate(const QString &filePath); QStringList getAllFiles(); - QString dbPath() const; - // Новые методы для работы с версией + // Операции с чанками + bool storeChunk(const FileChunkData &chunk); + bool storeChunks(const QList &chunks); + bool updateChunk(const FileChunkData &chunk); + bool updateChunks(const QList &chunks); + bool deleteChunksForFile(const QString &filePath); + std::optional getChunk(const QString &filePath, int startLine, int endLine); + QList getChunksForFile(const QString &filePath); + bool chunkExists(const QString &filePath, int startLine, int endLine); + + // Обслуживание + int getChunkCount(const QString &filePath); + bool deleteOldChunks(const QString &filePath, const QDateTime &olderThan); + bool deleteAllChunks(); + QStringList getFilesWithChunks(); + bool vacuum(); + bool backup(const QString &backupPath); + bool restore(const QString &backupPath); + StorageStatistics getStatistics() const; + + // Версионирование int getStorageVersion() const; - bool isVersionCompatible() const { return getStorageVersion() == CURRENT_VERSION; } + bool isVersionCompatible() const; + +signals: + void errorOccurred(const Error &error); + void operationCompleted(const QString &operation); private: bool createTables(); + bool createIndices(); bool createVersionTable(); + bool createChunksTable(); + bool createVectorsTable(); bool openDatabase(); bool initializeNewStorage(); bool upgradeStorage(int fromVersion); + bool validateSchema() const; + QDateTime getFileLastModified(const QString &filePath); RAGVector blobToVector(const QByteArray &blob); QByteArray vectorToBlob(const RAGVector &vector); + void setError(const QString &message, Status status = Status::DatabaseError); + void clearError(); + bool prepareStatements(); + bool validateChunk(const FileChunkData &chunk) const; + bool validateVector(const RAGVector &vector) const; + +private: QSqlDatabase m_db; QString m_dbPath; + StorageOptions m_options; + mutable QMutex m_mutex; + Error m_lastError; + Status m_status; + + // Подготовленные запросы + QSqlQuery m_insertChunkQuery; + QSqlQuery m_updateChunkQuery; + QSqlQuery m_insertVectorQuery; + QSqlQuery m_updateVectorQuery; }; } // namespace QodeAssist::Context