Skip to content
Projekte

tocreator/tostore has 67 GitHub stars and 11 forks. GitHot measured 0 stars in 24 hours and 0 in 7 days. Last checked 2026-11-15 04:06 UTC. Last push 2026-11-05.

So werden diese Zahlen gemessen

What you can do

  • Declarative schema changes—automatic refactoring, no manual versioning
  • Key rotation, schema migration, massive refactoring—all online, business-transparent
  • Built-in status spec for automated O&M and Agent recognition
  • Supports autonomous evolution and continuous iteration without interrupting service

Gemessener Sternzuwachs

in 24h
0
in 7 days
0
in 15 days
Verlauf wird gesammelt
in 1 month
Verlauf wird gesammelt

Nettoänderungen in überlappenden Zeiträumen, keine Tagessummen. Fehlende Messwerte bleiben leer. Sterne zeigen Interesse, nicht Qualität.

Über uns

Data engine: AI vector & SQL/NoSQL database. Key-value/KV, embeddings, BM25 full-text, hybrid & semantic search for RAG/LLM. Local storage, TTL/LRU cache, offline encrypted.

KI

doc/translations/de/README.md

Original

ToStore

pub package Pub Points Pub Likes Monthly Downloads

License Platform Architecture

English | 简体中文 | 日本語 | 한국어 | Español | Português (Brasil) | Русский | Deutsch | Français | Italiano | Türkçe

Schnelle Navigation

Warum ToStore wählen?

ToStore ist eine moderne Daten-Engine für die AGI-Ära und Edge-Intelligence-Szenarien. Basierend auf einer Self-Routing-Knotenarchitektur bietet sie Knoten hohe Autonomie und elastische horizontale Skalierbarkeit, wobei Leistung und Datenumfang logisch entkoppelt werden — Edge- und Cloud-Einzelknoten sind auf strukturierte Tabellen im Billionen-Maßstab ausgelegt; Vektor- und Sparse-Text-Abruf zielen ebenfalls auf 10B+ ab.

Runtime-Modellierung und nicht-blockierende Ausführungspfade halten die Architekturentwicklung stets online und vollständig transparent für den Geschäftsbetrieb – deklarativer Schemawechsel, Rotation der Datenkodierungsschlüssel und umfangreiches Daten-Refactoring erfolgen nahtlos online. Es unterstützt Agenten und automatisiertes O&M mit Tabellenerstellung zur Laufzeit, dynamischer Speicherevolution sowie deren autonomer Evolution und kontinuierlicher Iteration — ohne Dienstunterbrechung.

Es vereint relationale strukturierte Daten, dichte Vektoren, neuronale Sparse-Vektoren und unstrukturierten Text in einer Engine, mit integriertem lexikalischem Textabruf, dichtem Vektorabruf, neuronalem Sparse-Abruf und mehrwegiger Recall-Fusion. Enterprise-Funktionen umfassen ACID-Transaktionen, komplexe relationale Abfragen (JOINs, kaskadierende Fremdschlüssel), TTL auf Tabellenebene, Aggregationen sowie verteilte Primärschlüsselalgorithmen, atomare Ausdrücke, Verschlüsselung, Multi-Space-Isolierung und Selbstheilung.

Da sich die Datenverarbeitung zunehmend in Richtung Edge Intelligence verlagert, sind Geräte nicht mehr nur „Inhaltsanzeigen". Sie sind intelligente Knoten für lokale Erzeugung, Umweltbewusstsein, Echtzeitentscheidungen und koordinierten Datenfluss. ToStore bietet dem Edge verteilte Funktionen für riesige Datensätze und komplexe lokale KI-Generierung. Die tiefe intelligente Zusammenarbeit zwischen Edge- und Cloud-Knoten schafft eine zuverlässige Datengrundlage für multimodale Interaktion, Langzeitgedächtnis, RAG und lokale Wissensbasen, Hybridabruf, räumliche Modellierung, autonome Edge-Zusammenarbeit und ähnliche Szenarien.

Hauptmerkmale

  • 🤖 Runtime-Evolution und intelligentes O&M

    • Deklarative Schemaänderungen — automatisches Refactoring, ohne manuelle Versionsverwaltung
    • Schlüsselrotation, Schemamigration, umfangreiches Refactoring — alles online, geschäftstransparent
    • Integrierte Statusspezifikation für automatisiertes O&M und Agent-Erkennung
    • Unterstützt autonome Evolution und kontinuierliche Iteration ohne Dienstunterbrechung
  • 🧠 Self-Routing-Verteilte Architektur

    • Self-Routing-Knotenarchitektur, die physische Adressierung vom Datenumfang entkoppelt
    • Hochautonome Knoten arbeiten zusammen für eine flexible Datentopologie
    • Elastische horizontale Skalierung; Einzelknoten: Tabellen Billionen-, Vektoren 10B+-Maßstab
  • 🌐 Einheitliche plattformübergreifende Daten-Engine

    • Einheitliche API für Mobil-, Desktop-, Web- und Serverumgebungen
    • Abdeckung relationaler Daten, dichter / neuronaler Sparse-Vektoren und Textabruf
    • Vollständige Datenpipeline vom lokalen Speicher bis zur Edge-Cloud-Zusammenarbeit
  • 🔍 Strukturierte Abfragen und Hybridabruf

    • Komplexe Prädikate, JOINs, Aggregationen, TTL auf Tabellenebene und Kapazitätsbereinigung (FIFO / LRU)
    • Lexikalischer Text, dichte Vektoren und neuronaler Sparse-Abruf in derselben Abfragekette
    • Mehrwegiges Recall-Fusion-Ranking; Scores und Kanaldiagnosen im Ergebnis
  • ⚡ Parallele Ausführung und Ressourcenplanung

    • Kaltstart konstant ~35 ms, unabhängig vom Datenumfang
    • Ressourcenbewusste Lastplanung; parallele Mehrknoten-Aufgabenzerlegung
    • Time-Slicing hält UI-Animationen auch unter hoher Last flüssig
  • 🔐 Offline-First & Privatsphäre

    • Daten gerätegebunden und offline nutzbar halten — weniger Cloud-Abhängigkeit und geringere Expositionsfläche
    • Multi-Space-Isolierung mit optionaler globaler Freigabe — bereit für Multi-User / Multi-Tenant
    • Integrierte ChaCha20-Poly1305- und AES-256-GCM-Verschlüsselung
    • Validiert durch mehrere komplexe Disaster-Recovery-Szenarien

Installation

[!IMPORTANT] Upgrade von v2.x? Bitte lesen Sie den v3.x-Upgrade-Leitfaden für wichtige Migrationsschritte und wichtige Änderungen.

Fügen Sie tostore zu Ihrem pubspec.yaml hinzu:

dependencies:
  tostore: any # Please use the latest version

For AI Coding Assistants

Wenn Sie ToStore-Clientcode mit einem KI-Assistenten erzeugen, geben Sie ihm das Einzeldatei-Korpus llms-full.txt — z. B. @llms-full.txt in der IDE, Datei hochladen/einfügen oder die raw URL in die Assistenten-Dokumentation aufnehmen. Die Datei enthält API-Signaturen, Einschränkungen und Anti-Patterns, damit das Modell zur echten öffentlichen Oberfläche passt. Index: llms.txt.

Schnellstart

[!TIP] Wie sollten Sie einen Speichermodus auswählen?

  1. Schlüsselwertmodus (KV): Am besten für Konfigurationszugriff, verteilte Statusverwaltung oder JSON-Datenspeicherung geeignet. Dies ist der schnellste Weg, um loszulegen.
  2. Strukturierter Tabellenmodus: Am besten für Kerngeschäftsdaten geeignet, die komplexe Abfragen, Einschränkungsvalidierung oder umfangreiche Datenverwaltung erfordern. Indem Sie die Integritätslogik in die Engine integrieren, können Sie die Entwicklungs- und Wartungskosten auf der Anwendungsebene erheblich senken.
  3. Speichermodus: Am besten für temporäre Berechnungen, Unit-Tests, begrenzte In-Memory-Caches (TTL und FIFO / LRU Kapazitätsbereinigung) oder ultraschnelle globale Zustandsverwaltung. Mit globalen Abfragen und watch-Listenern können Sie die Anwendungsinteraktion umgestalten, ohne einen Stapel globaler Variablen verwalten zu müssen.

Schlüsselwertspeicher (KV)

Dieser Modus eignet sich, wenn Sie keine vordefinierten strukturierten Tabellen benötigen. Es ist einfach, praktisch und wird von einer leistungsstarken Speicher-Engine unterstützt. Seine effiziente Indexierungsarchitektur sorgt dafür, dass die Abfrageleistung selbst auf normalen Mobilgeräten bei sehr großen Datenmengen äußerst stabil und extrem reaktionsfähig bleibt. Daten in verschiedenen Spaces werden natürlich isoliert, während auch die globale Freigabe unterstützt wird.

// Initialize the database
final db = await ToStore.open();

// Set key-value pairs (supports String, int, bool, double, Map, List, Json, and more)
await db.setValue('user_profile', {
  'name': 'John',
  'age': 25,
});

// Switch space - isolate data for different users
await db.switchSpace(spaceName: 'user_123');

// Set a globally shared variable (isGlobal: true enables cross-space sharing, such as login state)
await db.setValue('current_user', 'John', isGlobal: true);

// Automatic expiration cleanup (TTL)
// Supports either a relative lifetime (ttl) or an absolute expiration time (expiresAt)
await db.setValue('temp_config', 'value', ttl: Duration(hours: 2));
await db.setValue('session_token', 'abc', expiresAt: DateTime(2026, 2, 31));

// Read data
final profile = await db.getValue('user_profile'); // Map<String, dynamic>

// Listen for real-time value changes (useful for refreshing local UI without extra state frameworks)
db.watchValue('current_user', isGlobal: true).listen((value) {
  print('Logged-in user changed to: $value');
});

// Listen to multiple keys at once
db.watchValues(['current_user', 'login_status']).listen((map) {
  print('Multiple config values were updated: $map');
});

// Remove data
await db.removeValue('current_user');

[!TIP] Benötigen Sie weitere Key-Value-Funktionen? Für fortgeschrittene Operationen wie typsicheres Lesen (getInt, getBool), atomares Inkrementieren, Präfixsuche, verkettete paginierte Datensatzabfragen (db.kv.query()) und Key-Space-Exploration lesen Sie bitte Fortgeschrittene Key-Value-Operationen (db.kv).

Beispiel für die automatische Aktualisierung der Flutter-Benutzeroberfläche

In Flutter erhalten Sie mit StreamBuilder plus watchValue einen sehr prägnanten reaktiven Aktualisierungsfluss:

StreamBuilder(
  // When listening to a global variable, remember to set isGlobal: true
  stream: db.watchValue('current_user', isGlobal: true),
  builder: (context, snapshot) {
    // snapshot.data is the latest value of 'current_user' in KV storage
    final user = snapshot.data ?? 'Not logged in';
    return Text('Current user: $user');
  },
)

Strukturierter Tabellenmodus

CRUD für strukturierte Tabellen erfordert, dass das Schema im Voraus erstellt wird (siehe Schemadefinition). Empfohlene Integrationsansätze für verschiedene Szenarien:

// 1. Initialize the database
final db = await ToStore.open();

// 2. Insert data (prepare some base records)
final result = await db.insert('users', {
  'username': 'John',
  'email': '[email protected]',
  'age': 25,
});

// Unified operation result model: DbResult
// It is recommended to check hasErrors
if (!result.hasErrors) {
  print('Insert succeeded, generated primary key ID: ${result.firstPrimaryKey}');
} else {
  print('Insert failed: ${result.message}');
}

// Chained query (see [Query Operators](#query-operators); supports =, !=, >, <, LIKE, IN, and more)
final users = await db.query('users')
    .where('age', '>', 20)
    .where('username', 'like', '%John%')
    .orderByDesc('age')
    .limit(20);

// Update and delete
await db.update('users', {'age': 26}).where('username', '=', 'John');
await db.delete('users').where('username', '=', 'John');

// Real-time listening (see [Reactive Query](#reactive-query) for more details)
db.query('users').where('age', '>', 18).watch().listen((users) {
  print('Users matching the condition have changed: $users');
});

// Pair with Flutter StreamBuilder for automatic local UI refresh
StreamBuilder(
  stream: db.query('users').where('age', '>', 18).watch(),
  builder: (context, snapshot) {
    final users = snapshot.data ?? [];
    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) => Text(users[index]['username']),
    );
  },
);

Speichermodus

Für Szenarien wie Caching, temporäre Berechnungen oder Arbeitslasten, die keine Persistenz auf der Festplatte benötigen, können Sie eine reine In-Memory-Datenbank über ToStore.memory() initialisieren. In diesem Modus verbleiben alle Daten, einschließlich Schemata, Indizes und Schlüssel-Wert-Paare, vollständig im Speicher, um eine maximale Lese-/Schreibleistung zu gewährleisten.

Der Speichermodus unterstützt mehrere Eviction-Strategien: zeitbasiertes TTL auf Tabellenebene / KV-TTL sowie mengen-/größenbasierte Kapazitätsbereinigung (FIFO / LRU)—einzeln oder kombiniert, damit Hot-Daten planmäßig ablaufen oder begrenzt bleiben. Ideal für Sitzungscaches, Abfrageergebnis-Pools und speicherempfindliche Pfade.

