Skip to main content

Introduction

With strict mode enabled (the default for all Ditto 4.x SDKs), all fields are treated as a register by default. When enabled, every field in a document must match the collection definition exactly, including its CRDT type (e.g., map, register, counter). Disabling strict mode enables new functionality: when set to false, collection definitions are no longer required when using multiple CRDT types.
SELECT queries will return and display all fields when strict mode is disabled. This matches the behavior of the legacy query language, objects in INSERT and UPDATE statements are treated as maps. When a field has multiple possible CRDT types, the most recently updated type is chosen.
When strict mode is disabled, Ditto will infer the CRDT type based on the document’s shape:
  • Objects → treated as CRDT maps
  • Scalars and arrays → treated as Registers
  • Counters and attachments → inferred from operations
Important for Cross-Peer SynchronizationWhen peers have different DQL_STRICT_MODE settings:
  • Data WILL sync between peers, but behavior changes
  • When strict mode is true (SDK 4.10 and earlier), nested objects default to REGISTER type
  • For consistent behavior, either use matching strict mode settings across all peers OR explicitly define MAP types in your collection definitions in 4.10 and below.
See Cross-Peer Synchronization for detailed examples.

Strict Mode Behavior

Strict mode is enabled (set to true) by default in SDK 4.x, but will be set to false by default in SDK 5.0 and later. If you are using SDK 4.11+, ensure you have configured strict mode by setting DQL_STRICT_MODE=false, before starting your sync.
FeatureSDK <4.10: DQL_STRICT_MODE=trueSDK 4.11+: DQL_STRICT_MODE=false
Nested MAPsRequires explicit collection definitionsAutomatically inferred
Collection DefinitionsRequired to use non-register CRDT typesOnly required to use register objects
Legacy CompatibilityDifficult, due to collection definitionsSupported out of the box
Default object typeREGISTER (whole object replacement)MAP (field-level merging)
Nested field updatesReplaces entire objectMerges individual fields

HTTP Usage

To use the HTTP API with strict mode disabled, use the /api/v5/store/execute endpoint.

Endpoint Compatibility

EndpointSupports DQL_STRICT_MODE=falseUse When
/api/v4/store/execute❌ NoAll SDK peers have DQL_STRICT_MODE=true
/api/v5/store/execute✅ YesAny peers have DQL_STRICT_MODE=false
The v5 and v4 HTTP APIs are compatible, so you can use the v5 endpoint and changes will sync to v4 clients without issues. Example HTTP Request:
# Use v5 endpoint when DQL_STRICT_MODE=false sets `metadata` to MAP type
curl -X POST 'https://my_server.dittolive.app/<my_app_id>/api/v5/store/execute' \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "UPDATE orders SET metadata.updatedAt = :date WHERE _id = :id",
    "args": {"date": "2025-05-28", "id": "order-1"}
  }'
Read more about the HTTP API.

SDK Usage

First, disable strict mode before calling startSync or creating your DQL subscriptions, observers, or execute statements.
await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
ditto.startSync()

Subscriptions

Subscriptions behave the same regardless of whether DQL_STRICT_MODE is enabled or disabled.
ditto.sync.registerSubscription("SELECT * from orders WHERE _id.restaurantId = :restaurantId", args: [restaurantId: "01234"])

Examples

With DQL_STRICT_MODE=false, objects are treated as maps. In the following examples, items is an object so it is treated as a map, with further nested objects, which are also treated as maps. This nested structure is common in document databases.
let doc = {
  "_id": "my-id",
  "regionId": "01234",
  "items": {
     "shake": {...},
     "fries": {...},
  }
}
We can insert the document into the database using DQL.
INSERT INTO orders DOCUMENTS (:doc)
ON ID CONFLICT DO UPDATE
With DQL_STRICT_MODE=false, Ditto infers the CRDT type based on the document’s shape.
No collection definition is required.
// When strict mode is set to `false`, Ditto infers the CRDT type based on the
// document's shape.
try await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
ditto.startSync()

// Therefore we don't need to specify the collection definition in our query.
ditto.store.execute("SELECT * FROM orders WHERE _id = 'my-id'")
{
  "_id": "my-id",
  "regionId": "01234",
  "items": {
     "shake": {...},
     "fries": {...},
  }
}
  • When you define an object like items: {"shake": ..., "fries": ...} , Ditto treats that as a MAP when strict mode is disabled.
  • Whether you use Query Builder or DQL, this structure is preserved and behaves the same on both insert and read.
  • This is useful to know if you’re working with dynamic key-value structures inside documents.

Updating Nested Maps

In 4.11 and above, use UPDATE statement to set nested MAPs. Using UPDATE
UPDATE orders 
SET 
  metadata.updatedAt = '2025-05-28',
  metadata.updatedBy = '67c0faa40054d13a000c614a'
WHERE _id = 'my-id'

Updating Nested Maps with Dynamic Keys

Use INSERT and ON ID CONFLICT DO UPDATE to update nested maps with dynamic UUIDs. This is the same behavior as using upsert in the legacy query builder.
INSERT INTO orders DOCUMENTS (
  _id: 'my-id', 
  items: {
    '7c6a163e-3233-46db-a8ba-f604c4b8f':  { 'basePrice': 200 }, 
    '7c6a163e-3233-46db-a8ba-f604c4b8fdde':  { 'basePrice': 100 } 
  }
) ON ID CONFLICT DO UPDATE

Deleting Nested Values

In 4.11 and above with DQL_STRICT_MODE=false, use the UNSET statement.
UPDATE orders 
UNSET items.abc 
WHERE _id = 'my-id'

Register Objects

A REGISTER is a data type in Ditto that stores a single scalar value and uses last-write-wins merge strategy to atomically handle conflicts, rather than maps which use add-wins to create a merged object. With DQL_STRICT_MODE=false, if you want a register object (JSON-like object) data type in DQL, it must be specified explicitly. Key characteristics of registers:
  • Stores primitive types (string, boolean) or JSON-like objects
  • Last-write-wins conflict resolution ensures consistent values across peers
UPDATE COLLECTION orders (updatedAt REGISTER)  
SET updatedAt = {
	"datetime": "2025-02-28", 
	"authorId": "67c0faa40054d13a000c614a"
} 
WHERE _id = 'my-id'

SELECT * FROM COLLECTION orders (updatedAt REGISTER) WHERE _id = 'my-id'
{
  "_id": "my-id"
  "regionId": "01234",
  "items": {
    "shake": {...},
    "fries": {...},
    "burger": {...}
  },
  "updatedAt": {
	  "datetime": "2025-02-28",
	  "updatedBy": "67c0faa40054d13a000c614a" 
  }
}
For more information on using types and definitions, see DQL > Types and Definitions.

Last Write Wins

With DQL_STRICT_MODE=false, if there is no collection definition provided, writing objects will update a map, even if a register already exists for that field in Ditto. This is called “last write wins” behavior, and it means that the last operation to write to a field will overwrite any previous values, regardless of the type of the field.
-- Set a register
UPDATE orders 
SET items = 'a_string_value' 
WHERE _id = 'my-id' 

-- Implicitly infers (items MAP) even though there is a register on disk
-- as items is an object 
UPDATE menus 
SET items.burger = {...} 
WHERE _id = 'my-id' 

-- When querying, Ditto will return the map
-- as it was updated most recently
SELECT * FROM menus WHERE _id = 'my-id'
{
  "_id": "my-id"
  "regionId": "01234",
  "items": {
    "shake": {...},
    "fries": {...},
    "burger": {...}
  }
}
If you do not supply the collection definition, a register can be treated as a nested map which can lead to unexpected behavior. It is recommended that you always supply a collection definition if you want to force Ditto to use a register.

Cross-Peer Synchronization

Version Compatibility

Scenariov4.10 and earlierv4.11+
All peers same settingRequires collection definitionsWorks with or without definitions
Mixed settings, no definitionsNested updates may failObjects default to REGISTER on strict=true peers
Mixed settings, with explicit MAP definitionsWorks correctlyWorks correctly

Mixing Peers with Different Settings

You can mix peers with different strict mode settings, but understanding the behavior is crucial for proper data synchronization: When all peers use the same DQL_STRICT_MODE setting, behavior is predictable:
  • With false: Objects are treated as MAPs by default, nested updates work as expected
  • With true: Collection definitions are required, types must be explicitly defined

Mismatched Settings (Requires Careful Handling, Avoid Unless Necessary)

When peers have different DQL_STRICT_MODE settings:
  1. Data DOES sync between peers regardless of settings differences
  2. Default type behavior changes:
    • Peer with DQL_STRICT_MODE=true treats undefined objects as REGISTERs
    • Peer with DQL_STRICT_MODE=false treats objects as MAPs
  3. This affects nested field updates significantly
Example of the Issue:
-- Peer A (DQL_STRICT_MODE=false): Updates nested fields
ALTER SYSTEM SET DQL_STRICT_MODE=false

UPDATE cars
SET items.sub1 = 'foo', 
    items.sub2 = 'bar'
WHERE _id.location = '1234'

-- Peer B (DQL_STRICT_MODE=true): WITHOUT collection definition
SELECT * from cars WHERE _id.location='1234'
{
  "_id": {"location": "1234"}
}

-- Result: The entire cars.items object is missing
-- 'foo' and 'bar' values appear not to sync because the object
-- is treated as a REGISTER, not a MAP
Solution Options:
  1. Option 1: Match strict mode settings across all peers (simplest)
// All peers use the same setting
await ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
  1. Option 2: Explicitly define MAP types in collection definitions
-- When peers have different settings, explicitly define MAPs in 4.10 clients
-- Peer B (DQL_STRICT_MODE=true): WITH collection definition 
SELECT COLLECTION cars (items MAP)
WHERE _id.location = '1234'

{
  "_id": {"location": "1234"},
  "items": {
    "sub1": 'foo'
    "sub2": 'bar'
  }
}

Troubleshooting Nested Field Sync Issues

Common Symptom: “Nested fields are not syncing”

This is often caused by mismatched strict mode settings between peers without explicit MAP definitions. In this case, the fields have actually all been synchronized correctly, but when querying the data is not displayed as expected.

Diagnostic Steps

  1. Check strict mode on all peers:
-- Run on each peer (4.11+) to check current setting
SHOW DQL_STRICT_MODE
  1. Verify your update query:
-- Example that might "fail" with mismatched settings
UPDATE orders 
SET metadata.updatedAt = '2025-05-28',
    metadata.updatedBy = 'user123'
WHERE _id = 'order-1'
  1. Check how the data appears on each peer:
-- Peer with DQL_STRICT_MODE=false sees nested updates
SELECT * FROM orders WHERE _id = 'order-1'
-- Returns: metadata: {updatedAt: '2025-05-28', updatedBy: 'user123'}

-- Peer with DQL_STRICT_MODE=true might see entire object replacement
SELECT * FROM orders WHERE _id = 'order-1'  
-- Returns: metadata as a single REGISTER value

Real-World Example Fix

Problem: Updating nested fields:
-- v4.11+ or v5 client, DQL_STRICT_MODE=false
UPDATE orders
SET metadata.updatedAt = '2025-05-28',
	metadata.updatedBy = 'user123'
	table = '12'
WHERE _id ='order-1'


-- v4.10 client, DQL_STRICT_MODE=true
SELECT * FROM orders WHERE _id = 'order-1'
-- Returns: table: '12'
-- Result: 'table' appears, but 'metadata.updatedAt' and 'metadata.updatedBy' do not
Root Cause: The client has DQL_STRICT_MODE=true while the updating peer has DQL_STRICT_MODE=false. Without a MAP definition, metadata is treated as a REGISTER. Solutions:
-- Solution 1: Add MAP definition on 4.10 client, DQL_STRICT_MODE=true
SELECT * FROM COLLECTION orders (metadata MAP) WHERE _id = 'order-1'

-- Solution 2: Ensure both peers have same strict mode
ALTER SYSTEM SET DQL_STRICT_MODE = false
SELECT * FROM orders WHERE _id = 'order-1'