MikroORM Best Practices - Entity Relations & Unit of Work Refactoring #267

Closed
opened 2026-01-25 13:00:21 +00:00 by jack · 3 comments
Owner

Übersicht

Umfassende Überarbeitung der MikroORM Entities nach Best Practices. Aktuell fehlen viele Relationen und moderne MikroORM-Features werden nicht genutzt.

Aktuelle Entity-Struktur (16 Entities)

Entity Relationen Status
Session Keine Sollte Hub sein
Observation OneToMany zu CodeSnippet, Document, ObservationLink Gut
CodeSnippet ManyToOne zu Observation Gut
Document ManyToOne zu Observation Gut
ObservationLink ManyToOne zu Observation (2x) Gut
Task Keine ⚠️ Könnte Worker-Relation haben
Summary Keine Fehlt Session-Relation
ClaudeMd Keine Fehlt Session-Relation
UserPrompt Keine Fehlt Session-Relation
ArchivedOutput Nur Field, keine Relation! KRITISCH
RawMessage Keine Fehlt Session + Observation
Repository Keine ⚠️ OK (eigenständig)
DailyStats Keine ⚠️ OK (Aggregat)
TechnologyUsage Keine ⚠️ OK (eigenständig)
ObservationTemplate Keine ⚠️ OK (eigenständig)
ProjectSettings Keine ⚠️ OK (eigenständig)
Achievement Keine ⚠️ OK (eigenständig)

Kritische Probleme

1. ArchivedOutput → Observation: Nur Field, keine Relation!

// AKTUELL (FALSCH):
@Property({ nullable: true })
@Index()
compressed_observation_id?: number;

// SOLLTE SEIN:
@ManyToOne(() => Observation, { nullable: true, ref: true })
compressedObservation?: Ref<Observation>;

Konsequenzen:

  • Keine Foreign-Key-Integrität
  • Kein Lazy-Loading möglich
  • Keine Navigation zur Observation

2. Session als Hub ohne Relationen

Session sollte der zentrale Hub sein, aber hat keine Relationen:

// AKTUELL:
@Entity({ tableName: 'sessions' })
export class Session { ... }

// SOLLTE SEIN:
@Entity({ tableName: 'sessions' })
export class Session {
  // ... existing fields

  @OneToMany(() => UserPrompt, prompt => prompt.session)
  prompts = new Collection<UserPrompt>(this);

  @OneToMany(() => Summary, summary => summary.session)
  summaries = new Collection<Summary>(this);

  @OneToMany(() => ClaudeMd, claudeMd => claudeMd.session)
  claudeMds = new Collection<ClaudeMd>(this);

  @OneToMany(() => RawMessage, msg => msg.session)
  rawMessages = new Collection<RawMessage>(this);
}

3. Fehlende ManyToOne Relationen

Entity Field Sollte Relation sein
UserPrompt content_session_id @ManyToOne(() => Session)
Summary memory_session_id @ManyToOne(() => Session)
ClaudeMd content_session_id @ManyToOne(() => Session)
ClaudeMd memory_session_id @ManyToOne(() => Session)
RawMessage session_id @ManyToOne(() => Session)
RawMessage observation_id @ManyToOne(() => Observation)
CodeSnippet memory_session_id @ManyToOne(() => Session)
Document memory_session_id @ManyToOne(() => Session)

MikroORM Best Practices umsetzen

1. Type-Safe Relations mit Ref<T>

import { Entity, ManyToOne, Ref, ref } from '@mikro-orm/core';

@Entity()
export class UserPrompt {
  @ManyToOne(() => Session, { ref: true })
  session!: Ref<Session>;

  constructor(session: Session) {
    this.session = ref(session);
  }
}

// Usage:
const prompt = await em.findOne(UserPrompt, 1, { populate: ['session'] });
console.log(prompt.session.$.status); // Type-safe access

2. Collections mit orphanRemoval