💡 Funktioniert auch als Global State Management

Sie benötigen keinen Haufen globaler Variablen oder ein schwergewichtiges Zustandsverwaltungs-Framework. Durch die Kombination des Speichermodus mit watchValue oder watch() können Sie eine vollautomatische Aktualisierung der Benutzeroberfläche über Widgets und Seiten hinweg erreichen. Es behält die leistungsstarken Abruffähigkeiten einer Datenbank bei und bietet Ihnen gleichzeitig ein reaktives Erlebnis, das weit über gewöhnliche Variablen hinausgeht, was es ideal für den Anmeldestatus, die Live-Konfiguration oder globale Nachrichtenzähler macht.

[!CAUTION] Hinweis: Im reinen Speichermodus erstellte Daten gehen vollständig verloren, nachdem die App geschlossen oder neu gestartet wird. Verwenden Sie es nicht für Kerngeschäftsdaten.

// Reine In-Memory-DB — TTL und/oder FIFO/LRU-Kapazität nach Bedarf
final memDb = await ToStore.memory(
  config: DataStoreConfig(
    kvCapacityConfig: CapacityConfig(
      maxEntries: 5000,
      policy: CapacityEvictionPolicy.lru,
    ),
  ),
);

// Set a global state value (for example: unread message count)
await memDb.setValue('unread_count', 5, isGlobal: true);

// Listen from anywhere in the UI without passing parameters around
memDb.watchValue<int>('unread_count', isGlobal: true).listen((count) {
  print('UI automatically sensed the message count change: $count');
});

// All CRUD, KV access, and vector search run at in-memory speed
await memDb.insert('active_users', {'name': 'Marley', 'status': 'online'});

Schemadefinition

Definieren Sie es einmal und überlassen Sie die Engine die automatisierte End-to-End-Governance, sodass für Ihre Anwendung keine aufwändige Validierungswartung mehr erforderlich ist.

Die folgenden mobilen, serverseitigen und Agent-Beispiele verwenden alle das hier definierte appSchemas wieder.

TableSchema-Übersicht

const userSchema = TableSchema(
  name: 'users', // Table name, required
  tableId: 'users', // Unique identifier of the table, optional
  primaryKeyConfig: PrimaryKeyConfig(
    name: 'id', // Primary key field name, defaults to id
    type: PrimaryKeyType.sequential, // Primary key auto-generation strategy
    sequentialConfig: SequentialIdConfig(
      initialValue: 1000, // Initial value for sequential IDs
      increment: 1, // Step size
      useRandomIncrement: false, // Whether to use random step sizes
    ),
  ),
  fields: [
    FieldSchema(
      name: 'username', // Field name, required
      type: DataType.text, // Field data type, required
      nullable: false, // Whether null is allowed
      minLength: 3, // Minimum length
      maxLength: 32, // Maximum length
      unique: true, // Whether it must be unique
      fieldId: 'username', // Stable field identifier, optional, used to detect field renames
      comment: 'Login name', // Optional comment
    ),
    FieldSchema(
      name: 'status',
      type: DataType.integer,
      minValue: 0, // Minimum numeric value
      maxValue: 150, // Maximum numeric value
      defaultValue: 0, // Static default value
      createIndex: true, // Shortcut for creating an index
    ),
    FieldSchema(
      name: 'created_at',
      type: DataType.datetime,
      nullable: false,
      defaultValueType: DefaultValueType.currentTimestamp, // Automatically fill with current time
      createIndex: true,
    ),
  ],
  indexes: const [
    IndexSchema(
      indexName: 'idx_users_status_created_at', // Optional index name
      fields: ['status', 'created_at'], // Composite index fields
      unique: false, // Whether it is a unique index
      type: IndexType.btree, // Index type: btree/vector
    ),
  ],
  foreignKeys: const [], // Optional foreign-key constraints; see "Foreign Keys & Cascading"
  isGlobal: false, // Whether this is a global table; true means it can be shared across spaces
  ttlConfig: null, // Optional table-level TTL; see "Table-level TTL"
  capacityConfig: null, // Optional capacity eviction; see "Capacity Eviction"
);

const appSchemas = [userSchema];
  • Gemeinsame DataType-Zuordnungen:

    Geben Sie ein Entsprechender Darttyp Beschreibung
    integer int Standard-Ganzzahl, geeignet für IDs, Zähler und ähnliche Daten
    bigInt BigInt / String Große ganze Zahlen; empfohlen, wenn Zahlen mehr als 18 Ziffern umfassen, um Genauigkeitsverluste zu vermeiden
    double double Gleitkommazahl, geeignet für Preise, Koordinaten und ähnliche Daten
    text String Textzeichenfolge mit optionalen Längenbeschränkungen
    blob Uint8List Rohe Binärdaten
    boolean bool Boolescher Wert
    datetime DateTime / String Datum/Uhrzeit; intern als ISO8601
    array List Listen- oder Array-Typ
    json Map<String, dynamic> JSON-Objekt, geeignet für dynamisch strukturierte Daten
    vector VectorData / List<num> Dichte Vektoren für semantischen Nearest-Neighbor-Abruf (Embeddings)
    sparse SparseVectorData Neuronale Sparse-Vektoren für Sparse-Abruf
  • PrimaryKeyType Strategien zur automatischen Generierung:

    Strategie Beschreibung Eigenschaften
    none Keine automatische Generierung Sie müssen den Primärschlüssel beim Einfügen manuell angeben
    sequential Sequentielles Inkrement Gut für benutzerfreundliche IDs, aber weniger geeignet für verteilte Leistung
    timestampBased Zeitstempelbasiert Empfohlen für verteilte Umgebungen
    datePrefixed Datum mit Präfix Nützlich, wenn die Lesbarkeit des Datums für das Unternehmen wichtig ist
    shortCode Kurzcode-Primärschlüssel Kompakt und für externe Anzeige geeignet

Alle Primärschlüssel werden standardmäßig als text (String) gespeichert.

Einschränkungen und automatische Validierung

Sie können allgemeine Validierungsregeln direkt in FieldSchema schreiben und so doppelte Logik im Anwendungscode vermeiden:

  • nullable: false: Nicht-Null-Einschränkung
  • minLength / maxLength: Einschränkungen der Textlänge
  • minValue / maxValue: Ganzzahl- oder Gleitkomma-Bereichseinschränkungen
  • defaultValue / defaultValueType: statische Standardwerte und dynamische Standardwerte
  • unique: eindeutige Einschränkung
  • createIndex: Erstellen Sie Indizes für Hochfrequenzfilterung, Sortierung oder Beziehungen
  • fieldId / tableId: Unterstützt die Umbenennungserkennung für Felder und Tabellen während der Migration

Darüber hinaus erstellt unique: true automatisch einen eindeutigen Einzelfeldindex. createIndex: true und Fremdschlüssel erstellen automatisch normale Einzelfeldindizes. Verwenden Sie indexes, wenn Sie zusammengesetzte Indizes, benannte Indizes oder Text- / Sparse- / Vektor-Abrufindizes benötigen.

Schemaentwicklung (Schema Evolution)

Die Engine erkennt strukturelle Änderungen automatisch (Hinzufügen, Entfernen oder Umbenennen von Tabellen/Feldern, Attributanpassungen, Indexänderungen usw.) und führt die Datenmigration durch — ohne manuelle Versionsverwaltung oder Migrationsskripte. Deklarative schemas entwickeln sich bei ToStore.open(); zur Laufzeit auch per updateSchema — geschäftslogiktransparent, Lesen und Schreiben bleiben unterbrechungsfrei.

Eindeutiges Feld zum Primärschlüssel erheben

Ein bestehendes eindeutiges, nicht-null-Feld kann zum Primärschlüssel erhoben werden (Umbenennung optional; mit vorhandenen Daten; geschäftslogiktransparent). Nicht zusammen mit setPrimaryKeyConfig ändern.

  • Mobil (deklarative schemas): Automatische Erkennung bei ToStore.open(). Der Ziel-Primärschlüssel muss PrimaryKeyType.none sein (Werte stammen aus dem Quellfeld; andere Auto-Generierungstypen werden nicht unterstützt). Bei gleichem Namen reicht der Name; zum Umbenennen fromFieldId auf die fieldId des Quellfelds setzen.
  • Server (Laufzeit): updateSchema(...).promoteFieldToPrimaryKey(sourceFieldName: ..., targetPrimaryKeyName: ...) aufrufen. targetPrimaryKeyName ist optional; ohne Angabe bleibt der Quellfeldname erhalten.

Auswahl einer Integrationsmethode

  • Mobil/Desktop: Am besten, wenn appSchemas direkt an ToStore.open(...) übergeben wird
  • Server/Agent: Am besten geeignet, wenn Schemata zur Laufzeit dynamisch über createTables(appSchemas) erstellt werden.

Integration für Mobilgeräte, Desktops und andere häufige Startszenarien

📱 Beispiel: mobile_quickstart.dart

import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';

// On Android/iOS, resolve the app's writable directory first, then pass dbPath explicitly
final docDir = await getApplicationDocumentsDirectory();
final dbRoot = p.join(docDir.path, 'common');

// Reuse the appSchemas defined above
final db = await ToStore.open(
  dbPath: dbRoot,
  schemas: appSchemas,
);

// Multi-space architecture - isolate data for different users
await db.switchSpace(spaceName: 'user_123');

Startfortschritt verfolgen

Normale Schema-Änderungen sind für die Geschäftslogik transparent und blockieren den Start nicht. Nur in seltenen Ausnahmefällen bei mobilen Apps, die häufig zwangsweise geschlossen werden (z. B. kurze Datenvalidierung und Crash-Wiederherstellung nach einem abnormalen Beenden) kann die Initialisierung spürbar dauern — verwenden Sie onStartupProgress, um einen Splash-Screen oder Fortschrittsbalken anzuzeigen:

final db = await ToStore.open(
  dbPath: dbRoot,
  schemas: appSchemas,
  onStartupProgress: (progress, stage) {
    // progress: 0.0 – 1.0  |  stage: opening → recovering → optimizing → ready
    print('Startfortschritt ${(progress * 100).toStringAsFixed(0)}% [$stage]');
    // Splash-Screen / Fortschrittsbalken aktualisieren
  },
);
// Datenbank ist vollständig bereit

Phasen:

  • opening — Konfiguration laden, Basis-Engine vorbereiten
  • recovering — Sicherheitsprüfungen und Absturzwiederherstellung
  • optimizing — Interne Motorabstimmung und Strukturpflege-Optimierung
  • ready — Initialisierung abgeschlossen, einsatzbereit

Anmeldestatus und Abmeldung beibehalten (aktiver Bereich)

Multi-Space ist ideal zum Isolieren von Benutzerdaten: ein Space pro Benutzer, eingeschaltet bei der Anmeldung. Mit Active Space und Schließoptionen können Sie den aktuellen Benutzer über App-Neustarts hinweg behalten und ein sauberes Abmeldeverhalten unterstützen.

  • Anmeldestatus beibehalten: Nachdem Sie einen Benutzer in seinen eigenen Bereich versetzt haben, markieren Sie diesen Bereich als aktiv. Beim nächsten Start kann dieser Bereich direkt beim Öffnen der Standardinstanz aufgerufen werden, ohne den Schritt „Zuerst Standard, dann wechseln“.
  • Abmelden: Wenn sich der Benutzer abmeldet, schließen Sie die Datenbank mit keepActiveSpace: false. Beim nächsten Start wird nicht automatisch der Bereich des vorherigen Benutzers betreten.
// After login: switch to the user's space and mark it active
await db.switchSpace(spaceName: 'user_$userId', keepActive: true);

// Optional: strictly stay in default when needed (for example, login screen only)
// final db = await ToStore.open(..., applyActiveSpaceOnDefault: false);

// On logout: close and clear the active space so the next launch starts from default
await db.close(keepActiveSpace: false);

Serverseitige / Agenten-Integration (Langzeitszenarien)

🖥️ Beispiel: server_quickstart.dart

final db = await ToStore.open();

// Create table structures while the process is running
await db.createTables(appSchemas);

// Online schema updates
final result = await db.updateSchema('users')
  .renameTable('users_new')                // Rename table
  .modifyField(
    'username',
    minLength: 5,
    maxLength: 20,
    unique: true
  )                                        // Modify field attributes
  .renameField('old_name', 'new_name')     // Rename field
  .removeField('deprecated_field')         // Remove field
  .addField('created_at', type: DataType.datetime)  // Add field
  .removeIndex(fields: ['age'])            // Remove index
  .setPrimaryKeyConfig(                    // Change auto-generated PK strategy; avoid when the table already has data
    const PrimaryKeyConfig(type: PrimaryKeyType.shortCode)
  );
// Promote a unique field to PK (see promote-primary-key section; do not chain with the above):
// await db.updateSchema('users').promoteFieldToPrimaryKey(
//   sourceFieldName: 'user_id',
//   targetPrimaryKeyName: 'uid', // optional; omit to keep the source field name
// );

// Monitor migration progress
final taskId = result.taskId;
if (taskId != null) {
  // Inspect migration metadata
  print('Estimated duration: ${result.estimateDuration?.inMilliseconds} ms');
  print('Migration write mode: ${result.writeMode}'); // e.g. MigrationWriteMode.indexOnly

  final status = await db.queryMigrationTaskStatus(taskId);
  print('Migration progress: ${status?.progressPercentage}%');
}


