summaryrefslogtreecommitdiff
path: root/src/storage
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
committertanaka takashi <isazaka@gmail.com>2026-07-04 14:54:45 +0900
commit1efecaef1f867cb218366e2b4ab4b901fdd2835f (patch)
tree3080a3d80a1a0671e8e82819d3f785465aace162 /src/storage
parent34e00404e9561dcaf8d2d99df7470757a4fee915 (diff)
途中経過コミット
Diffstat (limited to 'src/storage')
-rw-r--r--src/storage/store.c856
-rw-r--r--src/storage/store.h25
2 files changed, 873 insertions, 8 deletions
diff --git a/src/storage/store.c b/src/storage/store.c
index 296f4d0..d798d86 100644
--- a/src/storage/store.c
+++ b/src/storage/store.c
@@ -9,10 +9,14 @@
#include "base/allocator.h"
#define RECORD_HEADER_SIZE 24U
-#define RECORD_VERSION 1U
+#define RECORD_VERSION 2U
+#define RECORD_VERSION_LEGACY 1U
#define RECORD_PUT 1U
#define RECORD_DEL 2U
#define RECORD_MARKER 3U
+#define RECORD_UPDATE 4U
+#define UPDATE_CONTENT 1U
+#define UPDATE_METADATA 2U
#define RECORD_PAYLOAD_MAX (TINY_CONTENT_MAX + 16384U)
#define SNAPSHOT_HEADER_SIZE 52U
@@ -27,6 +31,8 @@ struct tiny_store
size_t entry_count;
size_t entry_capacity;
uint64_t last_sequence;
+ uint64_t update_records;
+ uint64_t dead_bytes;
uint64_t last_ulid_ms;
unsigned char last_random[10];
unsigned char generation[16];
@@ -180,6 +186,7 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
{
size_t session_len = strlen(message->session_id), content_len = strlen(message->content);
size_t tool_len = message->metadata.tool_call_id ? strlen(message->metadata.tool_call_id) : 0;
+ size_t summary_len = message->metadata.summary ? strlen(message->metadata.summary) : 0;
size_t total = 8 + 26 + 2 + 16 + session_len + content_len + tool_len;
size_t index, offset;
unsigned char *data;
@@ -187,6 +194,8 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
{
total += 4 + strlen(message->metadata.tags[index]);
}
+ total += 32 + summary_len + message->metadata.relation_count * 27
+ + message->metadata.source_count * 26;
if (total > UINT32_MAX || total > RECORD_PAYLOAD_MAX)
{
return false;
@@ -228,6 +237,36 @@ serialize_put(const tiny_message_t *message, unsigned char **payload, uint32_t *
memcpy(data + offset, message->metadata.tags[index], tag_len);
offset += tag_len;
}
+ memcpy(data + offset, "M2", 2);
+ offset += 2;
+ tiny_codec_put_u64(data + offset, message->revision);
+ offset += 8;
+ tiny_codec_put_u64(data + offset, (uint64_t)message->updated_at_ms);
+ offset += 8;
+ data[offset++] = (unsigned char)message->metadata.memory_kind;
+ data[offset++] = message->metadata.importance;
+ tiny_codec_put_u32(data + offset, (uint32_t)summary_len);
+ offset += 4;
+ tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.relation_count);
+ offset += 4;
+ tiny_codec_put_u32(data + offset, (uint32_t)message->metadata.source_count);
+ offset += 4;
+ if (summary_len > 0)
+ {
+ memcpy(data + offset, message->metadata.summary, summary_len);
+ offset += summary_len;
+ }
+ for (index = 0; index < message->metadata.relation_count; ++index)
+ {
+ data[offset++] = (unsigned char)message->metadata.relations[index].type;
+ memcpy(data + offset, message->metadata.relations[index].message_id, 26);
+ offset += 26;
+ }
+ for (index = 0; index < message->metadata.source_count; ++index)
+ {
+ memcpy(data + offset, message->metadata.source_message_ids[index], 26);
+ offset += 26;
+ }
*payload = data;
*length = (uint32_t)total;
return true;
@@ -330,12 +369,95 @@ deserialize_put(const unsigned char *data, uint32_t length, tiny_message_t *mess
message->metadata.tag_count++;
}
}
- if (offset != length || !tiny_utf8_valid(message->session_id, session_len)
+ if (!tiny_utf8_valid(message->session_id, session_len)
|| !tiny_utf8_valid(message->content, content_len))
{
return deserialize_failed(message);
}
- return true;
+ if (offset == length)
+ {
+ message->updated_at_ms = message->timestamp_ms;
+ return true;
+ }
+ {
+ uint32_t summary_len, relation_count, source_count;
+ if (length - offset < 32 || memcmp(data + offset, "M2", 2) != 0)
+ {
+ return deserialize_failed(message);
+ }
+ offset += 2;
+ message->revision = tiny_codec_get_u64(data + offset);
+ offset += 8;
+ message->updated_at_ms = (int64_t)tiny_codec_get_u64(data + offset);
+ offset += 8;
+ message->metadata.memory_kind = (tiny_memory_kind_t)data[offset++];
+ message->metadata.importance = data[offset++];
+ summary_len = tiny_codec_get_u32(data + offset);
+ offset += 4;
+ relation_count = tiny_codec_get_u32(data + offset);
+ offset += 4;
+ source_count = tiny_codec_get_u32(data + offset);
+ offset += 4;
+ if (message->revision == 0 || message->metadata.memory_kind < TINY_KIND_CONVERSATION
+ || message->metadata.memory_kind > TINY_KIND_SUMMARY || summary_len > TINY_SUMMARY_MAX
+ || relation_count > TINY_RELATION_COUNT_MAX || source_count > TINY_SOURCE_COUNT_MAX
+ || offset + summary_len + (size_t)relation_count * 27 + (size_t)source_count * 26 != length)
+ {
+ return deserialize_failed(message);
+ }
+ if (summary_len > 0)
+ {
+ message->metadata.summary = copy_field(data + offset, summary_len);
+ if (!message->metadata.summary || !tiny_utf8_valid(message->metadata.summary, summary_len))
+ {
+ return deserialize_failed(message);
+ }
+ offset += summary_len;
+ }
+ if (relation_count > 0)
+ {
+ message->metadata.relations = calloc(relation_count, sizeof(tiny_relation_t));
+ if (!message->metadata.relations)
+ {
+ return deserialize_failed(message);
+ }
+ for (index = 0; index < relation_count; ++index)
+ {
+ tiny_relation_t *relation = &message->metadata.relations[index];
+ relation->type = (tiny_relation_type_t)data[offset++];
+ memcpy(relation->message_id, data + offset, 26);
+ relation->message_id[26] = '\0';
+ offset += 26;
+ if (relation->type < TINY_RELATION_DEPENDS_ON
+ || relation->type > TINY_RELATION_RELATED_TO
+ || !tiny_message_id_valid(relation->message_id))
+ {
+ return deserialize_failed(message);
+ }
+ message->metadata.relation_count++;
+ }
+ }
+ if (source_count > 0)
+ {
+ message->metadata.source_message_ids = calloc(source_count, sizeof(char *));
+ if (!message->metadata.source_message_ids)
+ {
+ return deserialize_failed(message);
+ }
+ for (index = 0; index < source_count; ++index)
+ {
+ message->metadata.source_message_ids[index] = copy_field(data + offset, 26);
+ offset += 26;
+ if (!message->metadata.source_message_ids[index]
+ || !tiny_message_id_valid(message->metadata.source_message_ids[index]))
+ {
+ return deserialize_failed(message);
+ }
+ message->metadata.source_count++;
+ }
+ }
+ }
+ return offset == length;
}
static tiny_store_status_t
@@ -381,6 +503,545 @@ apply_put(tiny_store_t *store, tiny_message_t *message, uint64_t sequence)
return TINY_STORE_OK;
}
+static bool
+string_equal(const char *left, const char *right)
+{
+ return left == right || (left && right && strcmp(left, right) == 0);
+}
+
+static bool
+metadata_patch_changes(const tiny_metadata_t *current,
+ const tiny_metadata_t *patch,
+ tiny_metadata_mask_t mask)
+{
+ tiny_metadata_t merged = *current;
+ if (mask & TINY_METADATA_TOOL_CALL_ID)
+ {
+ merged.tool_call_id = patch->tool_call_id;
+ }
+ if (mask & TINY_METADATA_TAGS)
+ {
+ merged.tags = patch->tags;
+ merged.tag_count = patch->tag_count;
+ }
+ if (mask & TINY_METADATA_TIER)
+ {
+ merged.memory_tier = patch->memory_tier;
+ }
+ if (mask & TINY_METADATA_KIND)
+ {
+ merged.memory_kind = patch->memory_kind;
+ }
+ if (mask & TINY_METADATA_IMPORTANCE)
+ {
+ merged.importance = patch->importance;
+ }
+ if (mask & TINY_METADATA_SUMMARY)
+ {
+ merged.summary = patch->summary;
+ }
+ if (mask & TINY_METADATA_RELATIONS)
+ {
+ merged.relations = patch->relations;
+ merged.relation_count = patch->relation_count;
+ }
+ if (mask & TINY_METADATA_SOURCES)
+ {
+ merged.source_message_ids = patch->source_message_ids;
+ merged.source_count = patch->source_count;
+ }
+ if (!string_equal(current->tool_call_id, merged.tool_call_id)
+ || current->tag_count != merged.tag_count || current->memory_tier != merged.memory_tier
+ || current->memory_kind != merged.memory_kind || current->importance != merged.importance
+ || !string_equal(current->summary, merged.summary)
+ || current->relation_count != merged.relation_count
+ || current->source_count != merged.source_count)
+ {
+ return true;
+ }
+ if (current->tag_count)
+ {
+ size_t index;
+ for (index = 0; index < current->tag_count; ++index)
+ {
+ if (strcmp(current->tags[index], merged.tags[index]) != 0)
+ {
+ return true;
+ }
+ }
+ }
+ if (current->relation_count
+ && memcmp(current->relations,
+ merged.relations,
+ current->relation_count * sizeof(*current->relations))
+ != 0)
+ {
+ return true;
+ }
+ if (current->source_count)
+ {
+ size_t index;
+ for (index = 0; index < current->source_count; ++index)
+ {
+ if (strcmp(current->source_message_ids[index], merged.source_message_ids[index]) != 0)
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+static bool
+metadata_targets_valid(tiny_store_t *store, const char *self_id, const tiny_metadata_t *metadata)
+{
+ size_t index;
+ if (!tiny_metadata_valid(metadata))
+ {
+ return false;
+ }
+ for (index = 0; index < metadata->relation_count; ++index)
+ {
+ tiny_store_entry_t *target;
+ if (self_id && strcmp(self_id, metadata->relations[index].message_id) == 0)
+ {
+ return false;
+ }
+ target = find_entry(store, metadata->relations[index].message_id);
+ if (!target || target->deleted)
+ {
+ return false;
+ }
+ }
+ for (index = 0; index < metadata->source_count; ++index)
+ {
+ tiny_store_entry_t *target;
+ if (self_id && strcmp(self_id, metadata->source_message_ids[index]) == 0)
+ {
+ return false;
+ }
+ target = find_entry(store, metadata->source_message_ids[index]);
+ if (!target || target->deleted)
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+static tiny_store_status_t
+prepare_update(tiny_store_t *store,
+ tiny_store_entry_t *entry,
+ const tiny_message_patch_t *patch,
+ uint64_t revision,
+ int64_t updated_at,
+ tiny_message_t *prepared)
+{
+ tiny_message_t merged = entry->message;
+ tiny_metadata_t metadata = entry->message.metadata;
+ if (patch->content)
+ {
+ merged.content = (char *)patch->content;
+ }
+ if (patch->metadata_mask & TINY_METADATA_TOOL_CALL_ID)
+ {
+ metadata.tool_call_id = patch->metadata->tool_call_id;
+ }
+ if (patch->metadata_mask & TINY_METADATA_TAGS)
+ {
+ metadata.tags = patch->metadata->tags;
+ metadata.tag_count = patch->metadata->tag_count;
+ }
+ if (patch->metadata_mask & TINY_METADATA_TIER)
+ {
+ metadata.memory_tier = patch->metadata->memory_tier;
+ }
+ if (patch->metadata_mask & TINY_METADATA_KIND)
+ {
+ metadata.memory_kind = patch->metadata->memory_kind;
+ }
+ if (patch->metadata_mask & TINY_METADATA_IMPORTANCE)
+ {
+ metadata.importance = patch->metadata->importance;
+ }
+ if (patch->metadata_mask & TINY_METADATA_SUMMARY)
+ {
+ metadata.summary = patch->metadata->summary;
+ }
+ if (patch->metadata_mask & TINY_METADATA_RELATIONS)
+ {
+ metadata.relations = patch->metadata->relations;
+ metadata.relation_count = patch->metadata->relation_count;
+ }
+ if (patch->metadata_mask & TINY_METADATA_SOURCES)
+ {
+ metadata.source_message_ids = patch->metadata->source_message_ids;
+ metadata.source_count = patch->metadata->source_count;
+ }
+ merged.metadata = metadata;
+ if (!merged.content || !merged.content[0] || strlen(merged.content) > TINY_CONTENT_MAX
+ || !tiny_utf8_valid(merged.content, strlen(merged.content))
+ || !metadata_targets_valid(store, merged.message_id, &metadata))
+ {
+ return TINY_STORE_CONFLICT;
+ }
+ if (tiny_message_copy(prepared, &merged) != 0)
+ {
+ return TINY_STORE_NO_MEMORY;
+ }
+ prepared->revision = revision;
+ prepared->updated_at_ms = updated_at;
+ return TINY_STORE_OK;
+}
+
+static bool
+delta_put_string(unsigned char *data, size_t capacity, size_t *offset, const char *value)
+{
+ size_t length = value ? strlen(value) : 0;
+ if (length > UINT32_MAX || *offset > capacity || capacity - *offset < 4 + length)
+ {
+ return false;
+ }
+ tiny_codec_put_u32(data + *offset, (uint32_t)length);
+ *offset += 4;
+ if (length)
+ {
+ memcpy(data + *offset, value, length);
+ *offset += length;
+ }
+ return true;
+}
+
+static bool
+serialize_update(const tiny_store_entry_t *entry,
+ const tiny_message_patch_t *patch,
+ uint64_t revision,
+ int64_t updated_at,
+ unsigned char **payload,
+ uint32_t *payload_length)
+{
+ size_t capacity = 64 + (patch->content ? strlen(patch->content) : 0), offset = 0, index;
+ unsigned char *data;
+ tiny_metadata_mask_t mask = patch->metadata_mask;
+ if (mask & TINY_METADATA_TOOL_CALL_ID)
+ {
+ capacity += 4 + (patch->metadata->tool_call_id ? strlen(patch->metadata->tool_call_id) : 0);
+ }
+ if (mask & TINY_METADATA_TAGS)
+ {
+ capacity += 4;
+ for (index = 0; index < patch->metadata->tag_count; ++index)
+ {
+ capacity += 4 + strlen(patch->metadata->tags[index]);
+ }
+ }
+ if (mask & TINY_METADATA_SUMMARY)
+ {
+ capacity += 4 + (patch->metadata->summary ? strlen(patch->metadata->summary) : 0);
+ }
+ if (mask & TINY_METADATA_RELATIONS)
+ {
+ capacity += 4 + patch->metadata->relation_count * 27;
+ }
+ if (mask & TINY_METADATA_SOURCES)
+ {
+ capacity += 4 + patch->metadata->source_count * 26;
+ }
+ capacity += (mask & TINY_METADATA_TIER) ? 1 : 0;
+ capacity += (mask & TINY_METADATA_KIND) ? 1 : 0;
+ capacity += (mask & TINY_METADATA_IMPORTANCE) ? 1 : 0;
+ if (capacity > RECORD_PAYLOAD_MAX || capacity > UINT32_MAX || !(data = malloc(capacity)))
+ {
+ return false;
+ }
+ memcpy(data, entry->message.message_id, 26);
+ offset = 26;
+ tiny_codec_put_u64(data + offset, revision);
+ offset += 8;
+ tiny_codec_put_u64(data + offset, (uint64_t)updated_at);
+ offset += 8;
+ data[offset++] = patch->content ? UPDATE_CONTENT : 0;
+ data[offset - 1] |= mask ? UPDATE_METADATA : 0;
+ tiny_codec_put_u32(data + offset, mask);
+ offset += 4;
+ if (patch->content && !delta_put_string(data, capacity, &offset, patch->content))
+ {
+ free(data);
+ return false;
+ }
+ if ((mask & TINY_METADATA_TOOL_CALL_ID)
+ && !delta_put_string(data, capacity, &offset, patch->metadata->tool_call_id))
+ {
+ free(data);
+ return false;
+ }
+ if (mask & TINY_METADATA_TAGS)
+ {
+ tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->tag_count);
+ offset += 4;
+ for (index = 0; index < patch->metadata->tag_count; ++index)
+ {
+ if (!delta_put_string(data, capacity, &offset, patch->metadata->tags[index]))
+ {
+ free(data);
+ return false;
+ }
+ }
+ }
+ if (mask & TINY_METADATA_TIER)
+ {
+ data[offset++] = (unsigned char)patch->metadata->memory_tier;
+ }
+ if (mask & TINY_METADATA_KIND)
+ {
+ data[offset++] = (unsigned char)patch->metadata->memory_kind;
+ }
+ if (mask & TINY_METADATA_IMPORTANCE)
+ {
+ data[offset++] = patch->metadata->importance;
+ }
+ if ((mask & TINY_METADATA_SUMMARY)
+ && !delta_put_string(data, capacity, &offset, patch->metadata->summary))
+ {
+ free(data);
+ return false;
+ }
+ if (mask & TINY_METADATA_RELATIONS)
+ {
+ tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->relation_count);
+ offset += 4;
+ for (index = 0; index < patch->metadata->relation_count; ++index)
+ {
+ data[offset++] = (unsigned char)patch->metadata->relations[index].type;
+ memcpy(data + offset, patch->metadata->relations[index].message_id, 26);
+ offset += 26;
+ }
+ }
+ if (mask & TINY_METADATA_SOURCES)
+ {
+ tiny_codec_put_u32(data + offset, (uint32_t)patch->metadata->source_count);
+ offset += 4;
+ for (index = 0; index < patch->metadata->source_count; ++index)
+ {
+ memcpy(data + offset, patch->metadata->source_message_ids[index], 26);
+ offset += 26;
+ }
+ }
+ *payload = data;
+ *payload_length = (uint32_t)offset;
+ return true;
+}
+
+static char *
+delta_get_string(const unsigned char *data, size_t length, size_t *offset, size_t maximum)
+{
+ uint32_t field_length;
+ char *value;
+ if (*offset > length || length - *offset < 4)
+ {
+ return NULL;
+ }
+ field_length = tiny_codec_get_u32(data + *offset);
+ *offset += 4;
+ if (field_length > maximum || *offset > length || length - *offset < field_length)
+ {
+ return NULL;
+ }
+ value = copy_field(data + *offset, field_length);
+ *offset += field_length;
+ return value;
+}
+
+static tiny_store_status_t
+prepare_update_payload(tiny_store_t *store,
+ const unsigned char *payload,
+ uint32_t length,
+ tiny_store_entry_t **target,
+ tiny_message_t *prepared)
+{
+ tiny_message_t holder;
+ tiny_message_patch_t patch;
+ size_t offset = 0, index;
+ uint64_t revision;
+ int64_t updated_at;
+ uint8_t flags;
+ uint32_t mask;
+ char id[27];
+ tiny_store_status_t status = TINY_STORE_CORRUPT;
+ tiny_message_init(&holder);
+ memset(&patch, 0, sizeof(patch));
+#define UPDATE_PAYLOAD_BAD() \
+ do \
+ { \
+ tiny_message_free(&holder); \
+ return TINY_STORE_CORRUPT; \
+ } \
+ while (0)
+ if (length < 47)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ memcpy(id, payload, 26);
+ id[26] = '\0';
+ offset = 26;
+ revision = tiny_codec_get_u64(payload + offset);
+ offset += 8;
+ updated_at = (int64_t)tiny_codec_get_u64(payload + offset);
+ offset += 8;
+ flags = payload[offset++];
+ mask = tiny_codec_get_u32(payload + offset);
+ offset += 4;
+ *target = find_entry(store, id);
+ if (!*target || (*target)->deleted || revision != (*target)->message.revision + 1
+ || revision > INT32_MAX || !updated_at || (mask & ~TINY_METADATA_ALL)
+ || flags != ((flags & UPDATE_CONTENT ? UPDATE_CONTENT : 0) | (mask ? UPDATE_METADATA : 0)))
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ if (flags & UPDATE_CONTENT)
+ {
+ holder.content = delta_get_string(payload, length, &offset, TINY_CONTENT_MAX);
+ if (!holder.content)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ patch.content = holder.content;
+ }
+ patch.metadata = &holder.metadata;
+ patch.metadata_mask = mask;
+ if (mask & TINY_METADATA_TOOL_CALL_ID)
+ {
+ holder.metadata.tool_call_id
+ = delta_get_string(payload, length, &offset, TINY_TOOL_CALL_ID_MAX);
+ if (!holder.metadata.tool_call_id)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ if (!holder.metadata.tool_call_id[0])
+ {
+ free(holder.metadata.tool_call_id);
+ holder.metadata.tool_call_id = NULL;
+ }
+ }
+ if (mask & TINY_METADATA_TAGS)
+ {
+ uint32_t count;
+ if (offset > length || length - offset < 4)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ count = tiny_codec_get_u32(payload + offset);
+ offset += 4;
+ if (count > TINY_TAG_COUNT_MAX
+ || (count && !(holder.metadata.tags = calloc(count, sizeof(char *)))))
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ for (index = 0; index < count; ++index)
+ {
+ holder.metadata.tags[index] = delta_get_string(payload, length, &offset, TINY_TAG_MAX);
+ if (!holder.metadata.tags[index] || !holder.metadata.tags[index][0])
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ holder.metadata.tag_count++;
+ }
+ }
+ if (mask & TINY_METADATA_TIER)
+ {
+ if (offset >= length)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ holder.metadata.memory_tier = (tiny_memory_tier_t)payload[offset++];
+ }
+ if (mask & TINY_METADATA_KIND)
+ {
+ if (offset >= length)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ holder.metadata.memory_kind = (tiny_memory_kind_t)payload[offset++];
+ }
+ if (mask & TINY_METADATA_IMPORTANCE)
+ {
+ if (offset >= length)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ holder.metadata.importance = payload[offset++];
+ }
+ if (mask & TINY_METADATA_SUMMARY)
+ {
+ holder.metadata.summary = delta_get_string(payload, length, &offset, TINY_SUMMARY_MAX);
+ if (!holder.metadata.summary)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ if (!holder.metadata.summary[0])
+ {
+ free(holder.metadata.summary);
+ holder.metadata.summary = NULL;
+ }
+ }
+ if (mask & TINY_METADATA_RELATIONS)
+ {
+ uint32_t count;
+ if (offset > length || length - offset < 4)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ count = tiny_codec_get_u32(payload + offset);
+ offset += 4;
+ if (count > TINY_RELATION_COUNT_MAX || offset > length || length - offset < (size_t)count * 27
+ || (count && !(holder.metadata.relations = calloc(count, sizeof(tiny_relation_t)))))
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ for (index = 0; index < count; ++index)
+ {
+ holder.metadata.relations[index].type = (tiny_relation_type_t)payload[offset++];
+ memcpy(holder.metadata.relations[index].message_id, payload + offset, 26);
+ holder.metadata.relations[index].message_id[26] = '\0';
+ offset += 26;
+ holder.metadata.relation_count++;
+ }
+ }
+ if (mask & TINY_METADATA_SOURCES)
+ {
+ uint32_t count;
+ if (offset > length || length - offset < 4)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ count = tiny_codec_get_u32(payload + offset);
+ offset += 4;
+ if (count > TINY_SOURCE_COUNT_MAX || offset > length || length - offset < (size_t)count * 26
+ || (count && !(holder.metadata.source_message_ids = calloc(count, sizeof(char *)))))
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ for (index = 0; index < count; ++index)
+ {
+ holder.metadata.source_message_ids[index] = copy_field(payload + offset, 26);
+ if (!holder.metadata.source_message_ids[index])
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ offset += 26;
+ holder.metadata.source_count++;
+ }
+ }
+ if (offset != length)
+ {
+ UPDATE_PAYLOAD_BAD();
+ }
+ status = prepare_update(store, *target, &patch, revision, updated_at, prepared);
+ tiny_message_free(&holder);
+#undef UPDATE_PAYLOAD_BAD
+ return status;
+}
+
static void
clear_entries(tiny_store_t *store)
{
@@ -431,8 +1092,10 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
{
return recover_torn_tail(store, boundary);
}
- if (memcmp(header, "TCMR", 4) != 0 || header[4] != RECORD_VERSION
- || (header[5] != RECORD_PUT && header[5] != RECORD_DEL && header[5] != RECORD_MARKER))
+ if (memcmp(header, "TCMR", 4) != 0
+ || (header[4] != RECORD_VERSION && header[4] != RECORD_VERSION_LEGACY)
+ || (header[5] != RECORD_PUT && header[5] != RECORD_DEL && header[5] != RECORD_MARKER
+ && header[5] != RECORD_UPDATE))
{
return TINY_STORE_CORRUPT;
}
@@ -507,9 +1170,28 @@ replay_log(tiny_store_t *store, uint64_t starting_offset)
return TINY_STORE_CORRUPT;
}
entry->deleted = true;
+ store->dead_bytes += RECORD_HEADER_SIZE + payload_length;
}
break;
+ case RECORD_UPDATE:
+ {
+ tiny_store_entry_t *target = NULL;
+ tiny_message_t prepared;
+ tiny_message_init(&prepared);
+ status = prepare_update_payload(store, payload, payload_length, &target, &prepared);
+ if (status == TINY_STORE_OK)
+ {
+ tiny_message_free(&target->message);
+ target->message = prepared;
+ tiny_message_init(&prepared);
+ }
+ tiny_message_free(&prepared);
+ }
+ store->update_records++;
+ store->dead_bytes += RECORD_HEADER_SIZE + payload_length;
+ break;
+
default:
free(payload);
return TINY_STORE_CORRUPT;
@@ -596,8 +1278,10 @@ log_generation_matches(tiny_store_t *store, const unsigned char generation[16])
uint32_t actual = 0;
if (!tiny_sys_file_seek(&store->log, 0, TINY_SYS_SEEK_BEGIN)
|| !read_all(&store->log, header, sizeof(header), &actual) || actual != sizeof(header)
- || memcmp(header, "TCMR", 4) != 0 || header[4] != RECORD_VERSION || header[5] != RECORD_MARKER
- || tiny_codec_get_u64(header + 12) != 0 || tiny_codec_get_u32(header + 8) != 16
+ || memcmp(header, "TCMR", 4) != 0
+ || (header[4] != RECORD_VERSION && header[4] != RECORD_VERSION_LEGACY)
+ || header[5] != RECORD_MARKER || tiny_codec_get_u64(header + 12) != 0
+ || tiny_codec_get_u32(header + 8) != 16
|| !read_all(&store->log, payload, sizeof(payload), &actual) || actual != sizeof(payload)
|| tiny_codec_get_u32(header + 20) != tiny_codec_crc32(payload, sizeof(payload)))
{
@@ -799,6 +1483,8 @@ tiny_store_status_name(tiny_store_status_t status)
return "IO_ERROR";
case TINY_STORE_CORRUPT:
return "CORRUPT_STORE";
+ case TINY_STORE_CONFLICT:
+ return "CONFLICT";
default:
return "INTERNAL_ERROR";
}
@@ -820,6 +1506,12 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
{
return TINY_STORE_IO_ERROR;
}
+ if (!input->content[0] || strlen(input->content) > TINY_CONTENT_MAX
+ || !tiny_utf8_valid(input->content, strlen(input->content))
+ || !metadata_targets_valid(store, NULL, &input->metadata))
+ {
+ return TINY_STORE_CONFLICT;
+ }
tiny_message_init(&message);
tiny_message_init(&indexed);
if (tiny_message_copy(&message, input) != 0)
@@ -827,6 +1519,8 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
return TINY_STORE_NO_MEMORY;
}
message.timestamp_ms = (int64_t)now_milliseconds();
+ message.updated_at_ms = message.timestamp_ms;
+ message.revision = 1;
make_ulid(store, (uint64_t)message.timestamp_ms, message.message_id);
if (!serialize_put(&message, &payload, &length) || !ensure_entries(store)
|| tiny_message_copy(&indexed, &message) != 0)
@@ -856,6 +1550,127 @@ tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_
}
tiny_store_status_t
+tiny_store_update(tiny_store_t *store,
+ const char *message_id,
+ const tiny_message_patch_t *patch,
+ tiny_message_t *updated,
+ bool *changed)
+{
+ tiny_store_entry_t *entry;
+ tiny_message_t prepared, response;
+ tiny_message_patch_t effective;
+ unsigned char *payload = NULL;
+ uint32_t payload_length = 0;
+ uint64_t log_size_before = 0;
+ uint64_t revision;
+ int64_t updated_at;
+ tiny_store_status_t status;
+ if (!store || !message_id || !patch || !updated || !changed || !tiny_message_id_valid(message_id))
+ {
+ return TINY_STORE_NOT_FOUND;
+ }
+ tiny_message_init(updated);
+ tiny_message_init(&prepared);
+ tiny_message_init(&response);
+ *changed = false;
+ entry = find_entry(store, message_id);
+ if (!entry || entry->deleted)
+ {
+ return TINY_STORE_NOT_FOUND;
+ }
+ if (patch->has_expected_revision && patch->expected_revision != entry->message.revision)
+ {
+ return TINY_STORE_CONFLICT;
+ }
+ if (entry->message.revision >= INT32_MAX)
+ {
+ return TINY_STORE_CONFLICT;
+ }
+ effective = *patch;
+ if ((effective.metadata_mask & ~TINY_METADATA_ALL) != 0
+ || (effective.metadata_mask
+ && (!effective.metadata || !tiny_metadata_valid(effective.metadata))))
+ {
+ return TINY_STORE_CONFLICT;
+ }
+ if (effective.content && strcmp(effective.content, entry->message.content) == 0)
+ {
+ effective.content = NULL;
+ }
+ if (!effective.metadata || effective.metadata_mask == 0
+ || !metadata_patch_changes(
+ &entry->message.metadata, effective.metadata, effective.metadata_mask))
+ {
+ effective.metadata_mask = 0;
+ }
+ if (!effective.content && effective.metadata_mask == 0)
+ {
+ return tiny_message_copy(updated, &entry->message) == 0 ? TINY_STORE_OK
+ : TINY_STORE_NO_MEMORY;
+ }
+ revision = entry->message.revision + 1;
+ updated_at = (int64_t)now_milliseconds();
+ status = prepare_update(store, entry, &effective, revision, updated_at, &prepared);
+ if (status != TINY_STORE_OK)
+ {
+ tiny_message_free(&prepared);
+ return status;
+ }
+ if (tiny_message_copy(&response, &prepared) != 0)
+ {
+ status = TINY_STORE_NO_MEMORY;
+ tiny_message_free(&prepared);
+ return status;
+ }
+ if (!serialize_update(entry, &effective, revision, updated_at, &payload, &payload_length))
+ {
+ status = TINY_STORE_NO_MEMORY;
+ tiny_message_free(&prepared);
+ tiny_message_free(&response);
+ return status;
+ }
+ if (!tiny_sys_file_size(&store->log, &log_size_before))
+ {
+ free(payload);
+ tiny_message_free(&prepared);
+ tiny_message_free(&response);
+ return TINY_STORE_IO_ERROR;
+ }
+ status = append_record_to(
+ &store->log, RECORD_UPDATE, store->last_sequence + 1, payload, payload_length, true);
+ if (status != TINY_STORE_OK)
+ {
+ bool rolled_back = log_size_before <= INT64_MAX
+ && tiny_sys_file_seek(
+ &store->log, (int64_t)log_size_before, TINY_SYS_SEEK_BEGIN)
+ && tiny_sys_file_truncate(&store->log)
+ && tiny_sys_file_flush(&store->log);
+ set_error(store, "failed to append and flush an UPDATE record");
+ if (!rolled_back)
+ {
+ set_error(store, "UPDATE failed and its incomplete record could not be rolled back");
+ }
+ free(payload);
+ tiny_message_free(&prepared);
+ tiny_message_free(&response);
+ return status;
+ }
+ tiny_message_free(&entry->message);
+ entry->message = prepared;
+ tiny_message_init(&prepared);
+ *updated = response;
+ tiny_message_init(&response);
+ store->last_sequence++;
+ store->update_records++;
+ store->dead_bytes += RECORD_HEADER_SIZE + payload_length;
+ *changed = true;
+ free(payload);
+ tiny_message_free(&prepared);
+ tiny_message_free(&response);
+ return TINY_STORE_OK;
+}
+
+tiny_store_status_t
tiny_store_delete(tiny_store_t *store, const char *message_id, const char *session_id)
{
tiny_store_entry_t *entry;
@@ -882,6 +1697,7 @@ tiny_store_delete(tiny_store_t *store, const char *message_id, const char *sessi
}
store->last_sequence++;
entry->deleted = true;
+ store->dead_bytes += RECORD_HEADER_SIZE + sizeof(payload);
return TINY_STORE_OK;
}
@@ -905,6 +1721,15 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
{
return TINY_STORE_IO_ERROR;
}
+ if (store->snapshot_valid && store->update_records == 0 && store->dead_bytes == 0)
+ {
+ uint64_t size = 0;
+ (void)tiny_sys_file_size(&store->log, &size);
+ report->messages_indexed = store->entry_count;
+ report->bytes_before = size;
+ report->bytes_after = size;
+ return TINY_STORE_OK;
+ }
memcpy(previous_generation, store->generation, sizeof(previous_generation));
previous_has_generation = store->has_generation;
temporary = tiny_sys_path_join(store->directory, "active.tmp");
@@ -1002,6 +1827,11 @@ tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report)
report->bytes_before = before;
report->bytes_after = after;
status = write_snapshot(store);
+ if (status == TINY_STORE_OK)
+ {
+ store->update_records = 0;
+ store->dead_bytes = 0;
+ }
}
if (status != TINY_STORE_OK)
{
@@ -1032,6 +1862,7 @@ tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
{
return;
}
+ health->format_version = RECORD_VERSION;
if (tiny_sys_file_size(&store->log, &size))
{
health->log_size = size;
@@ -1059,6 +1890,17 @@ tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health)
}
}
health->last_sequence = store->last_sequence;
+ health->update_records = store->update_records;
+ health->dead_bytes = store->dead_bytes;
+ {
+ tiny_sys_io_counters_t counters;
+ tiny_sys_io_counters_get(&counters);
+ health->write_bytes = counters.write_bytes;
+ health->write_calls = counters.write_calls;
+ health->flush_calls = counters.flush_calls;
+ health->truncate_calls = counters.truncate_calls;
+ health->replace_calls = counters.replace_calls;
+ }
health->recovered_tail = store->recovered_tail;
health->snapshot_valid = store->snapshot_valid;
snprintf(health->last_error, sizeof(health->last_error), "%s", store->last_error);
diff --git a/src/storage/store.h b/src/storage/store.h
index 619f86f..3ddd24d 100644
--- a/src/storage/store.h
+++ b/src/storage/store.h
@@ -14,7 +14,8 @@ typedef enum tiny_store_status
TINY_STORE_BUSY,
TINY_STORE_IO_ERROR,
TINY_STORE_CORRUPT,
- TINY_STORE_NO_MEMORY
+ TINY_STORE_NO_MEMORY,
+ TINY_STORE_CONFLICT
} tiny_store_status_t;
typedef struct tiny_store_entry
@@ -36,16 +37,33 @@ typedef struct tiny_rebuild_report
typedef struct tiny_store_health
{
+ uint32_t format_version;
uint64_t log_size;
uint64_t snapshot_size;
uint64_t active_messages;
uint64_t deleted_messages;
uint64_t last_sequence;
+ uint64_t update_records;
+ uint64_t dead_bytes;
+ uint64_t write_bytes;
+ uint64_t write_calls;
+ uint64_t flush_calls;
+ uint64_t truncate_calls;
+ uint64_t replace_calls;
bool recovered_tail;
bool snapshot_valid;
char last_error[160];
} tiny_store_health_t;
+typedef struct tiny_message_patch
+{
+ const char *content;
+ const tiny_metadata_t *metadata;
+ tiny_metadata_mask_t metadata_mask;
+ uint64_t expected_revision;
+ bool has_expected_revision;
+} tiny_message_patch_t;
+
tiny_store_status_t tiny_store_open(tiny_store_t **store, const char *directory);
void tiny_store_close(tiny_store_t *store);
const char *tiny_store_status_name(tiny_store_status_t status);
@@ -54,6 +72,11 @@ tiny_store_status_t
tiny_store_write(tiny_store_t *store, const tiny_message_t *input, tiny_message_t *written);
tiny_store_status_t
tiny_store_delete(tiny_store_t *store, const char *message_id, const char *session_id);
+tiny_store_status_t tiny_store_update(tiny_store_t *store,
+ const char *message_id,
+ const tiny_message_patch_t *patch,
+ tiny_message_t *updated,
+ bool *changed);
tiny_store_status_t tiny_store_rebuild(tiny_store_t *store, tiny_rebuild_report_t *report);
void tiny_store_health(const tiny_store_t *store, tiny_store_health_t *health);