Introduction

With strict mode enabled, 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. SELECT queries will return and display all fields by default. 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 types, the most recently updated type is chosen.

Compatibility

FeatureDQL_STRICT_MODE=trueDQL_STRICT_MODE=false
Nested MAPs❌ Difficult to use✅ Supported
Collection Definitions❌ Required✅ Optional
Legacy Compatibility❌ Not supported✅ Supported

Strict mode is enabled (set to true) in v4, but will be set to false in v5.

HTTP Usage

To use the HTTP API with strict mode, you can use /store/v5/execute endpoint. This endpoint allows you to execute DQL statements without needing to define collection types.

v5 and v4 API are compatible, so you can use /store/v5/execute and changes will sync to v4 clients without issues.

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"])

Example

With DQL_STRICT_MODE=false, objects are treated as maps. In the following examples, items is a map: each key points to some object. This structure is common in NoSQL/document-style databases.

let doc = {
  "_id": "my-id",
  "regionId": "01234",
  "items": {
     "shake": {...},
     "fries": {...},
  }
}

Insert the document into the database using DQL.

INSERT INTO orders VALUES (:doc)
ON ID CONFLICT DO MERGE

With DQL_STRICT_MODE=false,, Ditto infers the CRDT type based on the document’s shape. No collection definition is required.

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 MERGE to update nested maps with dynamic UUIDs. This is the same behavior as using upsert in the legacy query builder.

INSERT INTO orders VALUES (
  _id: 'my-id', 
  items: {
    '7c6a163e-3233-46db-a8ba-f604c4b8f':  { 'basePrice': 200 }, 
    '7c6a163e-3233-46db-a8ba-f604c4b8fdde':  { 'basePrice': 100 } 
  }
) ON ID CONFLICT DO MERGE

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 Maps

A REGISTER is a data type in Ditto that stores a single scalar value and uses last-write-wins merge strategy for handling conflicts.

With DQL_STRICT_MODE=false, if you want a REGISTER JSON object data type in DQL, it must be specified explicitly.

Key characteristics of REGISTER:

  • Stores primitive types (string, boolean) or JSON 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.

How it works

When DQL_STRICT_MODE=false, Ditto is more flexible and will infer the CRDT type based on the document’s shape, meaning that collection definitions are no longer required:

  • Objects → treated as CRDT maps
  • Scalars and arrays → treated as Registers
  • Counters and attachments → inferred from operations

When strict mode is set to true, Ditto infers the CRDT type based on the document’s shape. By default, objects treated as registers, which means that every field’s type must be specified in the collection definition. In this case, the items field is a map, so it must be defined as such in the collection definition.

Last Write Wins

With DQL_STRICT_MODE=false, if there is no collection definition provided, objects will be implicitly updating the MAP after a dot update operation, even if a register exists on disk.

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. 
UPDATE menus 
SET items.burger = {...} 
WHERE _id = 'my-id' 

// Last write wins -- MAP
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 data loss. This means it is recommended that you always supply a collection definition if you want to force Ditto to use a register.

❌ Do not do this

UPDATE orders 
UNSET updatedAt.datetime = "2025-04-28"
WHERE _id = 'my-id'

SELECT * FROM orders WHERE _id = 'my-id'
{
  "_id": "my-id"
  "regionId": "01234",
  "items": {
    "shake": {...},
    "fries": {...},
    "burger": {...}
  },
  "updatedAt": { ## implicit map because of Last Write Wins behavior
	"datetime": "2025-04-28" 
  }
}

✅ Do this instead:

UPDATE COLLECTION orders (updatedAt REGISTER)  
SET updatedAt = {
	"datetime": "2025-05-01", 
	"authorId": "13aa600aa40c61407c0f054d"
} 
WHERE _id = 'my-id'

SELECT * FROM orders (updatedAt REGISTER) WHERE _id = 'my-id'

{
  "_id": "my-id"
  "regionId": "01234",
  "items": {
    "shake": {...},
    "fries": {...},
    "burger": {...}
  },
  "updatedAt": { # register
	  "datetime": "2025-05-01",
	  "updatedBy": "13aa600aa40c61407c0f054d" 
  }
}

Mixing Peers

When strict mode is enabled, you can mix peers with different strict mode settings. This is because subscriptions are the same regardless of whether strict mode is enabled or disabled.