// Optional performance tuning for pure server workloads
// yieldDurationMs controls how often long-running work yields time slices.
// The default is tuned to 8ms to keep frontend UI animations smooth.
// In environments without UI, 50ms is recommended for higher throughput.
final dbServer = await ToStore.open(
  config: DataStoreConfig(yieldDurationMs: 50),
);

Erweiterte Nutzung

ToStore bietet eine Vielzahl erweiterter Funktionen für komplexe Geschäftsszenarien:

Fortgeschrittene Key-Value-Operationen (db.kv)

Für komplexere Key-Value-Szenarien wird empfohlen, den Namespace db.kv zu verwenden. Er bietet einen vollständigen Satz von APIs mit Speicherplatzisolierung, globaler Freigabe, mehreren Datentypen sowie verketteten komplexen Abfragen und Filtern (z. B. db.kv.query().prefix(...).orderBy...().limit(...) für Paginierung, Sortierung, Ablauf-Filter usw.).

  • Grundlegender Zugriff (Basic Access)

    // Wert setzen (unterstützt String, int, bool, double, Map, List usw.)
    await db.kv.set('key', 'value', ttl: Duration(hours: 1));
    
    // Rohwert für dynamischen Typ abrufen
    dynamic val = await db.kv.get('key');
    
    // Einzelnen Schlüssel entfernen
    await db.kv.remove('key');
    
  • Typsichere Getter (Type-Safe Getters) Rufen Sie Daten direkt im Zielformat ab, ohne manuelle Konvertierung:

    String? name = await db.kv.getString('user_name');
    int? age = await db.kv.getInt('user_age');
    bool? isVip = await db.kv.getBool('is_vip');
    Map<String, dynamic>? profile = await db.kv.getMap('profile');
    List<String>? tags = await db.kv.getList<String>('tags');
    
  • Stapeloperationen (Bulk Operations) Verarbeiten Sie effizient mehrere Key-Value-Paare in einer einzigen Operation:

    // Stapelweises Setzen
    await db.kv.setMany({
      'theme': 'dark',
      'language': 'de_DE',
    });
    
    // Stapelweises Entfernen
    await db.kv.removeKeys(['temp_1', 'temp_2']);
    
  • Atomare Zähler (Atomic Increment) Erhöhen oder verringern Sie numerische Werte sicher in Szenarien mit hoher Parallelität:

    // Um 1 erhöhen (Standard)
    await db.kv.setIncrement('view_count');
    // Um 5 verringern (einen negativen Wert übergeben)
    await db.kv.setIncrement('stock_count', amount: -5);
    
  • Verkettete Datensatzabfragen (db.kv.query) Verkettete API ähnlich zu db.query() zum Abfragen von Key-Value-Datensätzen (inkl. dekodiertem value) mit Paginierung. Im Gegensatz zu getKeys (nur Schlüsselnamen) liefert dies vollständige Datensätze.

    // Erste Seite: nach Präfix filtern, nach Update-Zeit absteigend, 20 pro Seite
    final page = await db.kv.query()
        .prefix('setting_')
        .orderByUpdatedAtDesc() // oder orderByKeyAsc / orderByKeyDesc / orderByUpdatedAtAsc
        .limit(20);
    
    for (final record in page.data) {
      // record enthält: key, value, updated_at, expires_at
      print('${record['key']} = ${record['value']}');
    }
    
    // Empfohlen: mit next() / prev() blättern (wie bei Tabellenabfragen, am einfachsten)
    if (page.hasMore) {
      final page2 = await page.next();
      print('Nächste Seite: ${page2.data.length}');
      if (page2.hasPrev) {
        final back = await page2.prev();
        print('Vorherige Seite: ${back.data.length}');
      }
    }
    
    // Offset-Paginierung (schließt sich mit cursor aus; für tiefes Blättern besser next() oben)
    final byOffset = await db.kv.query()
        .orderByKeyAsc()
        .limit(20)
        .offset(20);
    
    // Gesamtzahl der Treffer (ohne prefix: O(1)-Metadatenstatistik)
    final total = await db.kv.query().prefix('setting_').count();
    
    // Ersten Treffer holen
    final first = await db.kv.query().prefix('setting_').orderByKeyAsc().first();
    
    // Globaler KV-Bereich
    final globalPage = await db.kv.query(isGlobal: true).limit(50);
    
    // Standardmäßig werden abgelaufene Datensätze ausgefiltert; für noch nicht bereinigte:
    final withExpired = await db.kv.query()
        .includeExpired()
        .limit(20);
    

    Häufige Kettenmethoden:

    Methode Beschreibung
    prefix(String) Nach Key-Präfix filtern
    orderByKeyAsc / orderByKeyDesc Nach Key (Primärschlüssel) sortieren
    orderByUpdatedAtAsc / orderByUpdatedAtDesc Nach updated_at sortieren
    limit(n) Max. Einträge pro Seite (explizit angeben empfohlen)
    offset(n) Offset-Paginierung (löscht cursor)
    cursor(token) Nur Sonderfälle: Paginierungs-Token über Prozesse/Netzwerke
    includeExpired([true]) Abgelaufene, noch nicht bereinigte Datensätze einschließen
    count() Treffer zählen
    first() Ersten Datensatz zurückgeben (ändert limit des Builders nicht)

    Abfrageergebnis QueryResult: Alltagspaginierung mit hasMore / hasPrev + next() / prev(); nextCursorToken / prevCursorToken nur für client-/server- oder netzwerkübergreifende Übertragung u. Ä. (wie bei Tabellenabfragen).

  • Erkundung & Verwaltung (Discovery & Management)

    // Nur Schlüsselnamen auflisten (ohne value); optional prefix / limit / offset
    final keys = await db.kv.getKeys(prefix: 'setting_');
    final pageKeys = await db.kv.getKeys(
      prefix: 'setting_',
      limit: 100,
      offset: 0,
    );
    
    // Gesamtzahl der Schlüssel im aktuellen Bereich zählen
    final count = await db.kv.count();
    
    // Prüfen, ob ein Schlüssel existiert und nicht abgelaufen ist
    final exists = await db.kv.exists('config_cache');
    
    // Speichersonde (synchron, nur bei Speichertreffer — siehe [Speicher-Sonde und synchrone Abfrage (peek)](#query-peek))
    final theme = db.kv.peekGet('theme') ?? await db.kv.get('theme');
    if (db.kv.peekExists('config_cache')) { /* ... */ }
    
    // Alle KV-Daten im aktuellen Bereich löschen
    await db.kv.clear();
    
  • Lebenszyklus-Management (TTL) Ablauf-Einstellungen für vorhandene Schlüssel prüfen oder aktualisieren:

    // Verbleibende Dauer abrufen
    Duration? ttl = await db.kv.getTtl('token');
    
    // TTL für einen vorhandenen Schlüssel aktualisieren (läuft in 7 Tagen ab)
    await db.kv.setTtl('token', Duration(days: 7));
    
  • Reaktive Überwachung (Reactive Watch)

    // Einzelnen Schlüssel überwachen
    db.kv.watch<int>('unread_count').listen((count) => print(count));
    
    // Snapshot mehrerer Schlüssel überwachen
    db.kv.watchValues(['theme', 'font_size']).listen((map) => print(map));
    
  • Globale Freigabe (isGlobal) Alle oben genannten Methoden unterstützen den optionalen Parameter isGlobal: true für den globalen Bereich (über alle Bereiche hinweg geteilt), false (Standard) für den aktuellen isolierten Bereich.

Stapeloperationen (Bulk Operations)

ToStore bietet spezialisierte Schnittstellen für die Stapelverarbeitung, die für einen hohen Datendurchsatz optimiert sind. Diese Schnittstellen nutzen parallele Aufgabenverteilung und Time-Slicing, um die Reaktionsfähigkeit der Benutzeroberfläche bei intensiven Schreibvorgängen zu gewährleisten.

Methode Hauptzweck Datenanforderungen Merkmale
batchInsert Datensätze stapelweise einfügen Muss alle nicht-nullbaren Felder enthalten Reines Einfügen, höchste Leistung
batchUpsert Intelligente Synchronisation (Upsert) Muss alle nicht-nullbaren Felder enthalten Vollständige Synchronisation, Identifizierung über Primärschlüssel oder eindeutiges Feld
batchUpdate Datensätze stapelweise aktualisieren Primärschlüssel oder eindeutiges Feld + Aktualisierungsfelder Partielle Aktualisierungen für vorhandene Datensätze
  • Stapelweises Einfügen (batchInsert)

    await db.batchInsert('users', [
      {'username': 'user1', 'email': '[email protected]'},
      {'username': 'user2', 'email': '[email protected]'},
    ]);
    
  • Intelligente Stapel-Synchronisation (batchUpsert) Identifiziert automatisch "Einfügen" oder "Aktualisieren" basierend auf dem Primärschlüssel oder eindeutigen Feldern. Häufig für die vollständige Datensynchronisation verwendet.

    [!IMPORTANT] Datenanforderungen: Da ein Einfügevorgang ausgelöst werden kann, erfordert batchUpsert, dass jeder Datensatz alle nicht-nullbaren (nullable: false) Felder enthält.

  • Hochleistungs-Stapelaktualisierung (batchUpdate) Speziell für die Aktualisierung vorhandener Datensätze. Jeder Datensatz muss einen Primärschlüssel oder ein eindeutiges Feld als Bezeichner enthalten, zusammen mit den zu ändernden Feldern.

    [!TIP] Partielle Aktualisierungen: batchUpdate ändert nur die bereitgestellten Felder und erfordert nicht alle nicht-nullbaren Felder, was es ideal für inkrementelle Aktualisierungen macht.

    await db.batchUpdate('users', [
      {'username': 'john', 'age': 27}, // Über eindeutiges Feld 'username' identifizieren und 'age' aktualisieren
      {'id': '1002', 'status': 'active'}, // Kann auch den Primärschlüssel direkt verwenden
    ]);
    

[!TIP] Sie können allowPartialErrors: true festlegen, um sicherzustellen, dass Fehler bei einzelnen Datensätzen (z. B. eine Verletzung einer eindeutigen Einschränkung) nicht die gesamte Stapeloperation ablehnen.

Garantie für Schreibwiederherstellung (Recovery Durability)

ToStore ermöglicht die flexible Steuerung der Selbstheilungswiederherstellungsstufe zum Zeitpunkt der Rückgabe des Ergebnisses von Schreiboperationen (await-Abschluss):

Stufe Wiederherstellungsgarantie bei Antwort Anwendungsfälle & Eigenschaften
RecoveryDurability.standard
(Standardempfehlung)
Vor der Antwort absturzsicher Schutz vor App-Abstürzen, nicht abgefangenen Ausnahmen und OOM-Prozessbeendigungen durch das Betriebssystem. Hoher Durchsatz bei alltäglicher Geschäftssicherheit.
RecoveryDurability.durable Vor der Antwort stromausfallsicher Vor der Rückgabe auf den physischen Speicher erzwungen. Vollständige Selbstheilung auch bei plötzlichen Stromausfällen oder Kernel-Panics. Ideal für Finanztransaktionen und kritische Audits.
RecoveryDurability.async Maximaler Durchsatz (asynchrones Speichern) Wird in den Speicher eingereiht und gibt sofort die Antwort zurück; nach der Rückgabe besteht bei kleinen Datenmengen meist nur ein extrem kurzes Datenverlustfenster von wenigen Millisekunden. Ideal für Massenprotokolle und Metriken.

1. Globale Standardkonfiguration

Konfigurieren Sie die Standardwiederherstellungsstufe der Instanz in DataStoreConfig:

final db = await ToStore.open(
  config: DataStoreConfig(
    defaultRecoveryDurability: RecoveryDurability.standard, // Standard: Vor der Antwort absturzsicher
  ),
);

2. Überschreibung pro Operation

Alle strukturierten Tabellenoperationen, Stapeloperationen, verketteten Updates/Deletes sowie der Key-Value-Speicher (KV) unterstützen die individuelle Überschreibung über den optionalen Parameter durability:

// 1. Strukturierte Tabellenoperationen
await db.insert('orders', orderData, durability: RecoveryDurability.durable); // Wichtige Bestellung: Stromausfallsicher
await db.batchInsert('logs', logList, durability: RecoveryDurability.async); // Massenprotokolle: Schnelle asynchrone Speicherung

// 2. Verkettete Updates und Löschvorgänge
await db.update('users', {'status': 'active'})
  .where('id', '=', userId)
  .durability(RecoveryDurability.durable);

// 3. Key-Value-Speicher (KV)
await db.setValue('token', tokenVal, durability: RecoveryDurability.standard);
await db.kv.set('session_id', 'xyz', durability: RecoveryDurability.async);
await db.kv.setMany(settingsMap, durability: RecoveryDurability.durable);

Vektor-, Text- und Hybridabruf