@Entity()
export class Session {
  @OneToMany(() => UserPrompt, prompt => prompt.session, { 
    orphanRemoval: true  // Prompts werden gelöscht wenn Session gelöscht
  })
  prompts = new Collection<UserPrompt>(this);
}

3. Cascade-Optionen

import { Cascade } from '@mikro-orm/core';

@Entity()
export class Observation {
  @OneToMany(() => CodeSnippet, snippet => snippet.observation, {
    cascade: [Cascade.PERSIST, Cascade.REMOVE],
    orphanRemoval: true
  })
  codeSnippets = new Collection<CodeSnippet>(this);
}

4. Lazy Loading mit populate

// Lazy by default
const session = await em.findOne(Session, 1);
// session.prompts not loaded

// Explicit populate
const session = await em.findOne(Session, 1, {
  populate: ['prompts', 'summaries']
});

// Or populate later
await em.populate(session, ['prompts']);

5. Unit of Work Pattern (bereits korrekt)

// Gut: Änderungen sammeln und mit flush() committen
const session = await em.findOne(Session, 1);
session.status = 'completed';
const summary = new Summary(session);
em.persist(summary);
await em.flush(); // Atomic transaction

Vorgeschlagene Entity-Struktur

Session (Hub)
├── @OneToMany UserPrompt
├── @OneToMany Summary  
├── @OneToMany ClaudeMd
├── @OneToMany RawMessage
└── (indirekt über Observation)
    
Observation
├── @OneToMany CodeSnippet (✅ vorhanden)
├── @OneToMany Document (✅ vorhanden)
├── @OneToMany ObservationLink (source) (✅ vorhanden)
├── @OneToMany ObservationLink (target) (✅ vorhanden)
└── @OneToMany ArchivedOutput (NEU)

Task
└── (keine Relationen nötig - eigenständig)

Standalone Entities (OK):
- Repository
- DailyStats
- TechnologyUsage
- ObservationTemplate
- ProjectSettings
- Achievement

Migration

Schritt 1: Neue Relationen definieren (ohne DB-Änderung)

// Mit persist: false werden keine neuen Columns erstellt
@ManyToOne(() => Session, { 
  fieldName: 'content_session_id',  // Existing column
  persist: false  // Don't create FK constraint yet
})
session?: Ref<Session>;

Schritt 2: Foreign Key Constraints hinzufügen

// In einer späteren Migration
@ManyToOne(() => Session, { 
  fieldName: 'content_session_id',
  ref: true
})
session!: Ref<Session>;

Schritt 3: SQLite FK Enforcement

// mikro-orm.config.ts
export default defineConfig({
  // ...
  driverOptions: {
    connection: {
      // Enable foreign keys in SQLite
      pragma: 'foreign_keys = ON'
    }
  }
});

Implementierungs-Checkliste

Phase 1: ArchivedOutput Fix (Kritisch)

  • compressed_observation_id zu @ManyToOne Relation konvertieren
  • Migration erstellen für FK Constraint
  • Repository-Methoden anpassen

Phase 2: Session als Hub

  • @OneToMany Collections zu Session hinzufügen
  • UserPrompt: @ManyToOne Session
  • Summary: @ManyToOne Session
  • ClaudeMd: @ManyToOne Session (2x)
  • RawMessage: @ManyToOne Session + Observation

Phase 3: Type-Safe Relations

  • Ref<T> Wrapper für alle ManyToOne Relationen
  • ref() Helper in Constructors
  • Populate-Hints in Repositories

Phase 4: Cascade & Orphan Removal

  • Cascade.PERSIST für Child-Entities
  • orphanRemoval für abhängige Entities
  • Cascade.REMOVE wo sinnvoll

Phase 5: Index-Optimierung

  • Composite Indexes für häufige Queries prüfen
  • Redundante Indexes entfernen

Offene Fragen

  1. SQLite FK Constraints: Sollen Foreign Keys in SQLite enforced werden? (Performance vs. Integrität)
  2. Backwards Compatibility: Wie Migration ohne Datenverlust?
  3. Lazy vs. Eager: Default Loading Strategy?

Referenzen

