Skip to content

Commit 73082a6

Browse files
committed
feat(db): sync tags from NotebookConfig to MetadataStore on open
- Add SyncTagsToStore() to sync tag definitions from config to DB - Extract NotebookDb class for notebook metadata operations - Add notebook_metadata table for key-value storage - Reuse tag_depth map for orphan detection (avoid redundant set) - Add test_tag_sync for tag synchronization verification
1 parent a8b0133 commit 73082a6

15 files changed

+747
-11
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ compile_commands.json
4848
test_notebook
4949

5050
.cache
51+
52+
.sisyphus

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ set(VXCORE_SOURCES
2727
db/db_manager.cpp
2828
db/file_db.cpp
2929
db/tag_db.cpp
30+
db/notebook_db.cpp
3031
db/sync_manager.cpp
3132
db/sqlite_metadata_store.cpp
3233
search/search_manager.cpp

src/core/bundled_notebook.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ VxCoreError BundledNotebook::InitOnCreation() {
6464
return err;
6565
}
6666

67+
// Sync tags from NotebookConfig to MetadataStore
68+
err = SyncTagsToMetadataStore();
69+
if (err != VXCORE_OK) {
70+
VXCORE_LOG_WARN("Tag sync failed on creation: root=%s, error=%d", root_folder_.c_str(), err);
71+
// Continue anyway - tags will be synced on next open
72+
}
73+
6774
err = folder_manager_->InitOnCreation();
6875
if (err != VXCORE_OK) {
6976
VXCORE_LOG_ERROR("Failed to initialize bundled notebook folder manager: root=%s, error=%d",
@@ -104,7 +111,14 @@ VxCoreError BundledNotebook::Open(const std::string &local_data_folder,
104111
return err;
105112
}
106113

107-
// Note: We do NOT sync MetadataStore from config files here.
114+
// Sync tags from NotebookConfig to MetadataStore if needed
115+
err = notebook->SyncTagsToMetadataStore();
116+
if (err != VXCORE_OK) {
117+
VXCORE_LOG_WARN("Tag sync failed on open: root=%s, error=%d", root_folder.c_str(), err);
118+
// Continue anyway - tags will be synced on next open or RebuildCache
119+
}
120+
121+
// Note: We do NOT sync folder/file MetadataStore from config files here.
108122
// The cache uses lazy sync - data is loaded on demand when accessed.
109123
// Users can call RebuildCache() if they need a full refresh.
110124

src/core/metadata_store.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ class MetadataStore {
217217
virtual void IterateAllFiles(
218218
std::function<bool(const std::string&, const StoreFileRecord&)> callback) = 0;
219219

220+
// --- Notebook Metadata ---
221+
// Key-value store for notebook-level metadata (e.g., tags_synced_utc)
222+
virtual std::optional<std::string> GetNotebookMetadata(const std::string& key) = 0;
223+
virtual bool SetNotebookMetadata(const std::string& key, const std::string& value) = 0;
224+
220225
// --- Error Handling ---
221226

222227
// Returns the last error message

src/core/notebook.cpp

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <vxcore/vxcore_types.h>
44

55
#include <algorithm>
6+
#include <unordered_map>
67

78
#include "db/sqlite_metadata_store.h"
89
#include "folder_manager.h"
@@ -45,7 +46,8 @@ nlohmann::json TagNode::ToJson() const {
4546
NotebookConfig::NotebookConfig()
4647
: assets_folder("vx_assets"),
4748
attachments_folder("vx_attachments"),
48-
metadata(nlohmann::json::object()) {}
49+
metadata(nlohmann::json::object()),
50+
tags_modified_utc(0) {}
4951

5052
NotebookConfig NotebookConfig::FromJson(const nlohmann::json &json) {
5153
NotebookConfig config;
@@ -72,6 +74,9 @@ NotebookConfig NotebookConfig::FromJson(const nlohmann::json &json) {
7274
config.tags.push_back(TagNode::FromJson(tag_json));
7375
}
7476
}
77+
if (json.contains("tagsModifiedUtc") && json["tagsModifiedUtc"].is_number_integer()) {
78+
config.tags_modified_utc = json["tagsModifiedUtc"].get<int64_t>();
79+
}
7580
return config;
7681
}
7782

@@ -88,6 +93,7 @@ nlohmann::json NotebookConfig::ToJson() const {
8893
tags_array.push_back(tag.ToJson());
8994
}
9095
json["tags"] = std::move(tags_array);
96+
json["tagsModifiedUtc"] = tags_modified_utc;
9197
return json;
9298
}
9399

@@ -155,6 +161,136 @@ VxCoreError Notebook::InitMetadataStore() {
155161
return VXCORE_OK;
156162
}
157163

164+
VxCoreError Notebook::SyncTagsToMetadataStore() {
165+
if (!metadata_store_ || !metadata_store_->IsOpen()) {
166+
return VXCORE_ERR_INVALID_STATE;
167+
}
168+
169+
// 1. Get tags_synced_utc from DB
170+
int64_t tags_synced_utc = 0;
171+
auto synced_str = metadata_store_->GetNotebookMetadata("tags_synced_utc");
172+
if (synced_str.has_value()) {
173+
try {
174+
tags_synced_utc = std::stoll(synced_str.value());
175+
} catch (...) {
176+
tags_synced_utc = 0;
177+
}
178+
}
179+
180+
// 2. Check if sync needed
181+
if (config_.tags_modified_utc > 0 && config_.tags_modified_utc <= tags_synced_utc) {
182+
VXCORE_LOG_DEBUG("Tags already synced: config=%lld, db=%lld", config_.tags_modified_utc,
183+
tags_synced_utc);
184+
return VXCORE_OK; // No sync needed
185+
}
186+
187+
VXCORE_LOG_INFO("Syncing tags to MetadataStore: config=%lld, db=%lld", config_.tags_modified_utc,
188+
tags_synced_utc);
189+
190+
// 3. Begin transaction
191+
if (!metadata_store_->BeginTransaction()) {
192+
VXCORE_LOG_ERROR("Failed to begin transaction for tag sync");
193+
return VXCORE_ERR_UNKNOWN;
194+
}
195+
196+
bool success = true;
197+
198+
// 4. Sort tags by depth (root tags first, then children)
199+
// Build a map of tag name -> depth
200+
std::unordered_map<std::string, int> tag_depth;
201+
for (const auto &tag : config_.tags) {
202+
if (tag.parent.empty()) {
203+
tag_depth[tag.name] = 0;
204+
}
205+
}
206+
207+
// Iteratively compute depths for tags with parents
208+
bool changed = true;
209+
while (changed) {
210+
changed = false;
211+
for (const auto &tag : config_.tags) {
212+
if (tag_depth.find(tag.name) != tag_depth.end()) {
213+
continue;
214+
}
215+
if (!tag.parent.empty() && tag_depth.find(tag.parent) != tag_depth.end()) {
216+
tag_depth[tag.name] = tag_depth[tag.parent] + 1;
217+
changed = true;
218+
}
219+
}
220+
}
221+
222+
// Sort tags by depth
223+
std::vector<const TagNode *> sorted_tags;
224+
sorted_tags.reserve(config_.tags.size());
225+
for (const auto &tag : config_.tags) {
226+
sorted_tags.push_back(&tag);
227+
}
228+
std::sort(sorted_tags.begin(), sorted_tags.end(),
229+
[&tag_depth](const TagNode *a, const TagNode *b) {
230+
int da = tag_depth.count(a->name) ? tag_depth[a->name] : 999;
231+
int db = tag_depth.count(b->name) ? tag_depth[b->name] : 999;
232+
return da < db;
233+
});
234+
235+
// 5. Create/update tags in depth order
236+
for (const TagNode *tag : sorted_tags) {
237+
StoreTagRecord store_tag;
238+
store_tag.name = tag->name;
239+
store_tag.parent_name = tag->parent;
240+
store_tag.metadata = tag->metadata.dump();
241+
242+
if (!metadata_store_->CreateOrUpdateTag(store_tag)) {
243+
VXCORE_LOG_ERROR("Failed to sync tag: %s", tag->name.c_str());
244+
success = false;
245+
break;
246+
}
247+
}
248+
249+
// 6. Delete orphan tags (tags in DB but not in config)
250+
if (success) {
251+
auto db_tags = metadata_store_->ListTags();
252+
for (const auto &db_tag : db_tags) {
253+
if (tag_depth.find(db_tag.name) == tag_depth.end()) {
254+
VXCORE_LOG_INFO("Deleting orphan tag: %s", db_tag.name.c_str());
255+
if (!metadata_store_->DeleteTag(db_tag.name)) {
256+
VXCORE_LOG_WARN("Failed to delete orphan tag: %s", db_tag.name.c_str());
257+
// Continue anyway - orphan cleanup is best-effort
258+
}
259+
}
260+
}
261+
}
262+
263+
// 7. Commit or rollback
264+
if (success) {
265+
if (!metadata_store_->CommitTransaction()) {
266+
VXCORE_LOG_ERROR("Failed to commit tag sync transaction");
267+
return VXCORE_ERR_UNKNOWN;
268+
}
269+
} else {
270+
metadata_store_->RollbackTransaction();
271+
return VXCORE_ERR_UNKNOWN;
272+
}
273+
274+
// 8. Update tags_synced_utc in DB
275+
int64_t sync_time =
276+
config_.tags_modified_utc > 0 ? config_.tags_modified_utc : GetCurrentTimestampMillis();
277+
metadata_store_->SetNotebookMetadata("tags_synced_utc", std::to_string(sync_time));
278+
279+
// 9. If config had tags_modified_utc=0, set it to current time and save
280+
if (config_.tags_modified_utc == 0 && !config_.tags.empty()) {
281+
config_.tags_modified_utc = sync_time;
282+
// Note: UpdateConfig is virtual, will save to appropriate location
283+
auto err = UpdateConfig(config_);
284+
if (err != VXCORE_OK) {
285+
VXCORE_LOG_WARN("Failed to update config after tag sync: error=%d", err);
286+
// Don't fail - sync itself succeeded
287+
}
288+
}
289+
290+
VXCORE_LOG_INFO("Tag sync completed successfully");
291+
return VXCORE_OK;
292+
}
293+
158294
void Notebook::Close() {
159295
VXCORE_LOG_INFO("Closing notebook: id=%s", config_.id.c_str());
160296

@@ -191,6 +327,7 @@ VxCoreError Notebook::CreateTag(const std::string &tag_name, const std::string &
191327
}
192328

193329
config_.tags.emplace_back(tag_name, parent_tag);
330+
config_.tags_modified_utc = GetCurrentTimestampMillis();
194331

195332
auto err = UpdateConfig(config_);
196333
if (err != VXCORE_OK) {
@@ -323,6 +460,7 @@ VxCoreError Notebook::DeleteTag(const std::string &tag_name) {
323460
}
324461
}
325462

463+
config_.tags_modified_utc = GetCurrentTimestampMillis();
326464
auto err = UpdateConfig(config_);
327465
if (err != VXCORE_OK) {
328466
VXCORE_LOG_ERROR(
@@ -367,6 +505,7 @@ VxCoreError Notebook::MoveTag(const std::string &tag_name, const std::string &pa
367505

368506
tag->parent = parent_tag;
369507

508+
config_.tags_modified_utc = GetCurrentTimestampMillis();
370509
auto err = UpdateConfig(config_);
371510
if (err != VXCORE_OK) {
372511
VXCORE_LOG_ERROR(

src/core/notebook.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ struct NotebookConfig {
3535
std::string attachments_folder;
3636
nlohmann::json metadata;
3737
std::vector<TagNode> tags;
38+
int64_t tags_modified_utc;
3839

3940
NotebookConfig();
4041

@@ -101,6 +102,10 @@ class Notebook {
101102
// Returns VXCORE_OK on success, or error code on failure
102103
VxCoreError InitMetadataStore();
103104

105+
// Syncs tags from NotebookConfig to MetadataStore if config is newer
106+
// Returns VXCORE_OK on success or if no sync needed
107+
VxCoreError SyncTagsToMetadataStore();
108+
104109
std::string GetDbPath() const;
105110
virtual std::string GetConfigPath() const = 0;
106111

src/core/raw_notebook.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ VxCoreError RawNotebook::InitOnCreation() {
8080
return err;
8181
}
8282

83+
// Sync tags from NotebookConfig to MetadataStore
84+
err = SyncTagsToMetadataStore();
85+
if (err != VXCORE_OK) {
86+
VXCORE_LOG_WARN("Tag sync failed on creation: root=%s, error=%d", root_folder_.c_str(), err);
87+
// Continue anyway - tags will be synced on next open
88+
}
89+
8390
return VXCORE_OK;
8491
}
8592

@@ -102,6 +109,14 @@ VxCoreError RawNotebook::Open(const std::string &local_data_folder, const std::s
102109
return err;
103110
}
104111

112+
// Sync tags from NotebookConfig to MetadataStore if needed
113+
err = notebook->SyncTagsToMetadataStore();
114+
if (err != VXCORE_OK) {
115+
VXCORE_LOG_WARN("Tag sync failed on open: root=%s, id=%s, error=%d", root_folder.c_str(),
116+
id.c_str(), err);
117+
// Continue anyway - tags will be synced on next open or RebuildCache
118+
}
119+
105120
out_notebook = std::move(notebook);
106121
return VXCORE_OK;
107122
}

src/db/db_manager.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include <sqlite3.h>
44

5+
#include <optional>
6+
57
#include "db_schema.h"
68
#include "utils/logger.h"
79

src/db/db_schema.h

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace db {
1010
namespace schema {
1111

1212
// Schema version for migration tracking
13-
constexpr int kCurrentSchemaVersion = 2;
13+
constexpr int kCurrentSchemaVersion = 3;
1414

1515
// Folders table: stores folder hierarchy
1616
// parent_id references folders(id) - NULL for root folders
@@ -82,6 +82,14 @@ CREATE TABLE IF NOT EXISTS file_tags (
8282
CREATE INDEX IF NOT EXISTS idx_file_tags_tag ON file_tags(tag_id);
8383
)";
8484

85+
// Notebook metadata: key-value store for notebook-level metadata
86+
inline constexpr const char* kCreateNotebookMetadataTable = R"(
87+
CREATE TABLE IF NOT EXISTS notebook_metadata (
88+
key TEXT PRIMARY KEY,
89+
value TEXT NOT NULL
90+
);
91+
)";
92+
8593
// Schema version table for migrations
8694
inline constexpr const char* kCreateSchemaVersionTable = R"(
8795
CREATE TABLE IF NOT EXISTS schema_version (
@@ -92,18 +100,19 @@ CREATE TABLE IF NOT EXISTS schema_version (
92100
// Table names in reverse dependency order (safe for dropping)
93101
// When adding a new table, add it to this array in the appropriate position
94102
inline constexpr const char* kTableNames[] = {
95-
"file_tags", // Many-to-many relationship (depends on files, tags)
96-
"files", // Depends on folders
97-
"tags", // Independent
98-
"folders", // Independent (now includes sync state)
99-
"schema_version", // Independent
103+
"file_tags", // Many-to-many relationship (depends on files, tags)
104+
"files", // Depends on folders
105+
"tags", // Independent
106+
"folders", // Independent (now includes sync state)
107+
"notebook_metadata", // Independent (key-value store)
108+
"schema_version", // Independent
100109
};
101110

102111
// Combined initialization script
103112
inline const std::string GetInitializationScript() {
104113
return std::string(kCreateFoldersTable) + "\n" + std::string(kCreateFilesTable) + "\n" +
105114
std::string(kCreateTagsTable) + "\n" + std::string(kCreateFileTagsTable) + "\n" +
106-
std::string(kCreateSchemaVersionTable);
115+
std::string(kCreateNotebookMetadataTable) + "\n" + std::string(kCreateSchemaVersionTable);
107116
}
108117

109118
// Generate DROP TABLE statements for all tables

0 commit comments

Comments
 (0)