Lexikalischer Textabruf, dichter Vektorabruf und neuronaler Sparse-Abruf teilen sich eine Abfragekette (matchText / matchVector / matchSparse): kombinieren Sie sie mit strukturierten Prädikaten in derselben Kette oder fusionieren Sie mehrere Recall-Zweige. Scores und Kanaldiagnosen werden in QueryResult.retrieval zurückgegeben und sind 1:1 auf data-Zeilen ausgerichtet. Wenn nur Primärschlüssel und Scores nötig sind, bevorzugen Sie .select(['id']) (oder Ihren PK-Feldnamen), damit die Engine keine vollständigen Tabellenzeilen materialisiert — sonst lädt der Hybridabruf komplette Datensätze (inkl. großer Embeddings) und der Durchsatz kann stark sinken. Für dediziertes ANN ohne Filter oder Fusion liefert db.vectorSearch(...) mit demselben Kernsuchpfad List<VectorSearchResult> (Primärschlüssel + Score).

Suchsichtbarkeit: Nach erfolgreichem Insert/Update sind normale Indizes, Textindizes und Sparse-Indizes sofort suchbar; Vektorindizes werden nicht an den Schreibpfad gekoppelt (um den Durchsatz nicht zu belasten) und sind nicht sofort suchbar. Wenn Sie sie für die Suche brauchen, rufen Sie nach Ihrem Geschäftsbatch await db.flush() auf (optionale Bequemlichkeit); erst danach nehmen sie an matchVector / vectorSearch teil.

await db.createTables([
  const TableSchema(
    name: 'documents',
    primaryKeyConfig: PrimaryKeyConfig(
      name: 'id',
      type: PrimaryKeyType.timestampBased,
    ),
    fields: [
      FieldSchema(name: 'content', type: DataType.text, nullable: false),
      FieldSchema(
        name: 'category',
        type: DataType.text,
        nullable: false,
        createIndex: true,
      ),
      FieldSchema(
        name: 'features',
        type: DataType.sparse, // Neural sparse vector
        nullable: false,
      ),
      FieldSchema(
        name: 'embedding',
        type: DataType.vector, // Dense vector
        nullable: false,
        vectorConfig: VectorFieldConfig(
          dimensions: 128, // Written and queried vectors must match this width
        ),
      ),
    ],
    indexes: [
      IndexSchema(fields: ['content'], type: IndexType.text), // Lexical text index
      IndexSchema(fields: ['features'], type: IndexType.sparse), // Neural sparse index
      IndexSchema(
        fields: ['embedding'],
        type: IndexType.vector, // Dense vector index
        vectorConfig: VectorIndexConfig(
          indexType: VectorIndexType.ngh, // NGH: ToStore built-in dense index
          distanceMetric: VectorDistanceMetric.cosine,
        ),
      ),
    ],
  ),
]);

final queryVector =
    VectorData.fromList(List.generate(128, (i) => i * 0.01));
final querySparse = SparseVectorData.fromMap({12: 0.8, 45: 0.6, 90: 0.3});

// 1) Lexical text retrieval (engine tokenizes TextQuery.text;
//    or use pre-tokenized TextQuery.terms / termsWeighted)
final textHits = await db
    .query('documents')
    .select(['id'])
    .matchText('content', TextQuery.text('distributed kernel storage'))
    .limit(5);

// 2) Neural sparse retrieval
final sparseHits = await db
    .query('documents')
    .select(['id'])
    .matchSparse('features', querySparse, minScore: 0.30)
    .limit(5);

// 3) Dense vector ANN (default searchDepth = 50)
final result = await db
    .query('documents')
    .select(['id'])
    .matchVector('embedding', queryVector, minScore: 0.40)
    .limit(5);

for (var i = 0; i < result.data.length; i++) {
  final record = result.data[i];
  final entry = result.retrieval?.entries[i];
  print('pk=${record['id']}, score=${entry?.score}, '
      'rawScore=${entry?.rawScore}, distance=${entry?.meta?['distance']}');
}

// 4) Structured filter + vector (AND)
final filtered = await db
    .query('documents')
    .whereEqual('category', 'tech')
    .matchVector('embedding', queryVector, minScore: 0.40)
    .limit(5);

// 5) Multi-way fusion: text + sparse + vector (default weightedScore)
final fused = await db
    .query('documents')
    .matchText('content', TextQuery.text('distributed storage'), weight: 0.4)
    .matchSparse('features', querySparse, weight: 0.3, minScore: 0.20)
    .matchVector('embedding', queryVector, weight: 0.6, minScore: 0.40)
    .limit(10);
print('fusion=${fused.retrieval?.fusionMethod}'); // weightedScore

// Opt into rank-only fusion (relative scores — not absolute relevance bands)
final fusedRrf = await db
    .query('documents')
    .matchText('content', TextQuery.text('distributed storage'), weight: 0.4)
    .matchVector('embedding', queryVector, weight: 0.6, minScore: 0.40)
    .fusionMethod(RetrievalFusionMethod.rrf)
    .limit(10);
print('fusion=${fusedRrf.retrieval?.fusionMethod}'); // rrf

// Dedicated ANN (PK + score only; no query chain / fusion)
final ann = await db.vectorSearch(
  'documents',
  fieldName: 'embedding',
  queryVector: queryVector,
  topK: 5,
  minScore: 0.40,
);

Kanal- / Schema-Zuordnung:

Kanal Feldtyp Index Abfrage-API Abfragewert
Lexikalischer Text DataType.text IndexType.text matchText / orMatchText TextQuery.text / terms / termsWeighted
Neuronal sparse DataType.sparse IndexType.sparse matchSparse / orMatchSparse SparseVectorData
Dichter Vektor DataType.vector IndexType.vector matchVector / orMatchVector VectorData usw.

Konfiguration dichter Vektoren (VectorFieldConfig, VectorIndexConfig):

  • dimensions: muss der tatsächlichen Embedding-Breite entsprechen, die Sie schreiben
  • indexType: undurchsichtige ID des dichten Algorithmus; derzeit ngh
  • distanceMetric: indexseitige Ähnlichkeitsmetrik für Aufbau und Suche; cosine ist üblich für semantische Embeddings, l2 für euklidische Distanz, innerProduct für Skalarproduktsuche. Nach dem Schreiben von Daten erfordert eine Änderung in der Regel den Neuaufbau des Vektorindex.

Parameter des verketteten Abrufs (jedes match* / orMatch*, plus limit in der Kette):

  • Text
    • TextQuery.text: wird von der Engine tokenisiert
    • TextQuery.terms / termsWeighted: vorab tokenisiert; kein zweiter Durchlauf; lateinische Terme müssen zur Indexseite passen (meist Kleinbuchstaben)
    • operator: or (Standard; Ranking über Vereinigung) oder and (jeder positiv gewichtete Term muss treffen, dann Ranking)
    • Phrasen-Matching wird nicht unterstützt
  • Sparse
    • SparseVectorData / SparseVectorData.fromMap(...): Sparse-Abfragevektor
    • Dokumentseitige Gewichte sollten vorzugsweise in (0, 1] liegen
  • Vektor
    • field / vector: Zielfeld und Abfragevektor
    • searchDepth: >= 1, Standard 50; größere Werte erhöhen meist Recall und Latenz; approximativer Nearest Neighbor — kein garantiertes Recall@K
    • distanceThreshold: Distanzobergrenze
  • Gemeinsam
    • weight: Mehrweg-Fusionsgewicht; Standard 1.0
    • minScore: Relevanzuntergrenze
    • limit: Ergebnisanzahl (topK)
    • .fusionMethod(...): Mehrweg-Standard weightedScore; umschaltbar auf rrf

Hinweise zu Ergebnis und Scoring (QueryResult / VectorSearchResult):

  • Geschäftsdatensätze liegen in data; Abruf-Scores und Kanalinfos in retrieval.entries, 1:1 auf data ausgerichtet. Bevorzugen Sie .select(['id']), wenn nur Primärschlüssel und Scores nötig sind.
  • score (normalisierter Relevanzscore, 0.0 ~ 1.0): einheitliche Relevanz über Text, Sparse und Vektor (höher = relevanter). Exakter Vektor-Selbsttreffer erreicht 1.0. Mit minScore starke Treffer behalten und schwache verwerfen. Standard-Mehrweg-weightedScore-Fusion behält einen starken Einzelkanal-Treffer; Text und Sparse bleiben in [0.0, 1.0).
  • rawScore: nicht normalisierte physische Metrik — Kosinus / Skalarprodukt / L2 / Text-Impact. Nutzen Sie rawScore / distance, wenn Sie die zugrunde liegende Metrik brauchen.
  • entry.meta['distance']: ANN-Distanz auf Vektorkanälen (niedriger = näher).
  • retrieval.fusionMethod: null bei Einzelkanal-Recall; Mehrweg-Standard weightedScore (umschaltbar via .fusionMethod(RetrievalFusionMethod.rrf)).
  • Top-Level db.vectorSearch(...) liefert List<VectorSearchResult> (primaryKey / score / rawScore / distance) für dediziertes ANN. Optionales minScore nutzt dieselbe Relevanzuntergrenze wie matchVector (kann weniger als topK liefern).

Relevanzscore (score) & Schwellen-Referenzbasis (Einzelkanal und Mehrweg-weightedScore; nicht für rrf). Für typische Embeddings (Dimensionen ≥ 128); bei niedrigeren Dimensionen können schwache Nachbarn hoch scoren — erhöhen Sie ggf. minScore oder nutzen Sie rawScore:

Relevanzscore (score) Basisbedeutung & typische Szenarien
>= 0.85 Gleiche Entität (Gesichtsverifikation, Biometrie, exakte Deduplizierung)
>= 0.70 Semantischer Treffer mit hoher Konfidenz (Kernfakten, präzises Q&A)
>= 0.40 Grundlegende semantische Relevanz (allgemeines RAG, Anti-Halluzination in Wissensbasen)
>= 0.30 Breite Erstrecall (unscharfe Entdeckung, Pre-Rerank-Kandidatenpool)
< 0.30 Schwache Assoziation oder Hintergrundrauschen

Konfigurieren Sie kanalweise minScore, um Treffer mit geringer Relevanz zu verwerfen.

TTL auf Tabellenebene (automatischer zeitbasierter Ablauf)

Für Protokolle, Telemetrie, Ereignisse und andere Daten, die mit der Zeit ablaufen sollen, können Sie TTL auf Tabellenebene über ttlConfig definieren. Die Engine bereinigt abgelaufene Datensätze automatisch im Hintergrund:

const TableSchema(
  name: 'event_logs',
  fields: [
    FieldSchema(
      name: 'created_at',
      type: DataType.datetime,
      nullable: false,
      createIndex: true,
      defaultValueType: DefaultValueType.currentTimestamp,
    ),
  ],
  ttlConfig: TableTtlConfig(
    ttlMs: 7 * 24 * 60 * 60 * 1000, // 7 Tage aufbewahren
    // Erforderlich für neue Tabellen / erstmaliges Aktivieren von TTL:
    // 1) DataType.datetime  2) nullable: false
    // 3) DefaultValueType.currentTimestamp
    sourceField: 'created_at',
  ),
);

Kapazitätsbereinigung (FIFO / LRU)

Begrenzt die Anzahl der Datensätze einer Tabelle nach Anzahl und/oder ungefährer Größe. Unabhängig von TTL—beides darf kombiniert werden. Überschüssige Zeilen werden im Hintergrund automatisch entfernt.

// FIFO (Standard): älteste Einträge nach Primärschlüssel entfernen.
// sourceField nicht setzen (außer Primärschlüsseltyp ist PrimaryKeyType.none).
capacityConfig: CapacityConfig(
  maxEntries: 10000,
  // maxBytes: 50 * 1024 * 1024, // optionale Größengrenze
  policy: CapacityEvictionPolicy.fifo,
),

// LRU: sourceField erforderlich (datetime, non-null, currentTimestamp).
// Feld aktualisieren, wenn der Datensatz wieder als „verwendet“ gilt.
capacityConfig: CapacityConfig(
  maxEntries: 10000,
  policy: CapacityEvictionPolicy.lru,
  sourceField: 'last_active_at',
),
  • Mindestens eines von maxEntries / maxBytes setzen.
  • FIFO + PrimaryKeyType.none erfordert ebenfalls sourceField (gleiche Datetime-Regeln).

Kapazitätsbereinigung kann zur Laufzeit für bestehende Tabellen aktiviert, geändert oder deaktiviert werden (kein Neustart nötig):

// Aktivieren / anpassen
await db.updateSchema('recent_events').setCapacityConfig(
  CapacityConfig(
    maxEntries: 10000,
    policy: CapacityEvictionPolicy.lru,
    sourceField: 'last_active_at',
  ),
);

// Deaktivieren (bestehende Zeilen bleiben; keine kapazitätsbasierten Löschungen mehr)
await db.updateSchema('recent_events').disableCapacity();

KV-Kapazität wird beim Öffnen der Datenbank über DataStoreConfig gesetzt (nicht pro kv.set):

  • kvCapacityConfig: Limit für Keys im aktuellen Space
  • globalKvCapacityConfig: Limit für globale KV
  • Gleiche Form wie Tabellen-CapacityConfig, aber kein sourceField
final db = await ToStore.open(
  dbPath: '...',
  config: DataStoreConfig(
    kvCapacityConfig: CapacityConfig(
      maxEntries: 5000,
      policy: CapacityEvictionPolicy.lru,
    ),
    globalKvCapacityConfig: CapacityConfig(
      maxEntries: 2000,
      policy: CapacityEvictionPolicy.fifo,
    ),
  ),
);

Intelligenter Speicher (Upsert)

