This guide will help you successfully migrate your Ditto C# application from the legacy query builder APIs to the modern DQL (Ditto Query Language). After reviewing this documentation, you’ll understand how to convert method chaining patterns to DQL syntax and systematically update your data operations.
Use this prompt when working with an AI coding assistant to migrate your Ditto C# app from legacy query builder to DQL.
Copy AI Migration Prompt (Click to Expand)
I need help migrating a Ditto C# application from the legacy query builder APIs to modern DQL (Ditto Query Language). This migration involves converting method chaining patterns to SQL-like DQL syntax.CRITICAL RULES:1. All query builder method chains (.Collection().Find()) must be replaced with ditto.Store.ExecuteAsync() using DQL2. Use parameterized queries with :paramName syntax - NEVER string interpolation3. Counter operations must use PN_INCREMENT BY in APPLY clause - do NOT initialize counter fields4. Sync subscriptions must use ditto.Sync.RegisterSubscription() instead of .Find().Subscribe()5. ObserveLocal must be replaced with RegisterObserver and await foreach iteration---CORE MIGRATION AREAS:1. QUERY SYNTAX MIGRATIONBEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars") .Find("color == $args.color", new Dictionary<string, object> { { "color", "red" } }) .Exec();```AFTER (DQL):```csharpawait ditto.Store.ExecuteAsync( "SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } });```2. INSERT OPERATIONSBEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars") .Upsert(new Dictionary<string, object> { { "_id", id }, { "color", "blue" } });```AFTER (DQL):```csharpawait ditto.Store.ExecuteAsync( "INSERT INTO cars DOCUMENTS (:car)", new Dictionary<string, object> { { "car", carData } });```3. UPDATE OPERATIONSBEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars") .FindByID(id) .Update(doc => { doc["color"].Set("green"); });```AFTER (DQL):```csharpawait ditto.Store.ExecuteAsync( "UPDATE cars SET color = :color WHERE _id = :id", new Dictionary<string, object> { { "color", "green" }, { "id", id } });```4. DELETE OPERATIONSBEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars").FindByID(id).Remove();```AFTER (DQL):```csharpawait ditto.Store.ExecuteAsync( "DELETE FROM cars WHERE _id = :id", new Dictionary<string, object> { { "id", id } });```5. EVICTION OPERATIONSBEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars").FindByID(id).Evict();```AFTER (DQL):```csharpawait ditto.Store.ExecuteAsync( "EVICT FROM cars WHERE _id = :id", new Dictionary<string, object> { { "id", id } });```6. COUNTER OPERATIONS (PN_COUNTER)BEFORE (Legacy Query Builder):```csharpditto.Store.Collection("cars") .FindByID(id) .Update(doc => { doc["numUpdates"].Counter?.Increment(1.0); });```AFTER (DQL with PN_INCREMENT):```csharpawait ditto.Store.ExecuteAsync( "UPDATE cars APPLY numUpdates PN_INCREMENT BY :increment WHERE _id = :id", new Dictionary<string, object> { { "increment", 1 }, { "id", id } });```IMPORTANT: Do NOT initialize counter fields in documents:```csharp// WRONG - Creates a register, not a counternew Dictionary<string, object> { { "counter", 0 } }// CORRECT - Omit counter field, it's created on first PN_INCREMENTnew Dictionary<string, object> { { "_id", id }, { "color", "blue" } }```7. DOCUMENT FIELD ACCESS MIGRATIONBEFORE (Legacy Query Builder):```csharpvar document = ditto.Store.Collection("cars").FindByID(id).Exec();var color = document?.Value["color"];```AFTER (DQL):```csharpvar result = await ditto.Store.ExecuteAsync(dqlString);var item = result.Items.FirstOrDefault();var color = item?.Value["color"];```8. OBSERVER MIGRATION (ObserveLocal → RegisterObserver)BEFORE (Legacy ObserveLocal):```csharpvar liveQuery = ditto.Store.Collection("cars") .Find($"_id.locationId == '{Constants.LocationId}'") .ObserveLocal((docs, ev) => { if (ev is DittoLiveQueryEvent.Update update) { // Handle changes } else if (ev is DittoLiveQueryEvent.Initial) { // Handle initial data } });```AFTER (DQL with RegisterObserver):```csharpawait foreach (var result in ditto.Store.RegisterObserver( "SELECT * FROM cars WHERE _id.locationId = :locationId", new Dictionary<string, object> { { "locationId", Constants.LocationId } })){ // Process result items var items = result.Items; // Update UI UpdateUI(items);}```9. SYNC SUBSCRIPTIONS MIGRATIONBEFORE (Legacy Query Builder):```csharpvar subscription = ditto.Store.Collection("cars") .Find("color == $args.color", new Dictionary<string, object> { { "color", "red" } }) .Subscribe();```AFTER (DQL):```csharpvar subscription = ditto.Sync.RegisterSubscription( "SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } });```---COMMON PITFALLS TO AVOID:1. DQL Syntax Errors - Use :paramName for parameters, not $args.paramName or string interpolation2. Missing Parameter Binding - NEVER use string interpolation in queries - Always use parameterized queries with Dictionary<string, object>3. Counter Type Errors - Use COUNTER annotation: `COLLECTION my_collection (my_count COUNTER)` - Do NOT use SET with COUNTER - Use APPLY with INCREMENT BY and RESET - Pass negative values for decrements4. Memory Management with Observers - Use await foreach for automatic cleanup - Break from loop to stop observation - Use indexes for improved memory and performance5. Attachment Handling - Use ATTACHMENT annotation: `COLLECTION my_collection (image ATTACHMENT)` - Create attachments with ditto.Store.NewAttachmentAsync()---MIGRATION CHECKLIST:Search for these legacy patterns and replace:- [ ] .Collection( → await ditto.Store.ExecuteAsync("SELECT * FROM- [ ] .Find( → Convert to DQL WHERE clause with Dictionary arguments- [ ] .FindByID( → Convert to DQL WHERE _id = :id- [ ] .Upsert( → Convert to DQL INSERT INTO- [ ] .Update( → Convert to DQL UPDATE SET- [ ] .Remove( → Convert to DQL DELETE FROM- [ ] .Evict( → Convert to DQL EVICT FROM- [ ] .Counter?.Increment( → Convert to PN_INCREMENT BY in APPLY clause- [ ] new DittoCounter() → Remove initialization, use PN_INCREMENT- [ ] .ObserveLocal( → Convert to RegisterObserver with await foreach- [ ] .Subscribe() → Convert to ditto.Sync.RegisterSubscription()---Please help me convert all legacy query builder patterns in my codebase to DQL syntax. Focus on:1. Maintaining the same functionality2. Using proper parameterized queries with Dictionary<string, object>3. Handling counter operations correctly with PN_INCREMENT4. Implementing proper observer cleanup with await foreach5. Converting all sync subscriptions to DQLStart by identifying all uses of .Collection() in my codebase and systematically converting each one to the appropriate DQL pattern.
await ditto.Store.ExecuteAsync( "SELECT * FROM cars");
Document Query by ID
await ditto.Store.ExecuteAsync( "SELECT * FROM cars WHERE _id = '123'");
Document Query with Predicate
await ditto.Store.ExecuteAsync( "SELECT * FROM cars WHERE color = 'blue'");
Document Query with Arguments
await ditto.Store.ExecuteAsync( "SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } });
Document Insert
await ditto.Store.ExecuteAsync( "INSERT INTO cars DOCUMENTS (:car)", new Dictionary<string, object> { { "car", carData } });
Document Update
await ditto.Store.ExecuteAsync( "UPDATE cars SET color = :color WHERE _id = :id", new Dictionary<string, object> { { "color", "green" }, { "id", id } });
Document Delete
await ditto.Store.ExecuteAsync( "DELETE FROM cars WHERE _id = :id", new Dictionary<string, object> { { "id", id } });
Document Local Eviction
// Evict by IDawait ditto.Store.ExecuteAsync( "EVICT FROM cars WHERE _id = :id", new Dictionary<string, object> { { "id", id } });// Evict all matching documentsawait ditto.Store.ExecuteAsync( "EVICT FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } });
var result = await ditto.Store.ExecuteAsync(dqlString);using var item = result.Items.FirstOrDefault();var color = item?.Value["color"];
Legacy Query Builder → Modern Document Conversion
// Extract data and dispose immediately — do not pass QueryResultItem outside this scopeCar DocumentToCar(DittoQueryResultItem item){ var id = item.Value["_id"]?.ToString(); var color = item.Value["color"]?.ToString(); item.Dematerialize(); item.Dispose(); return new Car { Id = id, Color = color };}
Legacy Query Builder → DQL Store Observer Migration
await foreach (var result in ditto.Store.RegisterObserver( "SELECT * FROM cars WHERE _id.locationId = :locationId", new Dictionary<string, object> { { "locationId", Constants.LocationId } })){ // Deserialize and dispose items within loop scope — do not pass QueryResultItems outside var cars = result.Items.Select((item) => { var itemJsonString = item.JsonString(); item.Dispose(); return JsonSerializer.Deserialize<Car>(itemJsonString); }).ToList(); // Update UI UpdateUI(cars);}
Performance Consideration: DQL observers provide more advanced return results including aggregates and projections. This requires more database full scans to ensure consistent results compared to the legacy query builder.Use indexes on query fields to maintain and improve observer performance. Indexes ensure your observers remain functional with optimal query performance.
Best Practice: Create Indexes for Observer Queries
// Create index on frequently queried fieldsawait ditto.Store.Execute(""" CREATE INDEX idx_cars_locationId ON cars (_id.locationId)""");// Then register observer - queries will use the indexawait foreach (var result in ditto.Store.RegisterObserver( "SELECT * FROM cars WHERE _id.locationId = :locationId", new Dictionary<string, object> { { "locationId", Constants.LocationId } })){ // Process results}
Legacy Query Builder → DQL Sync SubscriptionsSubscribe with Query
var subscription = ditto.Sync.RegisterSubscription( "SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } });
Subscribe with Parameters
var subscription = ditto.Sync.RegisterSubscription( "SELECT * FROM cars WHERE _id.locationId = :locationId", new Dictionary<string, object> { { "locationId", Constants.LocationId } });
Multiple Subscriptions
var subscriptions = new List<DittoSyncSubscription>();subscriptions.Add( ditto.Sync.RegisterSubscription( "SELECT * FROM cars WHERE color = :color", new Dictionary<string, object> { { "color", "red" } } ));subscriptions.Add( ditto.Sync.RegisterSubscription( "SELECT * FROM cars WHERE year > :year", new Dictionary<string, object> { { "year", 2020 } } ));
Cancel Subscription
subscription.Cancel();
Subscribe to All Documents
var subscription = ditto.Sync.RegisterSubscription( "SELECT * FROM cars");
PN_COUNTER is the DQL equivalent of the legacy DittoCounter type. When migrating counter operations from the legacy query builder’s counter methods, use PN_INCREMENT BY in the APPLY clause. This maintains full compatibility with existing counter data created by DittoCounter.
Counter Increment
await ditto.Store.ExecuteAsync( "UPDATE cars APPLY numUpdates PN_INCREMENT BY :increment WHERE _id = :id", new Dictionary<string, object> { { "increment", 1 }, { "id", id } });
Counter Decrement
await ditto.Store.ExecuteAsync( "UPDATE cars APPLY viewCount PN_INCREMENT BY :decrement WHERE _id = :id", new Dictionary<string, object> { { "decrement", -1 }, { "id", id } });
Initialize Counter in Document
// Counter fields are automatically created on first PN_INCREMENT useawait ditto.Store.ExecuteAsync( "INSERT INTO cars DOCUMENTS (:car)", new Dictionary<string, object> { { "car", new Dictionary<string, object> { { "_id", id }, { "color", "blue" } // Do NOT initialize counter fields - they are created on first PN_INCREMENT }} });// Then use PN_INCREMENT with APPLY clause to create and increment the counterawait ditto.Store.ExecuteAsync( "UPDATE cars APPLY numUpdates PN_INCREMENT BY 1 WHERE _id = :id", new Dictionary<string, object> { { "id", id } });
Multiple Counter Operations
await ditto.Store.ExecuteAsync( @"UPDATE cars APPLY likes PN_INCREMENT BY :likeIncrement, dislikes PN_INCREMENT BY :dislikeDecrement, views PN_INCREMENT BY :viewIncrement WHERE _id = :id", new Dictionary<string, object> { { "likeIncrement", 1 }, { "dislikeDecrement", -1 }, { "viewIncrement", 1 }, { "id", id } });
// Create attachment using store — use `using` to dispose when doneusing var attachment = await ditto.Store.NewAttachmentAsync( filePath, metadata);// Store attachment with DQLawait ditto.Store.ExecuteAsync( "INSERT INTO COLLECTION cars (image ATTACHMENT) DOCUMENTS (:doc)", new Dictionary<string, object> { { "doc", docWithAttachment } });
Attachment Fetching
// Fetch attachment with progress callback — use `using` to dispose when doneusing var fetchResult = await ditto.Store.FetchAttachmentAsync( attachmentToken, (ev) => { if (ev is DittoAttachmentFetchEvent.Progress progress) { UpdateProgress(progress.DownloadedBytes, progress.TotalBytes); } });
DQL observers and queries benefit significantly from proper indexing. When migrating from the legacy query builder to DQL, creating indexes on frequently queried fields is essential for maintaining optimal performance.Why Indexes Matter for DQL:
DQL observers support advanced features like aggregates and projections
These advanced features require full database scans to ensure consistent results
Indexes dramatically reduce query execution time by avoiding full scans
Combining indexes with observers provides better performance than legacy query builder
Creating Indexes:
// Create index on single fieldawait ditto.Store.Execute(""" CREATE INDEX idx_cars_color ON cars (color)""");// Create compound index on multiple fieldsawait ditto.Store.Execute(""" CREATE INDEX idx_cars_color_year ON cars (color, year)""");// Create index on nested fieldawait ditto.Store.Execute(""" CREATE INDEX idx_cars_location ON cars (_id.locationId)""");
Best Practices:
Create indexes on fields used in WHERE clauses
Create indexes before registering observers for those queries
Use compound indexes for queries with multiple filter conditions
Monitor query performance and add indexes as needed
For comprehensive information on indexing strategies, syntax, and best practices, see the DQL Indexing documentation.
Problem: Legacy counter operations not working with DQLSolution: Use PN_INCREMENT function for counter operations:
// Wrong: Using legacy counter methodsdoc["counter"].Counter?.Increment(1.0);// Wrong: Initializing counter with a number (creates a register, not a counter!)await ditto.Store.ExecuteAsync( "INSERT INTO items DOCUMENTS (:doc)", new Dictionary<string, object> { { "doc", new Dictionary<string, object> { { "counter", 0 } } } });// Correct: Using PN_INCREMENT with APPLY clause (creates counter on first use)await ditto.Store.ExecuteAsync( "UPDATE items APPLY counter PN_INCREMENT BY :value WHERE _id = :id", new Dictionary<string, object> { { "value", 1 }, { "id", itemId } });
Problem: Holding DittoQueryResultItem references beyond their intended scope leads to memory issues because each item maps to native Rust memory via FFI.Solution: Always extract the data you need and Dispose() the item within the same scope. Use one of the two recommended patterns:
// Option 1: pull out fields, dematerialize, then usevar cars = allCarsQueryResult.Items.Select((item) =>{ var model = item.Value["model"] as string; var mileage = item.Value["mileage"]; item.Dematerialize(); item.Dispose(); return new Car(model, mileage);}).ToList();
Do not store QueryResult.Items or individual DittoQueryResultItem objects outside the scope where they were created. Disposing promptly frees the underlying native memory.
Problem: Not properly cleaning up observersSolution: Stop iteration to cleanup:
// Iterator automatically cleans up when brokenawait foreach (var result in ditto.Store.RegisterObserver(query, args)){ if (shouldStop) { break; // Automatically cleans up }}