Aufwand

  • Phase 1: ~2h (kritisch, sofort)
  • Phase 2: ~4h (wichtig)
  • Phase 3: ~3h (nice-to-have)
  • Phase 4: ~2h (Optimierung)
  • Phase 5: ~1h (Optimierung)
## Übersicht Umfassende Überarbeitung der MikroORM Entities nach Best Practices. Aktuell fehlen viele Relationen und moderne MikroORM-Features werden nicht genutzt. ## Aktuelle Entity-Struktur (16 Entities) | Entity | Relationen | Status | |--------|------------|--------| | Session | Keine | ❌ Sollte Hub sein | | Observation | ✅ OneToMany zu CodeSnippet, Document, ObservationLink | ✅ Gut | | CodeSnippet | ✅ ManyToOne zu Observation | ✅ Gut | | Document | ✅ ManyToOne zu Observation | ✅ Gut | | ObservationLink | ✅ ManyToOne zu Observation (2x) | ✅ Gut | | Task | Keine | ⚠️ Könnte Worker-Relation haben | | Summary | Keine | ❌ Fehlt Session-Relation | | ClaudeMd | Keine | ❌ Fehlt Session-Relation | | UserPrompt | Keine | ❌ Fehlt Session-Relation | | ArchivedOutput | Nur Field, keine Relation! | ❌ **KRITISCH** | | RawMessage | Keine | ❌ Fehlt Session + Observation | | Repository | Keine | ⚠️ OK (eigenständig) | | DailyStats | Keine | ⚠️ OK (Aggregat) | | TechnologyUsage | Keine | ⚠️ OK (eigenständig) | | ObservationTemplate | Keine | ⚠️ OK (eigenständig) | | ProjectSettings | Keine | ⚠️ OK (eigenständig) | | Achievement | Keine | ⚠️ OK (eigenständig) | ## Kritische Probleme ### 1. ArchivedOutput → Observation: Nur Field, keine Relation! ```typescript // AKTUELL (FALSCH): @Property({ nullable: true }) @Index() compressed_observation_id?: number; // SOLLTE SEIN: @ManyToOne(() => Observation, { nullable: true, ref: true }) compressedObservation?: Ref<Observation>; ``` **Konsequenzen:** - Keine Foreign-Key-Integrität - Kein Lazy-Loading möglich - Keine Navigation zur Observation ### 2. Session als Hub ohne Relationen Session sollte der zentrale Hub sein, aber hat keine Relationen: ```typescript // AKTUELL: @Entity({ tableName: 'sessions' }) export class Session { ... } // SOLLTE SEIN: @Entity({ tableName: 'sessions' }) export class Session { // ... existing fields @OneToMany(() => UserPrompt, prompt => prompt.session) prompts = new Collection<UserPrompt>(this); @OneToMany(() => Summary, summary => summary.session) summaries = new Collection<Summary>(this); @OneToMany(() => ClaudeMd, claudeMd => claudeMd.session) claudeMds = new Collection<ClaudeMd>(this); @OneToMany(() => RawMessage, msg => msg.session) rawMessages = new Collection<RawMessage>(this); } ``` ### 3. Fehlende ManyToOne Relationen | Entity | Field | Sollte Relation sein | |--------|-------|---------------------| | UserPrompt | `content_session_id` | `@ManyToOne(() => Session)` | | Summary | `memory_session_id` | `@ManyToOne(() => Session)` | | ClaudeMd | `content_session_id` | `@ManyToOne(() => Session)` | | ClaudeMd | `memory_session_id` | `@ManyToOne(() => Session)` | | RawMessage | `session_id` | `@ManyToOne(() => Session)` | | RawMessage | `observation_id` | `@ManyToOne(() => Observation)` | | CodeSnippet | `memory_session_id` | `@ManyToOne(() => Session)` | | Document | `memory_session_id` | `@ManyToOne(() => Session)` | ## MikroORM Best Practices umsetzen ### 1. Type-Safe Relations mit `Ref<T>` ```typescript import { Entity, ManyToOne, Ref, ref } from '@mikro-orm/core'; @Entity() export class UserPrompt { @ManyToOne(() => Session, { ref: true }) session!: Ref<Session>; constructor(session: Session) { this.session = ref(session); } } // Usage: const prompt = await em.findOne(UserPrompt, 1, { populate: ['session'] }); console.log(prompt.session.$.status); // Type-safe access ``` ### 2. Collections mit `orphanRemoval` ```typescript @Entity() export class Session { @OneToMany(() => UserPrompt, prompt => prompt.session, { orphanRemoval: true // Prompts werden gelöscht wenn Session gelöscht }) prompts = new Collection<UserPrompt>(this); } ``` ### 3. Cascade-Optionen ```typescript import { Cascade } from '@mikro-orm/core'; @Entity() export class Observation { @OneToMany(() => CodeSnippet, snippet => snippet.observation, { cascade: [Cascade.PERSIST, Cascade.REMOVE], orphanRemoval: true }) codeSnippets = new Collection<CodeSnippet>(this); } ``` ### 4. Lazy Loading mit `populate` ```typescript // Lazy by default const session = await em.findOne(Session, 1); // session.prompts not loaded // Explicit populate const session = await em.findOne(Session, 1, { populate: ['prompts', 'summaries'] }); // Or populate later await em.populate(session, ['prompts']); ``` ### 5. Unit of Work Pattern (bereits korrekt) ```typescript // Gut: Änderungen sammeln und mit flush() committen const session = await em.findOne(Session, 1); session.status = 'completed'; const summary = new Summary(session); em.persist(summary); await em.flush(); // Atomic transaction ``` ## Vorgeschlagene Entity-Struktur ``` Session (Hub) ├── @OneToMany UserPrompt ├── @OneToMany Summary ├── @OneToMany ClaudeMd ├── @OneToMany RawMessage └── (indirekt über Observation) Observation ├── @OneToMany CodeSnippet (✅ vorhanden) ├── @OneToMany Document (✅ vorhanden) ├── @OneToMany ObservationLink (source) (✅ vorhanden) ├── @OneToMany ObservationLink (target) (✅ vorhanden) └── @OneToMany ArchivedOutput (NEU) Task └── (keine Relationen nötig - eigenständig) Standalone Entities (OK): - Repository - DailyStats - TechnologyUsage - ObservationTemplate - ProjectSettings - Achievement ``` ## Migration ### Schritt 1: Neue Relationen definieren (ohne DB-Änderung) ```typescript // Mit persist: false werden keine neuen Columns erstellt @ManyToOne(() => Session, { fieldName: 'content_session_id', // Existing column persist: false // Don't create FK constraint yet }) session?: Ref<Session>; ``` ### Schritt 2: Foreign Key Constraints hinzufügen ```typescript // In einer späteren Migration @ManyToOne(() => Session, { fieldName: 'content_session_id', ref: true }) session!: Ref<Session>; ``` ### Schritt 3: SQLite FK Enforcement ```typescript // mikro-orm.config.ts export default defineConfig({ // ... driverOptions: { connection: { // Enable foreign keys in SQLite pragma: 'foreign_keys = ON' } } }); ``` ## Implementierungs-Checkliste ### Phase 1: ArchivedOutput Fix (Kritisch) - [ ] `compressed_observation_id` zu `@ManyToOne` Relation konvertieren - [ ] Migration erstellen für FK Constraint - [ ] Repository-Methoden anpassen ### Phase 2: Session als Hub - [ ] `@OneToMany` Collections zu Session hinzufügen - [ ] UserPrompt: `@ManyToOne` Session - [ ] Summary: `@ManyToOne` Session - [ ] ClaudeMd: `@ManyToOne` Session (2x) - [ ] RawMessage: `@ManyToOne` Session + Observation ### Phase 3: Type-Safe Relations - [ ] `Ref<T>` Wrapper für alle ManyToOne Relationen - [ ] `ref()` Helper in Constructors - [ ] Populate-Hints in Repositories ### Phase 4: Cascade & Orphan Removal - [ ] Cascade.PERSIST für Child-Entities - [ ] orphanRemoval für abhängige Entities - [ ] Cascade.REMOVE wo sinnvoll ### Phase 5: Index-Optimierung - [ ] Composite Indexes für häufige Queries prüfen - [ ] Redundante Indexes entfernen ## Offene Fragen 1. **SQLite FK Constraints:** Sollen Foreign Keys in SQLite enforced werden? (Performance vs. Integrität) 2. **Backwards Compatibility:** Wie Migration ohne Datenverlust? 3. **Lazy vs. Eager:** Default Loading Strategy? ## Referenzen - [MikroORM Relationships](https://mikro-orm.io/docs/relationships) - [MikroORM Type-Safe Relations](https://mikro-orm.io/docs/type-safe-relations) - [MikroORM Cascading](https://mikro-orm.io/docs/cascading) - [MikroORM Unit of Work](https://mikro-orm.io/docs/unit-of-work) - [MikroORM Collections](https://mikro-orm.io/docs/collections) ## Aufwand - Phase 1: ~2h (kritisch, sofort) - Phase 2: ~4h (wichtig) - Phase 3: ~3h (nice-to-have) - Phase 4: ~2h (Optimierung) - Phase 5: ~1h (Optimierung)
Author
Owner

