Skip to main content
A data type is different than a standard scalar type by declaring merge behaviors, operations, and the spectrum of scalar types accessible for individual fields:

Data Types

In DQL, you’ll use three data types:REGISTER, MAP, and ATTACHMENT type. By default, fields in a DQL statement are assigned the REGISTER type unless otherwise specified by way of type definition. Following are the key characteristics:
TypeCRDT TypePayload
REGISTERLast-write-winsAny
MAPAdd-winsObject
ATTACHMENTLast-write-winsBinary file
COUNTERPositive-negative & LWW on SetInteger
PN_COUNTER (legacy)Positive-negativeInteger

Data Type Operations

Data types have different operations available.

REGISTER Operations

A Register supports scalar types, including primitive types, such as string and boolean, as well as a JSON blob, encapsulating multiple field‑value pairs that function as a single object. The REGISTER can only be set to a specific field. For example:
DQL
field1 = 1

MAP Operations

The MAP type supports inserting and tombstoning of fields using the functional operators. Inserting a field is an implicit operation performed by assigning a value to a field or a child of the field.
field1.sub1.s_sub1 = 1

ATTACHMENT Operations

To set the last-write-wins ATTACHMENT data type, provide an ATTACHMENT object:
DQL
field1 = :attachment
Read more about attachments and large binary files.

Counter (Settable Counter)

Counters are available on 4.13 and later.
A COUNTER is an enhanced version of the PN_COUNTER that combines positive-negative counter semantics with the ability to explicitly set the counter to a specific value. Like PN_COUNTER, it’s a CRDT type that can be incremented or decremented by any peer, but it also supports a RESTART operation that uses last-write-wins semantics. The counter is an integer value that automatically resolves conflicting increments and decrements from different peers by tracking operations and composing them to provide a final value. When peers perform RESTART operations concurrently, the last write wins. Counters are useful for tracking counts that multiple peers might update simultaneously, such as:
  • Like/vote counts with the ability to reset
  • Inventory counts that need periodic recalibration
  • Session counts that can be initialized to specific values
  • Metrics that require both incremental updates and explicit resets
Counter operations (INCREMENT BY, RESTART WITH, RESTART) are specified using the APPLY clause, not the SET clause. The APPLY clause is specifically designed for CRDT operations on special field types like counters.

Strict Mode and Type Declarations

The requirement to declare counter types in queries depends on your DQL_STRICT_MODE setting: When DQL_STRICT_MODE=true (default):
  • You must declare counter types in COLLECTION definitions for INSERT, UPDATE, and SELECT statements
  • Counter fields are only visible in queries when the type declaration is included
When DQL_STRICT_MODE=false:
  • Counter type declarations are not required for SELECT and UPDATE statements using APPLY
  • Counter type declarations are still required for INSERT statements
  • Counter fields are automatically visible in queries without type declarations
-- Must specify COUNTER type in all statements
INSERT INTO COLLECTION products (stock_count COUNTER)
VALUES ({ '_id': '123', 'stock_count': 100 })

UPDATE COLLECTION products (stock_count COUNTER)
APPLY stock_count INCREMENT BY 5
WHERE _id = '123'

SELECT * FROM COLLECTION products (stock_count COUNTER)
WHERE _id = '123'

Creating Counters

There are two ways to create a counter field:
  1. Using INSERT with type declaration: You can declare a field as a COUNTER in the COLLECTION definition when inserting a document. This allows you to initialize the counter with a specific value (type declaration is required for INSERT regardless of strict mode):
DQL
INSERT INTO COLLECTION products (stock_count COUNTER)
VALUES ({ '_id': '123', 'name': 'Widget', 'stock_count': 100 })
  1. Using APPLY operations: Counter fields are automatically created when you first use INCREMENT or RESTART operations on them. This is useful when you want to create documents without counter fields initially:
Do not initialize counter fields by inserting an integer value without declaring the COUNTER type. This creates a register field, not a counter.

Incrementing Counters

// Create document without counter field
let product = [
  "_id": "123",
  "name": "Widget"
]

await ditto.store.execute(
  query: """
    INSERT INTO COLLECTION products
    INITIAL DOCUMENTS (:product)
    """,
  arguments: [ "product": product ])

// Then increment the counter (creates it if doesn't exist)
await ditto.store.execute(
  query: """
    UPDATE COLLECTION products (stock_count COUNTER)
    APPLY stock_count INCREMENT BY 5
    WHERE _id = '123'
    """)