ToStore entscheidet basierend auf dem Primärschlüssel oder dem eindeutigen Feld in data, ob aktualisiert oder eingefügt wird. where wird hier nicht unterstützt; das Ziel des Konflikts wird durch die Daten selbst bestimmt.

// Nach Primärschlüssel
final result = await db.upsert('users', {
  'id': 1,
  'username': 'john',
  'email': '[email protected]',
});

// Nach eindeutigem Schlüssel (der Datensatz muss alle Felder enthalten, die an einer eindeutigen Einschränkung beteiligt sind, plus Pflichtfelder)
await db.upsert('users', {
  'username': 'john',
  'email': '[email protected]',
  'age': 26,
});

// Batch Upsert (unterstützt atomaren Modus oder Modus für Teilerfolg)
// allowPartialErrors: true bedeutet, dass einige Zeilen fehlschlagen können, während andere erfolgreich sind
final batchResult = await db.batchUpsert('users', [
  {'username': 'a', 'email': '[email protected]'},
  {'username': 'b', 'email': '[email protected]'},
], allowPartialErrors: true);

Erweiterte Abfragen

ToStore bietet eine deklarativ verkettbare Abfrage-API mit flexibler Feldverarbeitung und komplexen Beziehungen mit mehreren Tabellen.

1. Feldauswahl (select)

Die Methode select gibt an, welche Felder zurückgegeben werden. Wenn Sie es nicht aufrufen, werden standardmäßig alle Felder zurückgegeben.

  • Aliase: unterstützt die field as alias-Syntax (ohne Berücksichtigung der Groß- und Kleinschreibung), um Schlüssel im Ergebnissatz umzubenennen
  • Tabellenqualifizierte Felder: Bei Verknüpfungen mit mehreren Tabellen vermeidet table.field Namenskonflikte
  • Aggregationsmischung: Agg Objekte können direkt in der select Liste platziert werden
final results = await db.query('orders')
    .select([
      'orders.id',
      'users.name as customer_name',
      'orders.amount',
      Agg.count('id', alias: 'total_items')
    ])
    .join('users', 'orders.user_id', '=', 'users.id')
    .where('orders.amount', '>', 1000)
    .limit(20);

2. Tritt bei (join)

Unterstützt den Standard join (Inner Join), leftJoin und rightJoin.

3. Intelligente fremdschlüsselbasierte Verknüpfungen (empfohlen)

Wenn foreignKeys in TableSchema korrekt definiert ist, müssen Sie die Join-Bedingungen nicht handschriftlich verfassen. Die Engine kann Referenzbeziehungen auflösen und automatisch den optimalen JOIN-Pfad generieren.

  • joinReferencedTable(tableName): Verbindet automatisch die übergeordnete Tabelle, auf die die aktuelle Tabelle verweist
  • joinReferencingTable(tableName): Verbindet automatisch untergeordnete Tabellen, die auf die aktuelle Tabelle verweisen
// Assume posts defines a foreign key to users
final posts = await db.query('posts')
    .joinReferencedTable('users') // Automatically resolves to ON posts.user_id = users.id
    .select(['posts.title', 'users.username'])
    .limit(20);

Aggregation, Gruppierung und Statistik (Agg & GroupBy)

1. Aggregation (Agg Fabrik)

Aggregatfunktionen berechnen Statistiken über einen Datensatz. Mit dem Parameter alias können Sie Ergebnisfeldnamen anpassen.

Methode Zweck Beispiel
Agg.count(field) Nicht-Null-Datensätze zählen Agg.count('id', alias: 'total')
Agg.sum(field) Summenwerte Agg.sum('amount', alias: 'total_price')
Agg.avg(field) Durchschnittswert Agg.avg('score', alias: 'average_score')
Agg.max(field) Maximalwert Agg.max('age')
Agg.min(field) Mindestwert Agg.min('price')

[!TIP] Zwei gängige Aggregationsstile

  1. Abkürzungsmethoden (empfohlen für einzelne Metriken): Rufen Sie direkt die Kette auf und erhalten Sie den berechneten Wert sofort zurück. num? totalAge = await db.query('users').sum('age');
  2. Eingebettet in select (für mehrere Metriken oder Gruppierung): Übergeben Sie Agg-Objekte an die select-Liste. final stats = await db.query('orders').select(['status', Agg.sum('amount')]).groupBy(['status']);

2. Gruppieren und Filtern (groupBy / having)

Verwenden Sie groupBy, um Datensätze zu kategorisieren, und dann having, um aggregierte Ergebnisse zu filtern, ähnlich dem HAVING-Verhalten von SQL.

final stats = await db.query('orders')
    .select([
      'status',
      Agg.sum('amount', alias: 'sum_amount'),
      Agg.count('id', alias: 'order_count')
    ])
    .groupBy(['status'])
    // having accepts a QueryCondition used to filter aggregated results
    .having(QueryCondition().where(Agg.sum('amount'), '>', 5000))
    .limit(10);

3. Hilfsabfragemethoden

  • exists() (Hochleistung): prüft, ob ein Datensatz übereinstimmt. Im Gegensatz zu count() > 0 wird es kurzgeschlossen, sobald eine Übereinstimmung gefunden wird, was sich hervorragend für sehr große Datensätze eignet.
  • count(): Gibt effizient die Anzahl der übereinstimmenden Datensätze zurück.
  • first(): eine praktische Methode, die limit(1) entspricht und die erste Zeile direkt als Map zurückgibt.
  • distinct([fields]): dedupliziert Ergebnisse. Wenn fields angegeben werden, wird die Eindeutigkeit anhand dieser Felder berechnet.
// Efficient existence check
if (await db.query('users').whereEqual('email', '[email protected]').exists()) {
  print('Email is already registered');
}

// Get a deduplicated city list
final cities = await db.query('users').distinct(['city']);

4. Komplexe Logik mit QueryCondition

QueryCondition ist das Kerntool von ToStore für verschachtelte Logik und die Erstellung von Abfragen in Klammern. Wenn einfache verkettete where-Aufrufe für Ausdrücke wie (A AND B) OR (C AND D) nicht ausreichen, ist dies das richtige Werkzeug.

  • condition(QueryCondition sub): öffnet eine AND verschachtelte Gruppe
  • orCondition(QueryCondition sub): öffnet eine OR verschachtelte Gruppe
  • or(): Ändert den nächsten Connector zu OR (Standard ist AND)
Beispiel 1: Gemischte ODER-Bedingungen

Äquivalentes SQL: WHERE is_active = true AND (role = 'admin' OR fans >= 1000)

final subGroup = QueryCondition()
    .whereEqual('role', 'admin')
    .or()
    .whereGreaterThanOrEqualTo('fans', 1000);

final results = await db.query('users')
    .whereEqual('is_active', true)
    .condition(subGroup);
Beispiel 2: Wiederverwendbare Bedingungsfragmente

Sie können wiederverwendbare Geschäftslogikfragmente einmalig definieren und sie in verschiedenen Abfragen kombinieren:

final hotUser = QueryCondition().whereGreaterThan('fans', 5000);
final recentLogin = QueryCondition().whereGreaterThan('last_login', '2024-01-01');

final targetUsers = await db.query('users')
    .condition(hotUser)
    .condition(recentLogin);

5. Streaming-Abfrage

Geeignet für sehr große Datensätze, wenn Sie nicht alles auf einmal in den Speicher laden möchten. Die Ergebnisse können beim Lesen verarbeitet werden.

db.streamQuery('users').listen((data) {
  print('Processing one record: $data');
});

6. Reaktive Abfrage

Mit der watch()-Methode können Sie Abfrageergebnisse in Echtzeit überwachen. Es gibt ein Stream zurück und führt die Abfrage automatisch erneut aus, wenn sich übereinstimmende Daten in der Zieltabelle ändern.

  • Automatische Entprellung: Die integrierte intelligente Entprellung vermeidet redundante Abfragestöße
  • UI-Synchronisierung: Funktioniert natürlich mit Flutter StreamBuilder für die Live-Aktualisierung von Listen
// Simple listener
db.query('users').whereEqual('is_online', true).watch().listen((users) {
  print('Online user count changed: ${users.length}');
});

// Flutter StreamBuilder integration example
// Local UI refreshes automatically when data changes
StreamBuilder<List<Map<String, dynamic>>>(
  stream: db.query('messages').orderByDesc('id').limit(50).watch(),
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      return ListView.builder(
        itemCount: snapshot.data!.length,
        itemBuilder: (context, index) => MessageTile(snapshot.data![index]),
      );
    }
    return CircularProgressIndicator();
  },
)

Manuelles Zwischenspeichern von Abfrageergebnissen (optional)

[!IMPORTANT] ToStore enthält intern bereits einen effizienten mehrstufigen intelligenten LRU-Cache. Eine routinemäßige manuelle Cache-Verwaltung wird nicht empfohlen. Erwägen Sie dies nur in besonderen Fällen:

  1. Teure vollständige Scans nicht indizierter Daten, die sich selten ändern
  2. Anhaltende Anforderungen an extrem niedrige Latenzzeiten, auch für nicht heiße Abfragen
  • useQueryCache([Duration? expiry]): Cache aktivieren und optional einen Ablauf festlegen
  • noQueryCache(): Cache für diese Abfrage explizit deaktivieren
  • clearQueryCache(): Den Cache für dieses Abfragemuster manuell ungültig machen
final results = await db.query('heavy_table')
    .where('non_indexed_field', '=', 'value')
    .useQueryCache(const Duration(minutes: 10)); // Manual acceleration for a heavy query only

Abfrage und effiziente Paginierung

[!TIP] Geben Sie immer limit als Seitengröße an: Es wird dringend empfohlen, in jeder Abfrage explizit ein limit festzulegen. Wenn es weggelassen wird, begrenzt die Engine die Abfrage standardmäßig auf 1.000 Datensätze, um das Laden zu großer Datenmengen auf einmal zu verhindern.

ToStore unterstützt Dual-Mode-Paginierung. Für unendliches Scrollen oder das Laden von Listen empfehlen wir dringend die integrierte nahtlose Cursor-Paginierung. Für das direkte Springen zu bestimmten Seiten reicht die einfache Offset-Paginierung aus:

1. Einfache Paginierung (Offset-Modus)

Geeignet für Szenarien mit geringen Datenmengen (z. B. unter 10k) oder wenn Sie gezielt zu einer bestimmten Seite springen müssen.

final result = await db.query('users')
    .orderByDesc('created_at')
    .offset(40) // Die ersten 40 Zeilen überspringen
    .limit(20); // 20 Zeilen abrufen

[!TIP] Wenn der offset sehr groß wird, muss die Datenbank viele Zeilen scannen und verwerfen, wodurch die Leistung linear abnimmt. Für tiefe Paginierung oder größere Datensätze wird der Cursor-Modus empfohlen.

2. Cursor-Paginierung (Cursor-Modus - Empfohlen)

Ideal für riesige Datensätze und unendliches Scrollen. Indem die Startposition des aktuellen Seiten-Datenstroms aufgezeichnet wird, springt die Engine bei der Paginierung direkt dorthin (Seek). Historische Daten müssen nicht gescannt und verworfen werden, wodurch die Geschwindigkeit bei tiefer Paginierung stets konstant bleibt.

  • Automatische Verwaltung: Legen Sie die Seitengröße über das Limit fest und rufen Sie einfach next() oder prev() auf den nachfolgenden Seiten auf, um mühelos eine optimale Paginierungsleistung zu erzielen.
  • Startpunkt-Verschiebung: Unterstützt die Kombination mit .offset(N) bei der ersten Abfrage, um das Startfenster festzulegen. Danach rufen nachfolgende next()-Aufrufe direkt die folgenden Seiten ab.
// 1. Erste Abfrage starten
final page1 = await db.query('users')
    .orderByDesc('id')
    .limit(20);

// 2. Nächste Seite abrufen
if (page1.hasMore) {
  final page2 = await page1.next(); 
  print('Anzahl der Einträge auf der nächsten Seite: \${page2.data.length}');
  
  // 3. Vorherige Seite abrufen
  if (page2.hasPrev) {
    final prevPage = await page2.prev();
    print('Daten der vorherigen Seite: \${prevPage.data}');
  }
}
Fortgeschrittenes Szenario: Zustandlose Token-basierte Cursor-Paginierung (Token-based Cursor)

Für die Alltagspaginierung in der App bevorzugen Sie die oben genannten next() / prev(). Cursor-Token nur bei Client-Server-APIs oder wenn der Paginierungsstatus über Prozesse/Netzwerke serialisiert werden muss:

  • Die erste Abfrage gibt die Token-Strings nextCursorToken und prevCursorToken zurück.
  • Die nachfolgende Abfrage übergibt das Token über .cursor(token) für eine gezielte Positionierung (Seek).
  • Hinweis: cursor und offset schließen sich gegenseitig aus; das Setzen des einen löscht das andere.
// Erste Abfrage (z. B. auf der API-Serverseite)
final page1 = await db.query('users')
    .orderByDesc('id')
    .limit(20);

final String? nextToken = page1.nextCursorToken; // Serialisieren und diesen Token an den Client zurückgeben