Bezug zu #262 (SQLite vs. Alternative Datenbanken)

Bei der Entity-Überarbeitung sollten wir beachten, dass wir laut #262 möglicherweise PostgreSQL als Alternative anbieten wollen.

Database-Agnostische Entities

MikroORM unterstützt beides - wir sollten die Entities so gestalten, dass sie auf beiden DBs funktionieren:

// Gut: Abstrakte Typen verwenden
@Property({ type: 'text' })  // Funktioniert auf SQLite und PostgreSQL
content!: string;

// Vermeiden: DB-spezifische Typen
@Property({ columnType: 'TEXT' })  // SQLite-spezifisch

SQLite FK Constraints

SQLite hat standardmäßig keine Foreign Key Enforcement! Das muss explizit aktiviert werden:

// mikro-orm.config.ts
export default defineConfig({
  driver: SqliteDriver,
  // FK Enforcement in SQLite aktivieren
  pool: {
    afterCreate: (conn, done) => {
      conn.run('PRAGMA foreign_keys = ON', done);
    }
  }
});

Bei PostgreSQL sind FKs automatisch enforced.

MikroORM Features pro Datenbank

Feature SQLite PostgreSQL
Foreign Keys Manuell aktivieren Automatisch
JSON Columns ⚠️ Als TEXT Native JSONB
Array Columns Nicht unterstützt Native
Full-Text Search ⚠️ FTS5 Extension Native tsvector
Transactions
Concurrent Writes ⚠️ Eingeschränkt