To decrement a counter, use a negative value with INCREMENT:
DQL
UPDATE COLLECTION products (stock_count COUNTER)
APPLY stock_count INCREMENT BY -3
WHERE _id = '123'

Setting Counter Values with RESTART

The RESTART operation allows you to explicitly set a counter to a specific value or reset it to zero. This uses last-write-wins semantics, so if multiple peers restart a counter concurrently, the last write will win. Set counter to a specific value:
DQL
UPDATE COLLECTION products (stock_count COUNTER)
APPLY stock_count RESTART WITH 100
WHERE _id = '123'
Reset counter to zero:
DQL
UPDATE COLLECTION products (stock_count COUNTER)
APPLY stock_count RESTART
WHERE _id = '123'
// Set counter to specific value
await ditto.store.execute(
  query: """
    UPDATE COLLECTION products (stock_count COUNTER)
    APPLY stock_count RESTART WITH 100
    WHERE _id = '123'
    """)

// Reset counter to zero
await ditto.store.execute(
  query: """
    UPDATE COLLECTION products (stock_count COUNTER)
    APPLY stock_count RESTART
    WHERE _id = '123'
    """)

Combining Counter Operations with Other Updates

You can combine counter operations with other field updates in a single statement:
DQL
UPDATE COLLECTION products (stock_count COUNTER)
APPLY stock_count INCREMENT BY 10
SET lastUpdated = '2025-12-16', updatedBy = 'user123'
WHERE _id = '123'

Querying Counter Values

You can retrieve the current value of a counter using a SELECT statement:
DQL
SELECT * FROM COLLECTION products (stock_count COUNTER)
WHERE _id = '123'
The counter value will appear as a regular integer in the query results:
{
  "_id": "123",
  "name": "Widget",
  "stock_count": 105
}

Counter vs PN_COUNTER

The key differences between COUNTER and PN_COUNTER:
FeatureCOUNTERPN_COUNTER
Increment/Decrement
Set to specific value✓ (RESTART WITH)
Reset to zero✓ (RESTART)
Conflict resolutionPN + LWW on RESTARTPN only
Available since4.13+4.11+
Use COUNTER when you need the ability to explicitly set or reset counter values. Use PN_COUNTER only for backward compatibility with older Ditto versions.

PN Counter Operations (Legacy)

PN Counters are available in 4.11 and later. Read more Users should use Counter which also provide the ability to set the counter value.
A counter is a special type of field that can be incremented or decremented. A counter is a double-precision floating-point number. In 4.11 and above, ditto offers PN_COUNTER, or positive-negative counter, which is a CRDT type that can be incremented or decremented by any peer. Counters automatically resolve conflicting increments and decrements from different peers by tracking the operations and composing them to provide a final value. Counters are useful for tracking counts that multiple peers might update simultaneously, such as:
  • Like/vote counts
  • Number of views or interactions
Do not initialize counter fields by inserting a double value (e.g., 0.0). This creates a register field, not a counter. Counter fields are automatically created when you first use PN_INCREMENT on them.
To use a counter, apply the PN_INCREMENT operation directly on the field. If the field doesn’t exist, it will be created as a counter with the increment value. If you need to create a document first, insert it without the counter field or with other fields only:
// Create document without counter field
let product = [
  "_id": "123",
  "updatedBy": "abc123"
]

await ditto.store.execute(
  query: """
    INSERT INTO COLLECTION products
    INITIAL DOCUMENTS (:product)
    """,
  arguments: [ "product": product ])

// Then increment the counter (creates it if doesn't exist)
await ditto.store.execute(
  query: """
    UPDATE products
    APPLY in_stock PN_INCREMENT BY 5.0
    WHERE _id = '123'
    """)
To update a counter, use the APPLY keyword followed by the field name and then the PN_INCREMENT keyword followed by the value. To decrement a counter, use a negative value.
UPDATE products
APPLY in_stock PN_INCREMENT BY 1.0
SET updatedBy = 'def456'
WHERE _id = '123'
You can then retrieve the latest value of a counter using a SELECT statement:
SELECT * FROM products
WHERE _id = '123'

Default Value Operation

Some data types can be set to a default value type using the default() functional operator.
  • REGISTERNULL
  • AWMAP → Empty Map {}
  • ATTACHMENTNOT SUPPORTED
  • PN_COUNTERNOT SUPPORTED
  • COUNTERNOT SUPPORTED