// Wenn der Client die nächste Seite mit dem Token anfordert:
if (nextToken != null) {
  final page2 = await db.query('users')
      .orderByDesc('id')
      .limit(20)
      .cursor(nextToken); // Token übergeben, um die Position exakt zu bestimmen und zu lesen
}
Funktion Offset-Modus Cursor-Modus
Abfrageleistung Nimmt mit zunehmender Seitenzahl ab Konstante Geschwindigkeit bei tiefer Paginierung
Beste Eignung Kleinere Datensätze, exakte Seitensprünge Riesige Datensätze, unendliches Scrollen
Konsistenz bei Änderungen Datenänderungen können zu doppelten oder übersprungenen Zeilen führen Vermeidet Duplikate und Auslassungen durch Datenänderungen

Speicher-Sonde und synchrone Abfrage (peek)

Für Szenarien mit extremen Anforderungen an Durchsatz und Latenz bietet ToStore die peek-Serie rein synchroner In-Memory-Abfragen und absorbiert Burst-Hot-Reads direkt im Prozess: Edge-Geräte können Millionen Leseanfragen pro Sekunde verarbeiten; Server mit stärkerer Hardware erreichen zig Millionen pro Maschine (siehe Benchmarks).

[!NOTE] Nur In-Memory-Cache: peek ist ein Zero-Scheduling-Rein-Memory-Bypass. Bei Cache-Miss wird sofort leer/null zurückgegeben; die Engine führt kein synchrones Datei-I/O aus (vermeidet Event-Loop-Blockierung unter hoher Parallelität). Für vollständige persistente Ergebnisse await query() in der Anwendung verwenden.

peek-API-Methoden

Methode Rückgabetyp Beschreibung
peekFirst() Map<String, dynamic>? Einzelner Datensatz; null bei Cache-Miss
peek() QueryResult<T> QueryResult mit data-Liste und Paginierungsmetadaten (hasMore, Cursor usw.); Daten nur bei Cache-Treffer
peekExists() bool Prüft synchron, ob ein passender Datensatz im Cache existiert
peekCount() int Zählt synchron passende Datensätze im Cache
result.peekNext() QueryResult<T> Synchrone nächste Seite bei gecachtem Paginierungsergebnis
result.peekPrev() QueryResult<T> Synchrone vorherige Seite bei gecachtem Paginierungsergebnis

Best Practice: Speichersonde zuerst (Peek-Through)

// Einzeldatensatz: Speichersonde zuerst, bei Miss Standard-Async-Abfrage
final q = db.query('users').where('id', '=', userId);
final user = q.peekFirst() ?? await q.first();

// Paginierte Sondenabfrage
final listQ = db.query('users').orderByDesc('id').limit(20);
var page = listQ.peek();
if (page.data.isEmpty) page = await listQ;

if (page.hasMore) {
  final next = page.peekNext(); // Cache-Treffer: synchrone Seitenwechsel
  if (next.data.isEmpty) await page.next();
}

KV-Sonde (db.kv)

Methode Async-Entsprechung Beschreibung
peekGet(key) get(key) Synchrone In-Memory-Punktabfrage; abgelaufene Schlüssel → null
peekExists(key) exists(key) Synchrone Existenzprüfung im Speicher
db.kv.query().peek() await db.kv.query() Synchrone paginierte Sondenabfrage (Präfix / Sortierung / limit)
db.kv.query().peekFirst() await db.kv.query().first() Synchrone Sondenabfrage für ersten Treffer
// Punktabfrage: Sonde zuerst, bei Miss async zurückfallen
final theme = db.kv.peekGet('theme', isGlobal: true) ?? await db.kv.get('theme', isGlobal: true);

// Paginierte KV-Sonde
var page = db.kv.query().prefix('setting_').limit(20).peek();
if (page.data.isEmpty) page = await db.kv.query().prefix('setting_').limit(20);

[!TIP] Empfehlung: Standard-Async-Abfragen (await query()) sichern durch Event-Scheduling langfristige Stabilität und faire Multitasking; 100k+ QPS genügen für die meisten Workloads. Die peek-Serie ist für extreme Hot-Read-Spitzen mit Millionen/Zig-Millionen QPS pro Maschine konzipiert.

Fremdschlüssel und Kaskadierung

Fremdschlüssel garantieren referenzielle Integrität und ermöglichen die Konfiguration kaskadierender Aktualisierungen und Löschungen. Beziehungen werden beim Schreiben und Aktualisieren validiert. Wenn Kaskadenrichtlinien aktiviert sind, werden zugehörige Daten automatisch aktualisiert, wodurch der Konsistenzaufwand im Anwendungscode reduziert wird.

await db.createTables([
  const TableSchema(
    name: 'users',
    primaryKeyConfig: PrimaryKeyConfig(name: 'id'),
    fields: [
      FieldSchema(name: 'username', type: DataType.text, nullable: false),
    ],
  ),
  TableSchema(
    name: 'posts',
    primaryKeyConfig: const PrimaryKeyConfig(name: 'id'),
    fields: [
      const FieldSchema(name: 'title', type: DataType.text, nullable: false),
      const FieldSchema(name: 'user_id', type: DataType.integer, nullable: false),
      const FieldSchema(name: 'content', type: DataType.text),
    ],
    foreignKeys: [
        ForeignKeySchema(
          name: 'fk_posts_user',
          fields: ['user_id'],              // Field in the current table
          referencedTable: 'users',         // Referenced table
          referencedFields: ['id'],         // Referenced field
          onDelete: ForeignKeyCascadeAction.cascade,  // Delete posts automatically when the user is deleted
          onUpdate: ForeignKeyCascadeAction.cascade,  // Cascade updates
        ),
    ],
  ),
]);

Abfrageoperatoren

Alle where(field, operator, value)-Bedingungen unterstützen die folgenden Operatoren (ohne Berücksichtigung der Groß-/Kleinschreibung):

Operator Beschreibung Beispiel / Leistung
= Gleich where('status', '=', 'val') — [Empfohlen] Index-Seek
!=, <> Ungleich where('role', '!=', 'val') — [Vorsicht] Vollständiger Tabellenscan
> , >=, <, <= Vergleich where('age', '>', 18) — [Empfohlen] Index-Scan
IN In Liste where('id', 'IN', [...]) — [Empfohlen] Index-Seek
NOT IN Nicht in der Liste where('status', 'NOT IN', [...]) — [Vorsicht] Vollständiger Tabellenscan
BETWEEN Bereich where('age', 'BETWEEN', [18, 65]) — [Empfohlen] Index-Scan
LIKE Musterübereinstimmung (% = beliebige Zeichen, _ = einzelnes Zeichen) where('name', 'LIKE', 'John%') — [Vorsicht] Siehe Hinweis unten
NOT LIKE Musterkonflikt where('email', 'NOT LIKE', '...') — [Vorsicht] Vollständiger Tabellenscan
IS Ist null where('deleted_at', 'IS', null) — [Empfohlen] Index-Seek
IS NOT Ist nicht null where('email', 'IS NOT', null) — [Vorsicht] Vollständiger Tabellenscan

Semantische Abfragemethoden (empfohlen)

Empfohlen, um handgeschriebene Operatorzeichenfolgen zu vermeiden und eine bessere IDE-Unterstützung zu erhalten.

1. Vergleich

Wird für direkte numerische Vergleiche oder Zeichenfolgenvergleiche verwendet.

db.query('users').whereEqual('username', 'John');           // Equal
db.query('users').whereNotEqual('role', 'guest');          // Not equal
db.query('users').whereGreaterThan('age', 18);             // Greater than
db.query('users').whereGreaterThanOrEqualTo('score', 60);  // Greater than or equal
db.query('users').whereLessThan('price', 100);             // Less than
db.query('users').whereLessThanOrEqualTo('quantity', 10);  // Less than or equal
db.query('users').whereTrue('is_active');                  // Is true
db.query('users').whereFalse('is_banned');                 // Is false

2. Kollektion und Sortiment

Wird verwendet, um zu testen, ob ein Feld innerhalb einer Menge oder eines Bereichs liegt.

db.query('users').whereIn('id', ['id1', 'id2']);                 // In list
db.query('users').whereNotIn('status', ['banned', 'pending']);   // Not in list
db.query('users').whereBetween('age', 18, 65);                   // In range (inclusive)

3. Nullprüfung

Wird verwendet, um zu testen, ob ein Feld einen Wert hat.

db.query('users').whereNull('deleted_at');    // Is null
db.query('users').whereNotNull('email');      // Is not null
db.query('users').whereEmpty('nickname');     // Is null or empty string
db.query('users').whereNotEmpty('bio');       // Is not null and not empty

4. Mustervergleich