Empfehlung

  1. Entities database-agnostisch gestalten - Keine DB-spezifischen Column-Types
  2. JSON-Felder als type: 'json' - MikroORM serialisiert automatisch
  3. FK Constraints optional machen - persist: false für SQLite, echte FKs für PostgreSQL
  4. Migrations getrennt halten - SQLite und PostgreSQL Migrations

Konfiguration per Environment

// mikro-orm.config.ts
const driver = process.env.DB_TYPE === 'postgresql' 
  ? PostgreSqlDriver 
  : SqliteDriver;

export default defineConfig({
  driver,
  dbName: process.env.DB_TYPE === 'postgresql'
    ? process.env.DATABASE_URL
    : '~/.claude-mem/claude-mem.db',
  // ... rest
});

So können wir die MikroORM-Überarbeitung machen und sind für beide Datenbanken vorbereitet.

## Bezug zu #262 (SQLite vs. Alternative Datenbanken) Bei der Entity-Überarbeitung sollten wir beachten, dass wir laut #262 möglicherweise PostgreSQL als Alternative anbieten wollen. ### Database-Agnostische Entities MikroORM unterstützt beides - wir sollten die Entities so gestalten, dass sie auf beiden DBs funktionieren: ```typescript // Gut: Abstrakte Typen verwenden @Property({ type: 'text' }) // Funktioniert auf SQLite und PostgreSQL content!: string; // Vermeiden: DB-spezifische Typen @Property({ columnType: 'TEXT' }) // SQLite-spezifisch ``` ### SQLite FK Constraints SQLite hat standardmäßig **keine Foreign Key Enforcement**! Das muss explizit aktiviert werden: ```typescript // mikro-orm.config.ts export default defineConfig({ driver: SqliteDriver, // FK Enforcement in SQLite aktivieren pool: { afterCreate: (conn, done) => { conn.run('PRAGMA foreign_keys = ON', done); } } }); ``` **Bei PostgreSQL** sind FKs automatisch enforced. ### MikroORM Features pro Datenbank | Feature | SQLite | PostgreSQL | |---------|--------|------------| | Foreign Keys | Manuell aktivieren | ✅ Automatisch | | JSON Columns | ⚠️ Als TEXT | ✅ Native JSONB | | Array Columns | ❌ Nicht unterstützt | ✅ Native | | Full-Text Search | ⚠️ FTS5 Extension | ✅ Native tsvector | | Transactions | ✅ | ✅ | | Concurrent Writes | ⚠️ Eingeschränkt | ✅ | ### Empfehlung 1. **Entities database-agnostisch gestalten** - Keine DB-spezifischen Column-Types 2. **JSON-Felder als `type: 'json'`** - MikroORM serialisiert automatisch 3. **FK Constraints optional machen** - `persist: false` für SQLite, echte FKs für PostgreSQL 4. **Migrations getrennt halten** - SQLite und PostgreSQL Migrations ### Konfiguration per Environment ```typescript // mikro-orm.config.ts const driver = process.env.DB_TYPE === 'postgresql' ? PostgreSqlDriver : SqliteDriver; export default defineConfig({ driver, dbName: process.env.DB_TYPE === 'postgresql' ? process.env.DATABASE_URL : '~/.claude-mem/claude-mem.db', // ... rest }); ``` So können wir die MikroORM-Überarbeitung machen und sind für beide Datenbanken vorbereitet.
Author
Owner

Phase 1 Complete: ArchivedOutput → Observation Relation

Implemented

  • Converted compressed_observation_id from plain field to proper @ManyToOne relation
  • Added Ref<Observation> for type-safe lazy loading
  • Added inverse @OneToMany archivedOutputs collection on Observation
  • Updated repository to use ref() + getReference() pattern
  • Preserved backward compatibility with existing compressed_observation_id column

Commit

24215b7 - refactor(database): add ManyToOne relation ArchivedOutput → Observation


Phase 2 Challenge: Session String FK Issue

Analysis revealed a schema complexity:

  • Session has numeric PK (id) but unique string content_session_id
  • Related entities reference Session via string IDs, not the numeric PK:
    • UserPrompt.content_session_id → string
    • Summary.memory_session_id → string
    • ClaudeMd.content_session_id / memory_session_id → strings
    • RawMessage.session_id → string

MikroORM relations typically require pointing to the primary key. Options:

Option Pros Cons
Change FK columns to numeric Session.id Proper relations, FK integrity Data migration required, breaks existing queries
Use @Formula / virtual props No schema change No lazy loading, no cascades
Keep as-is with manual joins No changes No ORM relation benefits

Recommendation

Phase 2 should be deferred until:

  1. Decision made on #262 (SQLite vs PostgreSQL)
  2. Proper migration strategy planned for converting string references to numeric FKs

Phase 1 (ArchivedOutput) was the critical fix since it's new code with no legacy data constraints.