field1 = default()

Declaring Type Definition

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. ​

Registers

A REGISTER is a data type in Ditto that stores a single scalar value and uses last-write-wins merge strategy for handling conflicts. Key characteristics of REGISTER:
  • Stores primitive types (string, boolean) or JSON objects
  • Last-write-wins conflict resolution ensures consistent values across peers
With DQL_STRICT_MODE=false, if you want to force a JSON Object to use a REGISTER data type instead of a MAP in DQL, it must be specified explicitly.
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'
The results of the SELECT statement above would be:
{
  "_id": "my-id"
  "regionId": "01234",
  "items": {
    "shake": {...},
    "fries": {...},
    "burger": {...}
  },
  "updatedAt": {
	  "datetime": "2025-02-28",
	  "updatedBy": "67c0faa40054d13a000c614a"
  }
}
If you need to remove a register map, you need to use the UNSET statement at the top level. Because a register map is treated the same as a scalar value (such as string, int), you operate on the entire object as a whole, similar to a JSON blob.
UPDATE COLLECTION orders (updatedAt REGISTER)
UNSET updatedAt
WHERE _id = 'my-id'
You will receive an error if you attempt to SET or UNSET a nested key of a register using dot notation.
UPDATE COLLECTION orders (updatedAt REGISTER)
SET updatedAt.datetime = "2025-04-28"
WHERE _id = 'my-id'
-- Unsupported DML operation on REGISTER field "updatedAt"

Non-Registers

In 4.11+ and DQL_STRICT_MODE=false, collection definitions for non-registers are no longer required.Read more
With DQL_STRICT_MODE=true, REGISTER is the default type in DQL. That means that you need to specify the type definition when overriding with type MAP, PN_COUNTER, or ATTACHMENT within your query. DQL type definitions describe the schema of the documents within a specific collection — defining the field types within the collection and specifying the assigned data types for each field. To explicitly declare the type definition as non-REGISTER type, add a prefix of COLLECTION and the suffix of (field1 data_type, field2 data_type, ...) to list the fields within the collection and their associated data types:
DQL
SELECT *
FROM COLLECTION your_collection_name (field1 MAP, field2 ATTACHMENT)
WHERE field1.rating > 100
In this syntax:
DQL
... COLLECTION your_collection_name (field1 data_type, field2 data_type, ...) ...
  • COLLECTION declares that the collection has a type definition
  • your_collection_name is the name of the collection from which you want to set a definition.
  • (field1 data_type, field2 data_type, ...) specifies the data type of each field such as REGISTER , MAP, or ATTACHMENT
SELECT with Definition
DQL
SELECT *
FROM COLLECTION your_collection_name (field1 MAP, field2 ATTACHMENT)
UPDATE with Definition
DQL
UPDATE COLLECTION your_collection_name (field1 MAP, field2 ATTACHMENT)
SET ...
INSERT with Definition
DQL
INSERT INTO COLLECTION your_collection_name (field1 MAP, field2 ATTACHMENT)
DOCUMENTS (...)
MAP Type Specifics The MAP (Add-Wins Map) contains fields with their own data type. Data types for these fields are defined using parentheses following the MAP keyword. For example, MAP(sub1 data_type, sub2 data_type, ...):
DQL
... COLLECTION your_collection_name (map_name MAP(sub1 ATTACHMENT, sub2 MAP))
Single MAP The syntax for a single MAP with all other fields type REGISTER:
DQL
... COLLECTION your_collection_name (field1 MAP)
Single ATTACHMENT The syntax for a single ATTACHMENT with all other fields type REGISTER:
DQL
... COLLECTION your_collection_name (field1 ATTACHMENT)
MAP and ATTACHMENT The syntax for a single MAP and a single ATTACHMENT with all other fields type REGISTER:
DQL
... COLLECTION your_collection_name (field1 MAP, field2 ATTACHMENT)
Deeply Embedded MAP

Disable Strict Mode

In 4.11+ and DQL_STRICT_MODE=false, collection definitions are no longer required.Read more
The syntax for a document hierarchy of depth two — a single MAP nested with another MAP — with all other fields type REGISTER:
DQL
... COLLECTION your_collection_name (field1 MAP(sub1 MAP))
The syntax for a document hierarchy of depth four with all other fields type REGISTER:
DQL
... COLLECTION your_collection_name (field1 MAP(sub1 MAP(s_sub1 MAP(s_s_sub1 MAP))))