Unterstützt die Platzhaltersuche im SQL-Stil ((% entspricht einer beliebigen Anzahl von Zeichen, _ entspricht einem einzelnen Zeichen).

db.query('users').whereLike('name', 'John%');                        // SQL-style pattern match
db.query('users').whereContains('bio', 'flutter');                   // Contains match (LIKE '%value%')
db.query('users').whereStartsWith('name', 'Admin');                  // Prefix match (LIKE 'value%')
db.query('users').whereEndsWith('email', '.com');                    // Suffix match (LIKE '%value')
db.query('users').whereContainsAny('tags', ['dart', 'flutter']);     // Fuzzy match against any item in the list
// Equivalent to: .where('age', '>', 18).where('name', 'like', '%John%')
final users = await db.query('users')
    .whereGreaterThan('age', 18)
    .whereLike('username', '%John%')
    .orderByDesc('age')
    .limit(20);

[!CAUTION] Leitfaden zur Abfrageleistung (Index vs. Vollscan)

Befolgen Sie in Szenarien mit großen Datenmengen (Millionen von Zeilen oder mehr) diese Prinzipien, um Verzögerungen im Haupt-Thread und Abfrage-Timeouts zu vermeiden:

  1. Index-optimiert - [Empfohlen]:

    • Semantische Methoden: whereEqual, whereGreaterThan, whereLessThan, whereIn, whereBetween, whereNull, whereTrue, whereFalse und whereStartsWith (Präfix-Übereinstimmung).
    • Operatoren: =, >, <, >=, <=, IN, BETWEEN, IS null, LIKE 'prefix%'.
    • Erklärung: Diese Operationen erreichen eine superschnelle Positionierung über Indizes. Bei whereStartsWith / LIKE 'abc%' kann der Index weiterhin einen Präfix-Bereichsscan durchführen.
  2. Risiken bei Vollscan - [Vorsicht]:

    • Unscharfe Übereinstimmung: whereContains (LIKE '%val%'), whereEndsWith (LIKE '%val'), whereContainsAny.
    • Negationsabfragen: whereNotEqual (!=, <>), whereNotIn (NOT IN), whereNotNull (IS NOT null/whereNotEmpty).
    • Musterkonflikt: NOT LIKE.
    • Erklärung: Die oben genannten Operationen erfordern in der Regel das Durchlaufen des gesamten Datenspeicherbereichs, selbst wenn ein Index erstellt wurde. Während die Auswirkungen auf mobilen oder kleinen Datensätzen minimal sind, sollten sie in verteilten oder extrem großen Datenanalyseszenarien mit Vorsicht verwendet werden, kombiniert mit anderen Indexbedingungen (z. B. Filtern nach ID oder Zeitbereich) und der limit-Klausel.

Verteilte Architektur

// Configure distributed nodes
final db = await ToStore.open(
  config: DataStoreConfig(
    distributedNodeConfig: const DistributedNodeConfig(
      enableDistributed: true,            // Enable distributed mode
      clusterId: 1,                       // Cluster ID
      centralServerUrl: 'https://127.0.0.1:8080',
      accessToken: 'b7628a4f9b4d269b98649129'
    )
  )
);

// Batch insert
await db.batchInsert('vector_data', [
  {'vector_name': 'face_2365', 'timestamp': DateTime.now()},
  {'vector_name': 'face_2366', 'timestamp': DateTime.now()},
  // ... efficient one-shot insertion of vector records
]);

// Stream and process large datasets
await for (final record in db.streamQuery('vector_data')
  .where('vector_name', '=', 'face_2366')
  .where('timestamp', '>=', DateTime.now().subtract(Duration(days: 30)))
  .stream) {
  // Process each result incrementally to avoid loading everything at once
  print(record);
}

Beispiele für Primärschlüssel

ToStore bietet mehrere verteilte Primärschlüsselalgorithmen für verschiedene Geschäftsszenarien:

  • Sequentieller Primärschlüssel (PrimaryKeyType.sequential): 238978991
  • Zeitstempelbasierter Primärschlüssel (PrimaryKeyType.timestampBased): 1306866018836946
  • Primärschlüssel mit Datumspräfix (PrimaryKeyType.datePrefixed): 20250530182215887631
  • Kurzcode-Primärschlüssel (PrimaryKeyType.shortCode): 9eXrF0qeXZ
// Sequential primary key configuration example
await db.createTables([
  const TableSchema(
    name: 'users',
    primaryKeyConfig: PrimaryKeyConfig(
      type: PrimaryKeyType.sequential,
      sequentialConfig: SequentialIdConfig(
        initialValue: 10000,      // Starting value
        increment: 50,            // Step size
        useRandomIncrement: true, // Random step size to hide business volume
      ),
    ),
    fields: [/* field definitions */]
  ),
]);

Atomare Ausdrücke

Das Ausdruckssystem stellt typsichere Aktualisierungen atomarer Felder bereit. Alle Berechnungen werden atomar auf der Datenbankebene ausgeführt, wodurch gleichzeitige Konflikte vermieden werden:

// Simple increment: balance = balance + 100
await db.update('accounts', {
  'balance': Expr.field('balance') + Expr.value(100),
}).where('id', '=', accountId);

// Complex calculation: total = price * quantity + tax
await db.update('orders', {
  'total': Expr.field('price') * Expr.field('quantity') + Expr.field('tax'),
}).where('id', '=', orderId);

// Multi-layer parentheses: finalPrice = ((price * quantity) + tax) * (1 - discount)
await db.update('orders', {
  'finalPrice': ((Expr.field('price') * Expr.field('quantity')) + Expr.field('tax')) *
                 (Expr.value(1) - Expr.field('discount')),
}).where('id', '=', orderId);

// Use functions: price = min(price, maxPrice)
await db.update('products', {
  'price': Expr.min(Expr.field('price'), Expr.field('maxPrice')),
}).where('id', '=', productId);

// Timestamp: updatedAt = now()
await db.update('users', {
  'updatedAt': Expr.now(),
}).where('id', '=', userId);

Bedingte Ausdrücke (z. B. Unterscheidung zwischen Update und Insert in einem Upsert): Verwenden Sie Expr.isUpdate() / Expr.isInsert() zusammen mit Expr.ifElse oder Expr.when, sodass der Ausdruck nur bei der Aktualisierung oder nur beim Einfügen ausgewertet wird.

// Upsert: increment on update, set to 1 on insert
// The insert branch can use a plain literal; expressions are only evaluated on the update path
await db.upsert('counters', {
  'key': 'visits',
  'count': Expr.ifElse(
    Expr.isUpdate(),
    Expr.field('count') + Expr.value(1),
    1,
  ),
});

// Use Expr.when (single branch, otherwise null)
await db.upsert('orders', {
  'id': orderId,
  'updatedAt': Expr.when(Expr.isUpdate(), Expr.now(), otherwise: Expr.now()),
});

Transaktionen

Transaktionen stellen die Atomizität über mehrere Vorgänge hinweg sicher: Entweder ist alles erfolgreich oder alles wird zurückgesetzt, wodurch die Datenkonsistenz gewahrt bleibt.

Transaktionsmerkmale

  • Mehrere Vorgänge sind entweder alle erfolgreich oder werden alle rückgängig gemacht
  • Unvollendete Arbeiten werden nach Abstürzen automatisch wiederhergestellt
  • Erfolgreiche Vorgänge werden sicher aufrechterhalten
// Basic transaction - atomically commit multiple operations
final txResult = await db.transaction(() async {
  // Insert a user
  await db.insert('users', {
    'username': 'john',
    'email': '[email protected]',
    'fans': 100,
  });

  // Atomic update using an expression
  await db.update('users', {
    'fans': Expr.field('fans') + Expr.value(50),
  }).where('username', '=', 'john');

  // If any operation fails, all changes are rolled back automatically
});

if (!txResult.hasErrors) {
  print('Transaktion erfolgreich übermittelt');
} else {
  print('Transaktions-Rollback aus folgenden Gründen:');
  for (final status in txResult.statuses) {
    if (status.type != ResultType.success) {
      print(' - [$status.codeKey}] $status.message}');
    }
  }
}

// Automatic rollback on error
final txResult2 = await db.transaction(() async {
  await db.insert('users', {
    'username': 'jane',
    'email': '[email protected]',
  });
  throw Exception('Business logic error'); // Trigger rollback
}, rollbackOnError: true);

Verwaltung und Wartung

Die folgenden APIs decken die Datenbankverwaltung, Diagnose und Wartung für die Entwicklung im Plugin-Stil, Admin-Panels und Betriebsszenarien ab:

  • Tischverwaltung
    • createTable(schema): eine einzelne Tabelle manuell erstellen; Nützlich zum Laden von Modulen oder zum Erstellen von Laufzeittabellen bei Bedarf
    • getTableSchema(tableName): Rufen Sie die definierten Schemainformationen ab; nützlich für die automatisierte Validierung oder UI-Modellgenerierung
    • getTableNames({isGlobal}): Tabellennamen im globalen Schema-Inventar auflisten (Benutzertabellen). Optional isGlobal: true nur global, false nur nicht-global, weglassen = beide. Nicht-globale Schemas sind raumübergreifend geteilt; nur Daten sind raumisoliert.
    • getTableInfo(tableName): Laufzeitstatistiken abrufen (totalRecordCount, totalTableDataSizeBytes, totalIndexDataSizeBytes, indexCount, Erstellungszeit, ob global)
    • clear(tableName): Alle Tabellendaten löschen und dabei Schema, Indizes und interne/externe Schlüsseleinschränkungen sicher beibehalten
    • dropTable(tableName): Eine Tabelle und ihr Schema vollständig zerstören; nicht reversibel
  • Raumverwaltung
    • currentSpaceName: Erhalten Sie den aktuell aktiven Bereich in Echtzeit
    • listSpaces(): Listet alle zugewiesenen Bereiche in der aktuellen Datenbankinstanz auf
    • getSpaceInfo(useCache: true): raumlokale Aggregate (totalRecordCount, Tabellen-/Indexdatengröße). useCache: false für Meta-Reconcile.
    • deleteSpace(spaceName): Löschen Sie einen bestimmten Bereich und alle seine Daten, außer default und den aktuell aktiven Bereich
  • Instanzerkennung
    • config: Überprüfen Sie den endgültigen gültigen DataStoreConfig Snapshot für die Instanz
    • instancePath: Lokalisieren Sie das physische Speicherverzeichnis genau
    • getVersion() / setVersion(version): geschäftsdefinierte Versionskontrolle für Migrationsentscheidungen auf Anwendungsebene (nicht die Engine-Version)
  • Wartung
    • flush(mode: FlushMode.full): erzwingt das Schreiben ausstehender Daten in den Speicher. Modi: recovery (schnellste; Wiederherstellungsprotokoll nur in OS-Puffer; übersteht App-Absturz, nicht Stromausfall), recoveryDurable (Wiederherstellungsprotokoll auf stabilen Speicher; zielt auf Überstehen von Stromausfällen), full (Standard; Wiederherstellungsprotokoll + Tabellen- und Indexdaten auf stabilen Speicher).
    • deleteDatabase(): alle physischen Dateien und Metadaten für die aktuelle Instanz entfernen; mit Vorsicht verwenden
  • Diagnose
    • db.status.memory(): Überprüfen Sie die Cache-Trefferquoten, die Indexseitennutzung und die gesamte Heap-Zuordnung
    • db.status.space() / db.status.table(tableName): Überprüfen Sie Live-Statistiken und Gesundheitsinformationen für Räume und Tabellen
    • db.status.config(): Überprüfen Sie den aktuellen Snapshot der Laufzeitkonfiguration
    • db.status.migration(taskId): Verfolgen Sie den Fortschritt der asynchronen Migration in Echtzeit

final spaces = await db.listSpaces();
final tableNames = await db.getTableNames();
final spaceInfo = await db.getSpaceInfo(useCache: false);
final tableSchema = await db.getTableSchema('users');
final tableInfo = await db.getTableInfo('users');

print('spaces: $spaces');
print('tables: $tableNames');
print(spaceInfo.toJson());
print(tableSchema?.toJson());
print(tableInfo?.toJson());

await db.flush();

final memoryInfo = await db.status.memory();
final configInfo = await db.status.config();
print(memoryInfo.toJson());
print(configInfo.toJson());

Sichern und Wiederherstellen

Besonders nützlich für den lokalen Import/Export für Einzelbenutzer, die Migration großer Offline-Daten und das System-Rollback nach einem Fehler:

  • Sicherung (backup)
    • compress: ob die Komprimierung aktiviert werden soll; empfohlen und standardmäßig aktiviert
    • scope: steuert den Backup-Bereich
      • BackupScope.database: Sichert die gesamte Datenbankinstanz, einschließlich aller Leerzeichen und globalen Tabellen
      • BackupScope.currentSpace: Sichert nur den aktuell aktiven Speicherplatz, ausgenommen globale Tabellen
      • BackupScope.currentSpaceWithGlobal: Sichert den aktuellen Speicherplatz und die zugehörigen globalen Tabellen, ideal für die Migration einzelner Mandanten oder einzelner Benutzer
  • Wiederherstellen (restore)
    • backupPath: physischer Pfad zum Backup-Paket
    • cleanupBeforeRestore: ob zugehörige aktuelle Daten vor der Wiederherstellung stillschweigend gelöscht werden sollen; true wird empfohlen, um gemischte logische Zustände zu vermeiden
    • deleteAfterRestore: Nach erfolgreicher Wiederherstellung wird die Sicherungsquelldatei automatisch gelöscht
// Example: export the full data package for the current user
final backupPath = await db.backup(
  compress: true,
  scope: BackupScope.currentSpaceWithGlobal,
);

// Example: restore from a backup package and clean up the source file automatically
final restored = await db.restore(
  backupPath,
  cleanupBeforeRestore: true,
  deleteAfterRestore: true,
);

Statuscodes und Fehlerbehandlung

Es gibt zwei Kanäle für Fehler- und Ausnahme-Feedback in ToStore:

[!NOTE] Einheitliche Diagnosebasis: Unabhängig davon, ob sie über das Antwort-Ergebnismodell (statuses in DbResult/QueryResult) zurückgegeben oder über fatale Ausnahmen (statuses in DbException) ausgelöst werden, basieren alle Diagnosezustände einheitlich auf dem strukturierten ResultStatus-System und teilen dieselben Statuscodes, was Konsistenz garantiert.

  1. Antwort-Ergebnismodell (Result-based Response) Für alltägliche Operationen wie Einfügen, Aktualisieren, Löschen, Abfragen, Transaktionen und Tabellenstrukturänderungen zur Laufzeit. Diese Operationen lösen keine Ausnahmen aus, wenn Einschränkungsverletzungen, Validierungsfehler oder ungültige Argumente auftreten. Stattdessen verpackt ToStore die Ergebnisse in DbResult oder QueryResult und zeichnet alle Diagnoseinformationen in der Statusliste auf. Dies garantiert, dass gewöhnliche geschäftslogische Fehler die Datenbank nicht unterbrechen.
  • hasErrors: Gibt an, ob bei der aktuellen Operation Fehler aufgetreten sind. Bei Batch-Operationen oder Transaktionen ist diese Eigenschaft true, wenn mindestens ein Fehler vorliegt.
  • statuses: Eine detaillierte Liste aller ResultStatus-Diagnosen der Operation. Sie unterstützt eine 1:1-Reihenfolgeübereinstimmung, was für Batch-Operationen sehr nützlich ist.
  • firstPrimaryKey: Liest den physisch generierten Primärschlüssel bei einer einzelnen Einfüge-/Schreiboperation direkt aus, ohne die statuses manuell parsen zu müssen.
  • ResultType: Enum für die Zustandskategorie, praktisch für Verzweigungen (z. B. isBusinessError, isDeveloperError).
  1. Ausnahmen auslösen (Exception-based Throwing) Für fatale Fehler, die durch Entwicklerfehler oder Designfehler verursacht werden (z. B. Schemaüberprüfungsfehler während ToStore.open, Inkompatibilität der Engine-Version, schwerwiegende Datenmigrationsfehler usw.). In diesen Fällen löst ToStore eine DbException aus, um die Ausführung zu stoppen und den Entwickler zur Behebung aufzufordern.

[!WARNING] Entwicklungsrichtlinien: Gewöhnliche Geschäftsfehler dürfen keine Ausnahmen auslösen. Sie müssen im Antwort-Ergebnismodell zurückgegeben werden, um den Anwendungsbetrieb nicht zu stören.


Beispiele für Fehler und Ausnahmen

1. Fehlerbehandlung bei einer einzelnen Schreiboperation

final result = await db.insert('users', {
  'username': 'john',
  'email': '[email protected]',
});

if (result.hasErrors) {
  // Ersten Fehlertyp und Beschreibung abrufen
  print('Operation failed: [\${result.firstType.codeKey}] \${result.message}');
} else {
  print('Schreiben erfolgreich, Primärschlüssel ist: \${result.firstPrimaryKey}');
}

2. Batch-Schreiboperationen und detaillierte Diagnose

final batchResult = await db.batchInsert('users', [
  {'username': 'alice', 'email': '[email protected]'},
  {'username': 'bob', 'email': 'invalid-email-format'}, // Validation fails
]);

if (batchResult.hasErrors) {
  print('Batch-Operation teilweise fehlgeschlagen: erfolgreich \${batchResult.successCount}, fehlgeschlagen \${batchResult.failedCount}');
  
  for (final status in batchResult.statuses) {
    final int idx = status.index;
    
    if (status is ConstraintStatus) {
      print('Index [\$idx] Einschränkungsverletzung! Tabelle! Tabelle: \${status.tableName}, Felder: \${status.fields}');
    } else if (status is InvalidArgumentStatus) {
      print('Index [\$idx] Argumentfehler! Parameter! Parameter: \${status.parameterName}, übergebener Wert: \${status.passedValue}');
    } else if (status.type != ResultType.success) {
      print('Index [\$idx] Fehler aufgetreten: [\${status.codeKey}] \${status.message}');
    }
  }
}

3. Fatale Fehler und Abfangen von Initialisierungsausnahmen (DbException)

try {
  // Initialize database with schemas that might have validation issues
  final db = await ToStore.open(schemas: appSchemas);
} on DbException catch (e) {
  print('❌ Fatale Datenbank-Ausnahme! Fehlermeldung: \n\${e.message}');
  
  // Iterate through the detailed status list in the exception
  for (final status in e.statuses) {
    if (status is SchemaValidationStatus) {
      print('Schema-Validierung fehlgeschlagen! Tabelle! Tabelle: \${status.tableName}, Feld: \${status.field}, ungültige Konfiguration: \${status.wrongValue}');
    } else {
      print('Diagnose-Info: [\${status.codeKey}] \${status.message}');
    }
  }
}

Die vollständige Liste der Fehlertypen, Statuscodes, JSON-Serialisierungsformate und Feldzuordnungen finden Sie in der Spezifikation: ToStore ResultStatus-Spezifikation für automatische Diagnose und Statusauflösung.

Protokollrückruf und Datenbankdiagnose

ToStore kann Datenbank-Lebenszyklusprotokolle über ToStore.setLogConfig(...) an die Geschäftsschicht weiterleiten.

  • Der onLog-Callback empfängt alle LogRecord-Protokolleinträge, die die aktuellen Filter enableLog und logLevel passieren.
    • LogLevel.error: Ein lokaler Fehler ist aufgetreten, der den normalen Betrieb nicht beeinträchtigt.
    • LogLevel.critical: Globaler Fehler auf Katastrophenebene (z. B. Festplatte voll, unzureichender Arbeitsspeicher, schwerwiegender Migrationsfehler usw.), der ein manuelles Eingreifen erfordert. Es wird empfohlen, auf dieser Stufe Alarmbenachrichtigungen auszulösen.
  • Rufen Sie ToStore.setLogConfig(...) vor der Initialisierung auf, damit auch die während der Initialisierung und der automatischen Migration generierten Protokolle erfasst werden.
  // Protokollparameter oder Callback konfigurieren
  ToStore.setLogConfig(
    enableLog: true,
    logLevel: debugMode ? LogLevel.debug : LogLevel.warn,
    logLabel: 'my_app_db', // Hellgrauer Header-Titel oben im Protokoll zur Unterscheidung von Apps oder Datenbankinstanzen,
    onLog: (log) {
      // In der Produktion können warn/error/critical an das Backend oder die Protokollplattform gemeldet werden
      // log.level entspricht der Protokollstufe (LogLevel.debug, info, warn, error, critical)
      // log.message entspricht dem verarbeiteten Protokolltext
      // log.status entspricht dem zugrunde liegenden ResultStatus-Diagnosestatus (enthält code und codeKey)
      if (!debugMode && (log.level == LogLevel.warn || log.level == LogLevel.error || log.level == LogLevel.critical)) {
        developer.log(log.message, name: 'my_app_db', time: log.timestamp);
      }
    },
  );

  final db = await ToStore.open();

Sicherheitskonfiguration

[!WARNING] Schlüsselverwaltung

Schlüssel Rolle Änderung Vollständige Datenmigration?
encodingKey Datenverschlüsselungsschlüssel Neuen Wert setzen und erneut open Ja (zeitaufwendig)
encryptionKey Sicherheitsschlüssel; schützt encodingKey Zur Laufzeit db.rotateEncryptionKey aufrufen Nein (schnell)

Sensible Schlüssel niemals fest kodieren. Zur Gerätebindung encryptionKey im OS-Keychain / Keystore / Secure Enclave speichern und an die Engine übergeben.

final db = await ToStore.open(
  config: DataStoreConfig(
    encryptionConfig: EncryptionConfig(
      // Supported: none, xorObfuscation, chacha20Poly1305, aes256Gcm
      encryptionType: EncryptionType.chacha20Poly1305,

      // Data encryption key: encrypts table/index/log data; changing it triggers a background rewrite
      encodingKey: 'Your-Encoding-Key...',

      // Security key: protects encodingKey; rotate online via db.rotateEncryptionKey
      encryptionKey: 'Your-Secure-Encryption-Key...',

      // standard: critical table data, B-tree indexes, and log payloads
      // full: encrypts the entire engine files
      encryptionScope: EncryptionScope.standard,
    ),
    // Enable crash recovery logging (Write-Ahead Logging), enabled by default
    enableJournal: true,
    // Whether transactions force data to disk on commit; set false to reduce sync overhead
    persistRecoveryOnCommit: true,
  ),
);

encodingKey ändern: neuen Wert in EncryptionConfig setzen und erneut open aufrufen. Die Engine erkennt die Änderung und migriert Daten automatisch im Hintergrund—ohne Eingriff der Anwendung.

encryptionKey rotieren (regelmäßige Sicherheits-/Compliance-Rotation): keine Datenmigration; online ausführbar.

// If encryptionKey was never set explicitly, oldKey can be omitted
final result = await db.rotateEncryptionKey(newKey: 'new-secure-key');
// Or: await db.rotateEncryptionKey(oldKey: 'old-key', newKey: 'new-key');
if (result.hasErrors) {
  // Fehler behandeln (falscher oldKey, encodingKey-Migration läuft usw.)
  return;
}
// Erfolg: In-Memory-Konfiguration aktualisiert; beim nächsten ToStore.open aktuellen encryptionKey übergeben

Verschlüsselung auf Wertebene (ToCrypto)

Die vollständige Datenbankverschlüsselung schützt alle Tabellen- und Indexdaten, kann jedoch die Gesamtleistung beeinträchtigen. Wenn Sie nur wenige sensible Werte schützen müssen, verwenden Sie stattdessen ToCrypto. Es ist von der Datenbank entkoppelt, erfordert keine db-Instanz und ermöglicht Ihrer Anwendung das Kodieren/Dekodieren von Werten vor dem Schreiben oder nach dem Lesen. Die Ausgabe ist Base64, was natürlich in JSON- oder TEXT-Spalten passt.

  • key (erforderlich): String oder Uint8List. Wenn es nicht 32 Bytes sind, wird SHA-256 verwendet, um einen 32-Byte-Schlüssel abzuleiten.
  • type (optional): Verschlüsselungstyp von ToCryptoType, z. B. ToCryptoType.chacha20Poly1305 oder ToCryptoType.aes256Gcm. Standardmäßig ist ToCryptoType.chacha20Poly1305.
  • aad (optional): zusätzliche authentifizierte Daten vom Typ Uint8List. Wenn sie während der Codierung bereitgestellt werden, müssen genau dieselben Bytes auch während der Decodierung bereitgestellt werden.
const key = 'my-secret-key';
// Encode: plaintext -> Base64 ciphertext (can be stored in DB or JSON)
final cipher = ToCrypto.encode('sensitive data', key: key);
// Decode when reading
final plain = ToCrypto.decode(cipher, key: key);

// Optional: bind contextual data with aad (must match during decode)
final aad = Uint8List.fromList(utf8.encode('users:id_number'));
final cipher2 = ToCrypto.encode('secret', key: key, aad: aad);
final plain2 = ToCrypto.decode(cipher2, key: key, aad: aad);

Erweiterte Konfiguration erklärt (DataStoreConfig)

[!TIP] Zero-Config-Intelligenz ToStore erkennt automatisch die Plattform, Leistungsmerkmale, verfügbaren Speicher und E/A-Verhalten, um Parameter wie Parallelität, Shard-Größe und Cache-Budget zu optimieren. In 99 % der gängigen Geschäftsszenarien ist keine manuelle Feinabstimmung von DataStoreConfig erforderlich. Die Standardeinstellungen bieten bereits eine hervorragende Leistung für die aktuelle Plattform.

Parameter Standard Zweck & Empfehlung
yieldDurationMs 8ms Kernempfehlung. Der Zeitabschnitt, der verwendet wird, wenn lange Aufgaben nachgeben. 8ms passt gut zum 120-fps-/60-fps-Rendering und trägt dazu bei, dass die Benutzeroberfläche bei großen Abfragen oder Migrationen reibungslos bleibt.
maxQueryOffset 10000 Abfrageschutz. Wenn offset diesen Schwellenwert überschreitet, wird ein Fehler ausgelöst. Dadurch wird verhindert, dass pathologische E/A durch eine tiefe Offset-Paginierung verursacht werden.
defaultQueryLimit 1000 Ressourcenleitplanke. Wird angewendet, wenn eine Abfrage nicht limit angibt, um ein versehentliches Laden großer Ergebnismengen und potenzielle OOM-Probleme zu verhindern.
cacheMemoryBudgetMB (automatisch) Feingranulare Speicherverwaltung. Gesamtcache-Speicherbudget. Die Engine nutzt es, um die LRU-Rückgewinnung automatisch voranzutreiben.
enableJournal wahr Selbstheilung bei Abstürzen. Wenn diese Option aktiviert ist, kann sich der Motor nach Abstürzen oder Stromausfällen automatisch erholen.
defaultRecoveryDurability standard Wiederherstellungsgarantie bei Antwort. Definiert die Stufe der Selbstheilungswiederherstellung vor Rückgabe des Ergebnisses von Schreiboperationen:
• standard (Standard): Vor der Antwort absturzsicher. Garantiert Selbstheilung gegen App-Abstürze, unbehandelte Ausnahmen und OOM-Kills.
• durable: Vor der Antwort stromausfallsicher. Vor der Rückgabe physisch persistiert; garantiert Selbstheilung auch bei plötzlichem Stromausfall.
• async: Maximaler Durchsatz. Antwortet sofort und speichert im Hintergrund; nach der Rückgabe bei kleinen Datenmengen meist nur ein extrem kurzes Fenster von wenigen Millisekunden bis zur persistenten Speicherung.
persistRecoveryOnCommit wahr Starke Haltbarkeitsgarantie. Wenn dies zutrifft, werden festgeschriebene Transaktionen mit dem physischen Speicher synchronisiert. Bei „false“ erfolgt die Löschung asynchron im Hintergrund, um die Geschwindigkeit zu erhöhen, wobei bei extremen Abstürzen ein geringes Risiko besteht, dass eine kleine Datenmenge verloren geht.
ttlCleanupIntervalMs 300000 Globale TTL-Abfrage. Das Hintergrundintervall zum Scannen abgelaufener Daten, wenn die Engine nicht im Leerlauf ist. Niedrigere Werte löschen abgelaufene Daten früher, kosten aber mehr Overhead.
kvCapacityConfig null Space-KV-Kapazität. FIFO/LRU-Limit für space-bezogene Keys; kein sourceField. Siehe Kapazitätsbereinigung.
globalKvCapacityConfig null Globale KV-Kapazität. Gleiche Form wie kvCapacityConfig.
maxConcurrency (automatisch) Computing-Parallelitätskontrolle. Legt die maximale Anzahl paralleler Worker für intensive Aufgaben wie Vektorberechnung und Verschlüsselung/Entschlüsselung fest. Normalerweise ist es am besten, es automatisch zu lassen.
final db = await ToStore.open(
  config: DataStoreConfig(
    yieldDurationMs: 8, // Excellent for frontend UI smoothness; for servers, 50ms is often better
    defaultQueryLimit: 50, // Force a maximum result-set size
    enableJournal: true, // Ensure crash self-healing
    defaultRecoveryDurability: RecoveryDurability.standard, // Vor der Antwort absturzsicher
  ),
);

Leistung und Erfahrung

Benchmarks

Highlights der 100K-Suite und Milliarden-Edge-Validierung (3 Runden, standard durability, 2026-10-08):

Metric Result
Cold start ~35 ms (stable vs data scale)
PK Read (Hot Cache) 4.5M ops/s
Batch Insert 588K ops/s
Pagination (Hot Cache) 579K ops/s
ANN @ depth 1 (~91%) 36K ops/s

Vollständige Diagramme und alle Operationen: Benchmarks.

ToStore Basic Performance Demo

  • Basis-Performance-Demo (basic-demo.mp4): In der GIF-Vorschau wird möglicherweise nicht alles angezeigt. Bitte öffnen Sie das Video für die vollständige Demonstration. Selbst auf normalen Mobilgeräten bleiben Start, Paging und Abruf stabil und reibungslos, selbst wenn der Datensatz 100 Millionen Datensätze überschreitet.

ToStore Disaster Recovery Stress Test

  • Stresstest für die Notfallwiederherstellung (disaster-recovery.mp4): Bei hochfrequenten Schreibvorgängen wird der Vorgang absichtlich immer wieder unterbrochen, um Abstürze und Stromausfälle zu simulieren. ToStore kann sich schnell erholen.

Erlebnistipps

  • 📱 Beispielprojekt: Das Verzeichnis example enthält eine vollständige Flutter-Anwendung
  • 🚀 Produktions-Builds: Paket und Test im Release-Modus; Die Release-Leistung geht weit über den Debug-Modus hinaus
  • ✅ Standardtests: Kernkompetenzen werden durch standardisierte Tests abgedeckt

Wenn ToStore Ihnen hilft, geben Sie uns bitte ein ⭐️ – das ist eine der besten Möglichkeiten, das Projekt zu unterstützen. Vielen Dank!

🤝 Mitwirken

ToStore ist eine sich stetig weiterentwickelnde moderne Daten-Engine, und wir begrüßen Community-Beiträge sehr. Ob Fehler beheben, Dokumentation verbessern, Architektur optimieren oder neue Ideen vorschlagen – Sie können über PR teilnehmen: