Обновление схемы БД

This commit is contained in:
Alexander Zhirov 2025-09-10 00:13:02 +03:00
parent dc0c8349c7
commit 18bcf3742d
Signed by: alexander
GPG key ID: C8D8BE544A27C511
6 changed files with 180 additions and 41 deletions

View file

@ -3,6 +3,10 @@ module cdcdb.cdc.cas;
import cdcdb.db; import cdcdb.db;
import cdcdb.cdc.core; import cdcdb.cdc.core;
import std.digest.sha : SHA256, digest;
import std.format : format;
// CAS-хранилище (Content-Addressable Storage) со снапшотами
final class CAS final class CAS
{ {
private: private:
@ -13,8 +17,13 @@ public:
_db = new DBLite(database); _db = new DBLite(database);
} }
size_t saveSnapshot(const(ubyte)[] data) size_t saveSnapshot(string filePath, const(ubyte)[] data)
{ {
ubyte[32] hashSource = digest!SHA256(data);
// Сделать запрос в БД по filePath и сверить хеш файлов
// Параметры для CDC вынести в отдельные настройки (продумать) // Параметры для CDC вынести в отдельные настройки (продумать)
auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F); auto cdc = new CDC(100, 200, 500, 0xFF, 0x0F);
// Разбить на фрагменты // Разбить на фрагменты
@ -26,7 +35,7 @@ public:
// Записать фрагменты в БД // Записать фрагменты в БД
foreach (chunk; chunks) foreach (chunk; chunks)
{ {
writeln(chunk.index); writeln(format("%(%02x%)", chunk.sha256));
} }
_db.commit(); _db.commit();
// Записать манифест в БД // Записать манифест в БД

View file

@ -4,6 +4,7 @@ import cdcdb.cdc.types;
import std.digest.sha : SHA256, digest; import std.digest.sha : SHA256, digest;
// Change Data Capture
final class CDC final class CDC
{ {
private: private:

View file

@ -1,6 +1,6 @@
module cdcdb.cdc.types; module cdcdb.cdc.types;
/// Единица разбиения // Единица разбиения
struct Chunk struct Chunk
{ {
size_t index; // 1..N size_t index; // 1..N
@ -9,7 +9,7 @@ struct Chunk
ubyte[32] sha256; // hex(SHA-256) содержимого ubyte[32] sha256; // hex(SHA-256) содержимого
} }
/// Метаданные снимка // Метаданные снимка
struct SnapshotInfo struct SnapshotInfo
{ {
size_t id; size_t id;

View file

@ -9,6 +9,11 @@ private:
string _dbPath; string _dbPath;
// _scheme // _scheme
mixin(import("scheme.d")); mixin(import("scheme.d"));
SqliteResult sql(T...)(string queryText, T args)
{
return cast(SqliteResult) query(queryText, args);
}
public: public:
this(string database) this(string database)
{ {
@ -40,8 +45,5 @@ public:
query("ROLLBACK"); query("ROLLBACK");
} }
SqliteResult sql(T...)(string queryText, T args) // findFile()
{
return cast(SqliteResult) query(queryText, args);
}
} }

View file

@ -1,75 +1,202 @@
auto _scheme = [ auto _scheme = [
q{ q{
-- Метаданные снапшота -- ------------------------------------------------------------
-- Метаданные снимка
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS snapshots ( CREATE TABLE IF NOT EXISTS snapshots (
-- Уникальный числовой идентификатор снимка. Используется во внешних ключах. -- Уникальный числовой идентификатор снимка. Используется во внешних ключах.
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
-- Произвольная метка/название снимка.
-- Путь к исходному файлу, для удобства навигации/поиска.
file_path TEXT,
-- Произвольная метка/название снимка (для человека).
label TEXT, label TEXT,
-- Время создания записи в UTC. По умолчанию - сейчас.
-- Время создания записи (UTC). По умолчанию - текущее.
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- Полная длина исходного файла в байтах для этого снимка (до разбиения на чанки).
-- Полная длина исходного файла в байтах (до разбиения на чанки).
source_length INTEGER NOT NULL, source_length INTEGER NOT NULL,
-- Пороговые размеры FastCDC (минимальный/целевой/максимальный размер чанка) в байтах.
-- Фиксируются здесь, чтобы позже можно было корректно пересобрать/сравнить. -- Пороговые размеры FastCDC: минимальный/целевой/максимальный размер чанка (в байтах).
-- Фиксируются для воспроизводимости и сравнения результатов.
algo_min INTEGER NOT NULL, algo_min INTEGER NOT NULL,
algo_normal INTEGER NOT NULL, algo_normal INTEGER NOT NULL,
algo_max INTEGER NOT NULL, algo_max INTEGER NOT NULL,
-- Маски для определения границ чанков (быстрый роллинг-хэш/FastCDC).
-- Обычно степени вида 2^n - 1. Хранятся для воспроизводимости. -- Маски FastCDC для определения границ чанков (обычно вида 2^n - 1).
-- Хранятся для воспроизводимости.
mask_s INTEGER NOT NULL, mask_s INTEGER NOT NULL,
mask_l INTEGER NOT NULL, mask_l INTEGER NOT NULL,
-- Состояние снимка: -- Состояние снимка:
-- pending - метаданные созданы, состав не полностью загружен; -- "pending" - метаданные созданы, состав не полностью загружен;
-- ready - все чанки привязаны, снимок готов к использованию. -- "ready" - все чанки привязаны, снимок готов к использованию.
status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready")) status TEXT NOT NULL DEFAULT "pending" CHECK (status IN ("pending","ready"))
) )
}, },
q{ q{
-- Уникальные куски содержимого (сам контент в БД) -- ------------------------------------------------------------
-- Уникальные куски содержимого (дедупликация по sha256)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS blobs ( CREATE TABLE IF NOT EXISTS blobs (
-- Хэш содержимого чанка. Ключ обеспечивает дедупликацию: одинаковый контент хранится один раз. -- Хэш содержимого чанка. Обеспечивает уникальность контента.
sha256 TEXT PRIMARY KEY, -- Храним как BLOB(32) (сырые 32 байта SHA-256), а не hex-строку.
-- Размер этого чанка в байтах. sha256 BLOB PRIMARY KEY CHECK (length(sha256) = 32),
-- Размер чанка в байтах. Должен совпадать с длиной content.
size INTEGER NOT NULL, size INTEGER NOT NULL,
-- Сырые байты чанка. -- Сырые байты чанка.
content BLOB NOT NULL, content BLOB NOT NULL,
-- Когда этот контент впервые появился в базе (UTC).
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP) -- Момент, когда этот контент впервые появился в базе (UTC).
created_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- Последний раз, когда на контент сослались (для аналитики/GC).
last_seen_utc TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
-- Счётчик ссылок: сколько строк в snapshot_chunks ссылаются на этот sha256.
-- Используется для безопасного удаления неиспользуемых blob-ов.
refcount INTEGER NOT NULL DEFAULT 0,
-- Дополнительные гарантии целостности:
CHECK (refcount >= 0),
CHECK (size = length(content))
) )
}, },
q{ q{
-- Состав снапшота (порядок чанков важен) -- ------------------------------------------------------------
-- Состав снимка (упорядоченный список чанков)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS snapshot_chunks ( CREATE TABLE IF NOT EXISTS snapshot_chunks (
-- Ссылка на snapshots.id. Определяет, к какому снимку относится строка. -- Ссылка на snapshots.id. Определяет, к какому снимку относится строка.
snapshot_id INTEGER NOT NULL, snapshot_id INTEGER NOT NULL,
-- Позиция чанка в снимке (индексация).
-- Позиция чанка в снимке (индексация на твой выбор - 0/1-based; важно быть последовательным).
-- Обеспечивает порядок сборки. -- Обеспечивает порядок сборки.
chunk_index INTEGER NOT NULL, chunk_index INTEGER NOT NULL,
-- Смещение чанка в исходном файле в байтах.
-- Можно восстановить как сумму size предыдущих чанков по chunk_index, -- Смещение чанка в исходном файле (в байтах).
-- но хранение ускоряет проверки/отладку. -- Можно восстановить суммой size предыдущих чанков, но хранение ускоряет проверки/отладку.
offset INTEGER, offset INTEGER,
-- Размер именно этого чанка в байтах (дублирует blobs.size для быстрого доступа и валидации).
-- Размер конкретного чанка в составе (дублирует blobs.size для ускорения/валидации).
size INTEGER NOT NULL, size INTEGER NOT NULL,
-- Ссылка на blobs.sha256. Привязывает позицию в снимке к конкретному содержимому.
sha256 TEXT NOT NULL, -- Ссылка на blobs.sha256. Привязывает позицию к конкретному содержимому.
-- Гарантирует уникальность позиции чанка в рамках снимка и задаёт естественный порядок. -- Тип BLOB обязан совпадать с типом в родительской таблице.
sha256 BLOB NOT NULL,
-- Уникальность позиции чанка в рамках одного снимка.
PRIMARY KEY (snapshot_id, chunk_index), PRIMARY KEY (snapshot_id, chunk_index),
-- Внешние ключи и их поведение:
-- При удалении снимка его строки состава удаляются автоматически. -- При удалении снимка его строки состава удаляются автоматически.
-- Обновления id каскадятся (на практике id не меняют). FOREIGN KEY (snapshot_id)
FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON UPDATE CASCADE ON DELETE CASCADE, REFERENCES snapshots(id)
-- Нельзя удалить blob, если он где-то используется (RESTRICT). ON UPDATE CASCADE
-- Обновление хэша каскадится (редкий случай). ON DELETE CASCADE,
FOREIGN KEY (sha256) REFERENCES blobs(sha256) ON UPDATE CASCADE ON DELETE RESTRICT
-- Нельзя удалить blob, если он где-то используется; обновление ключа запрещено.
FOREIGN KEY (sha256)
REFERENCES blobs(sha256)
ON UPDATE RESTRICT
ON DELETE RESTRICT
) )
}, },
q{ q{
-- Быстрый выбор всех чанков конкретного снимка (частый запрос). -- Быстрый выбор всех чанков конкретного снимка (частый запрос).
CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot ON snapshot_chunks(snapshot_id) CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_snapshot
ON snapshot_chunks(snapshot_id)
}, },
q{ q{
-- Быстрый обратный поиск: где используется данный blob (для GC/аналитики). -- Быстрый обратный поиск: где используется данный blob (для GC/аналитики).
CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_sha ON snapshot_chunks(sha256) -- Индекс по BLOB(32) хорошо работает для точного поиска sha256.
CREATE INDEX IF NOT EXISTS idx_snapshot_chunks_sha
ON snapshot_chunks(sha256)
},
// -- ------------------------------------------------------------
// -- Триггеры для управления refcount и GC blob'ов
// -- ------------------------------------------------------------
q{
-- Инкремент счётчика при привязке чанка к снимку.
-- Обновляем last_seen_utc, чтобы фиксировать "живость" контента.
-- ВАЖНО: FK гарантирует, что соответствующий blobs.sha256 уже существует.
CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_ai
AFTER INSERT ON snapshot_chunks
BEGIN
UPDATE blobs
SET refcount = refcount + 1,
last_seen_utc = CURRENT_TIMESTAMP
WHERE sha256 = NEW.sha256;
END
},
q{
-- Декремент счётчика при удалении строки состава.
-- Если счётчик стал 0 - удаляем неиспользуемый blob (авто-GC).
CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_ad
AFTER DELETE ON snapshot_chunks
BEGIN
UPDATE blobs
SET refcount = refcount - 1
WHERE sha256 = OLD.sha256;
DELETE FROM blobs
WHERE sha256 = OLD.sha256
AND refcount <= 0;
END
},
q{
-- Корректировка счётчиков при смене sha256 в составе.
-- Триггер срабатывает только при реальном изменении значения.
-- Предполагается, что NEW.sha256 существует в blobs (иначе FK не даст обновить).
CREATE TRIGGER IF NOT EXISTS trg_snapshot_chunks_au
AFTER UPDATE OF sha256 ON snapshot_chunks
FOR EACH ROW
WHEN NEW.sha256 <> OLD.sha256
BEGIN
UPDATE blobs
SET refcount = refcount - 1
WHERE sha256 = OLD.sha256;
DELETE FROM blobs
WHERE sha256 = OLD.sha256
AND refcount <= 0;
UPDATE blobs
SET refcount = refcount + 1,
last_seen_utc = CURRENT_TIMESTAMP
WHERE sha256 = NEW.sha256;
END
},
q{
-- Автоматическая смена статуса снимка на "ready",
-- когда сумма размеров его чанков стала равна source_length.
-- Примечание: простая эвристика; если потом удалишь/поменяешь чанки,
-- триггер ниже вернёт статус обратно на "pending".
CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_ready
AFTER INSERT ON snapshot_chunks
BEGIN
UPDATE snapshots
SET status = "ready"
WHERE id = NEW.snapshot_id
AND (SELECT COALESCE(SUM(size),0)
FROM snapshot_chunks
WHERE snapshot_id = NEW.snapshot_id)
= (SELECT source_length FROM snapshots WHERE id = NEW.snapshot_id);
END
},
q{
-- При удалении любого чанка снимок снова помечается как "pending".
-- Это простой безопасный фоллбэк; следующая вставка приравняет суммы и вернёт "ready".
CREATE TRIGGER IF NOT EXISTS trg_snapshots_mark_pending
AFTER DELETE ON snapshot_chunks
BEGIN
UPDATE snapshots
SET status = "pending"
WHERE id = OLD.snapshot_id;
END
} }
]; ];

View file

@ -7,5 +7,5 @@ import std.file : read;
void main() void main()
{ {
auto cas = new CAS("/tmp/base.db"); auto cas = new CAS("/tmp/base.db");
cas.saveSnapshot(cast(ubyte[]) read("/tmp/text")); cas.saveSnapshot("/tmp/text", cast(ubyte[]) read("/tmp/text"));
} }