Two persona-infrastructure correctness fixes that go together because
the second one (FK with CASCADE) requires the first (preventing the
persona row from being mutated out from under its facts).
1. update_persona handler refuses name/systemPrompt edits to built-ins
(409). includeAllMemories stays editable — that's a per-user
preference, not the persona's identity. Mirrors the existing
delete_persona guard. The DAO is intentionally permissive so the
guard sits at the HTTP layer; persona_dao test pins that contract.
2. Migration 2026-05-10 adds user_id to entity_facts and a composite
FK (user_id, persona_id) -> personas(user_id, persona_id) ON DELETE
CASCADE. This closes two issues at once:
- Persona orphans: deleting a custom persona used to leave its
facts dangling forever, readable only via PersonaFilter::All.
CASCADE now wipes them with the persona row.
- Multi-user fact leakage: PersonaFilter::Single("default") used
to surface every user's default-scoped facts. PersonaFilter is
now { user_id, persona_id } and all read paths
(get_facts_for_entity, list_facts, get_recent_activity) filter
on user_id first. upsert_fact's dedup key extends to user_id so
identical claims under shared persona names from different
users no longer corroborate-bump each other's confidence.
- user_id threads from Claims.sub.parse::<i32>().unwrap_or(1) at
the chat / insight handlers through ChatTurnRequest, the
streaming agentic loop, execute_tool, and into the leaf tools
(tool_store_fact, tool_recall_facts_for_photo). The ".unwrap_or(1)"
accommodates Apollo's service token whose sub is non-numeric on
legacy mints.
- Backfill picks the smallest user_id matching each legacy fact's
persona_id so the FK holds for already-stored rows.
Five new knowledge_dao tests with FK-on connection: persona scoping
isolation, All-variant union per-user, dedup not crossing users,
CASCADE delete, FK rejection of unknown personas. Plus
dao_update_does_not_block_built_ins documenting where the
HTTP-layer guard lives.
Apollo coordinates separately — the matching changes there add the
/api/personas proxy and start sending persona_id on photo-chat turns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
294 lines
7.2 KiB
Rust
294 lines
7.2 KiB
Rust
// @generated automatically by Diesel CLI.
|
|
|
|
diesel::table! {
|
|
calendar_events (id) {
|
|
id -> Integer,
|
|
event_uid -> Nullable<Text>,
|
|
summary -> Text,
|
|
description -> Nullable<Text>,
|
|
location -> Nullable<Text>,
|
|
start_time -> BigInt,
|
|
end_time -> BigInt,
|
|
all_day -> Bool,
|
|
organizer -> Nullable<Text>,
|
|
attendees -> Nullable<Text>,
|
|
embedding -> Nullable<Binary>,
|
|
created_at -> BigInt,
|
|
source_file -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
daily_conversation_summaries (id) {
|
|
id -> Integer,
|
|
date -> Text,
|
|
contact -> Text,
|
|
summary -> Text,
|
|
message_count -> Integer,
|
|
embedding -> Binary,
|
|
created_at -> BigInt,
|
|
model_version -> Text,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
entities (id) {
|
|
id -> Integer,
|
|
name -> Text,
|
|
entity_type -> Text,
|
|
description -> Text,
|
|
embedding -> Nullable<Binary>,
|
|
confidence -> Float,
|
|
status -> Text,
|
|
created_at -> BigInt,
|
|
updated_at -> BigInt,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
entity_facts (id) {
|
|
id -> Integer,
|
|
subject_entity_id -> Integer,
|
|
predicate -> Text,
|
|
object_entity_id -> Nullable<Integer>,
|
|
object_value -> Nullable<Text>,
|
|
source_photo -> Nullable<Text>,
|
|
source_insight_id -> Nullable<Integer>,
|
|
confidence -> Float,
|
|
status -> Text,
|
|
created_at -> BigInt,
|
|
persona_id -> Text,
|
|
user_id -> Integer,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
entity_photo_links (id) {
|
|
id -> Integer,
|
|
entity_id -> Integer,
|
|
library_id -> Integer,
|
|
rel_path -> Text,
|
|
role -> Text,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
face_detections (id) {
|
|
id -> Integer,
|
|
library_id -> Integer,
|
|
content_hash -> Text,
|
|
rel_path -> Text,
|
|
bbox_x -> Nullable<Float>,
|
|
bbox_y -> Nullable<Float>,
|
|
bbox_w -> Nullable<Float>,
|
|
bbox_h -> Nullable<Float>,
|
|
embedding -> Nullable<Binary>,
|
|
confidence -> Nullable<Float>,
|
|
source -> Text,
|
|
person_id -> Nullable<Integer>,
|
|
status -> Text,
|
|
model_version -> Text,
|
|
created_at -> BigInt,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
favorites (id) {
|
|
id -> Integer,
|
|
userid -> Integer,
|
|
rel_path -> Text,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
image_exif (id) {
|
|
id -> Integer,
|
|
library_id -> Integer,
|
|
rel_path -> Text,
|
|
camera_make -> Nullable<Text>,
|
|
camera_model -> Nullable<Text>,
|
|
lens_model -> Nullable<Text>,
|
|
width -> Nullable<Integer>,
|
|
height -> Nullable<Integer>,
|
|
orientation -> Nullable<Integer>,
|
|
gps_latitude -> Nullable<Float>,
|
|
gps_longitude -> Nullable<Float>,
|
|
gps_altitude -> Nullable<Float>,
|
|
focal_length -> Nullable<Float>,
|
|
aperture -> Nullable<Float>,
|
|
shutter_speed -> Nullable<Text>,
|
|
iso -> Nullable<Integer>,
|
|
date_taken -> Nullable<BigInt>,
|
|
created_time -> BigInt,
|
|
last_modified -> BigInt,
|
|
content_hash -> Nullable<Text>,
|
|
size_bytes -> Nullable<BigInt>,
|
|
phash_64 -> Nullable<BigInt>,
|
|
dhash_64 -> Nullable<BigInt>,
|
|
duplicate_of_hash -> Nullable<Text>,
|
|
duplicate_decided_at -> Nullable<BigInt>,
|
|
date_taken_source -> Nullable<Text>,
|
|
original_date_taken -> Nullable<BigInt>,
|
|
original_date_taken_source -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
libraries (id) {
|
|
id -> Integer,
|
|
name -> Text,
|
|
root_path -> Text,
|
|
created_at -> BigInt,
|
|
enabled -> Bool,
|
|
excluded_dirs -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
location_history (id) {
|
|
id -> Integer,
|
|
timestamp -> BigInt,
|
|
latitude -> Float,
|
|
longitude -> Float,
|
|
accuracy -> Nullable<Integer>,
|
|
activity -> Nullable<Text>,
|
|
activity_confidence -> Nullable<Integer>,
|
|
place_name -> Nullable<Text>,
|
|
place_category -> Nullable<Text>,
|
|
embedding -> Nullable<Binary>,
|
|
created_at -> BigInt,
|
|
source_file -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
personas (id) {
|
|
id -> Integer,
|
|
user_id -> Integer,
|
|
persona_id -> Text,
|
|
name -> Text,
|
|
system_prompt -> Text,
|
|
is_built_in -> Bool,
|
|
include_all_memories -> Bool,
|
|
created_at -> BigInt,
|
|
updated_at -> BigInt,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
persons (id) {
|
|
id -> Integer,
|
|
name -> Text,
|
|
cover_face_id -> Nullable<Integer>,
|
|
entity_id -> Nullable<Integer>,
|
|
created_from_tag -> Bool,
|
|
notes -> Nullable<Text>,
|
|
created_at -> BigInt,
|
|
updated_at -> BigInt,
|
|
is_ignored -> Bool,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
photo_insights (id) {
|
|
id -> Integer,
|
|
library_id -> Integer,
|
|
rel_path -> Text,
|
|
title -> Text,
|
|
summary -> Text,
|
|
generated_at -> BigInt,
|
|
model_version -> Text,
|
|
is_current -> Bool,
|
|
training_messages -> Nullable<Text>,
|
|
approved -> Nullable<Bool>,
|
|
backend -> Text,
|
|
fewshot_source_ids -> Nullable<Text>,
|
|
content_hash -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
search_history (id) {
|
|
id -> Integer,
|
|
timestamp -> BigInt,
|
|
query -> Text,
|
|
search_engine -> Nullable<Text>,
|
|
embedding -> Binary,
|
|
created_at -> BigInt,
|
|
source_file -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
tagged_photo (id) {
|
|
id -> Integer,
|
|
rel_path -> Text,
|
|
tag_id -> Integer,
|
|
created_time -> BigInt,
|
|
content_hash -> Nullable<Text>,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
tags (id) {
|
|
id -> Integer,
|
|
name -> Text,
|
|
created_time -> BigInt,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
users (id) {
|
|
id -> Integer,
|
|
username -> Text,
|
|
password -> Text,
|
|
}
|
|
}
|
|
|
|
diesel::table! {
|
|
video_preview_clips (id) {
|
|
id -> Integer,
|
|
library_id -> Integer,
|
|
rel_path -> Text,
|
|
status -> Text,
|
|
duration_seconds -> Nullable<Float>,
|
|
file_size_bytes -> Nullable<Integer>,
|
|
error_message -> Nullable<Text>,
|
|
created_at -> Text,
|
|
updated_at -> Text,
|
|
}
|
|
}
|
|
|
|
diesel::joinable!(entity_facts -> photo_insights (source_insight_id));
|
|
diesel::joinable!(entity_photo_links -> entities (entity_id));
|
|
diesel::joinable!(entity_photo_links -> libraries (library_id));
|
|
diesel::joinable!(face_detections -> libraries (library_id));
|
|
diesel::joinable!(face_detections -> persons (person_id));
|
|
diesel::joinable!(image_exif -> libraries (library_id));
|
|
diesel::joinable!(personas -> users (user_id));
|
|
diesel::joinable!(persons -> entities (entity_id));
|
|
diesel::joinable!(photo_insights -> libraries (library_id));
|
|
diesel::joinable!(tagged_photo -> tags (tag_id));
|
|
diesel::joinable!(video_preview_clips -> libraries (library_id));
|
|
|
|
diesel::allow_tables_to_appear_in_same_query!(
|
|
calendar_events,
|
|
daily_conversation_summaries,
|
|
entities,
|
|
entity_facts,
|
|
entity_photo_links,
|
|
face_detections,
|
|
favorites,
|
|
image_exif,
|
|
libraries,
|
|
location_history,
|
|
personas,
|
|
persons,
|
|
photo_insights,
|
|
search_history,
|
|
tagged_photo,
|
|
tags,
|
|
users,
|
|
video_preview_clips,
|
|
);
|