## Phase 1 Complete: ArchivedOutput → Observation Relation ### Implemented - Converted `compressed_observation_id` from plain field to proper `@ManyToOne` relation - Added `Ref<Observation>` for type-safe lazy loading - Added inverse `@OneToMany archivedOutputs` collection on Observation - Updated repository to use `ref()` + `getReference()` pattern - Preserved backward compatibility with existing `compressed_observation_id` column ### Commit `24215b7` - refactor(database): add ManyToOne relation ArchivedOutput → Observation --- ## Phase 2 Challenge: Session String FK Issue Analysis revealed a schema complexity: - `Session` has numeric PK (`id`) but unique string `content_session_id` - Related entities reference Session via **string IDs**, not the numeric PK: - `UserPrompt.content_session_id` → string - `Summary.memory_session_id` → string - `ClaudeMd.content_session_id` / `memory_session_id` → strings - `RawMessage.session_id` → string **MikroORM relations typically require pointing to the primary key.** Options: | Option | Pros | Cons | |--------|------|------| | Change FK columns to numeric Session.id | Proper relations, FK integrity | Data migration required, breaks existing queries | | Use `@Formula` / virtual props | No schema change | No lazy loading, no cascades | | Keep as-is with manual joins | No changes | No ORM relation benefits | ### Recommendation Phase 2 should be deferred until: 1. Decision made on #262 (SQLite vs PostgreSQL) 2. Proper migration strategy planned for converting string references to numeric FKs Phase 1 (ArchivedOutput) was the critical fix since it's new code with no legacy data constraints.
Author
Owner

All Phases Complete

Phase 1: ArchivedOutput Fix

  • 24215b7 - Convert compressed_observation_id to proper @ManyToOne relation
  • Added Ref<Observation> for type-safe access
  • Added inverse @OneToMany on Observation

Phase 2: Session as Hub

  • cfa8453 - Add Session as central hub with OneToMany collections
  • Virtual relations for UserPrompt, Summary, ClaudeMd, RawMessage
  • Uses persist: false + referencedColumnNames for string FK compatibility
  • Added RawMessage → Observation relation

Phase 3: Type-Safe Relations

  • All ManyToOne relations use Ref<T> wrapper
  • Enables type-safe lazy loading

Phase 4: Cascade & Orphan Removal

  • 232f400 - Add cascade options to Observation and Session
  • CodeSnippets, Documents, ObservationLinks: cascade + orphanRemoval
  • Session children: cascade persist + orphanRemoval

Phase 5: Index Optimization

  • 74db210 - Add composite indexes for common queries
  • Summary: (memory_session_id, created_at_epoch)
  • RawMessage: (session_id, processed), (project, created_at_epoch)

Summary

All 5 phases implemented with backwards compatibility preserved. The virtual relation pattern allows ORM navigation without schema changes while keeping existing FK columns for queries.

## All Phases Complete ### Phase 1: ArchivedOutput Fix ✅ - `24215b7` - Convert `compressed_observation_id` to proper `@ManyToOne` relation - Added `Ref<Observation>` for type-safe access - Added inverse `@OneToMany` on Observation ### Phase 2: Session as Hub ✅ - `cfa8453` - Add Session as central hub with OneToMany collections - Virtual relations for UserPrompt, Summary, ClaudeMd, RawMessage - Uses `persist: false` + `referencedColumnNames` for string FK compatibility - Added RawMessage → Observation relation ### Phase 3: Type-Safe Relations ✅ - All ManyToOne relations use `Ref<T>` wrapper - Enables type-safe lazy loading ### Phase 4: Cascade & Orphan Removal ✅ - `232f400` - Add cascade options to Observation and Session - CodeSnippets, Documents, ObservationLinks: cascade + orphanRemoval - Session children: cascade persist + orphanRemoval ### Phase 5: Index Optimization ✅ - `74db210` - Add composite indexes for common queries - Summary: `(memory_session_id, created_at_epoch)` - RawMessage: `(session_id, processed)`, `(project, created_at_epoch)` ### Summary All 5 phases implemented with backwards compatibility preserved. The virtual relation pattern allows ORM navigation without schema changes while keeping existing FK columns for queries.
jack closed this issue 2026-01-25 16:43:44 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
customable/claude-mem#267
No description provided.