State Query
Dozer

Dozer

MUD initial data hydration, and therefore filtering, comes in two flavors: Dozer and generic. Note that this is for the initial hydration, currently limits on on-going synchronization are limited to the generic method.

DozerGeneric
FilteringCan filter on most SQL functionsCan only filter on tables and the first two key fields (limited by eth_getLogs (opens in a new tab) filters)
AvailabilityRedstone (opens in a new tab), Garnet (opens in a new tab), or elsewhere if you run your own instanceAny EVM chain
Security assumptionsThe Dozer instance returns accurate informationThe endpoint returns accurate information (same assumption as any other blockchain app)

If there is a dozer instance serving a blockchain, as there is for Redstone (opens in a new tab) and Garnet (opens in a new tab), you can use it to:

  • Run queries on the data of any World on that blockchain.
  • Speed up the initial hydration by reducing the amount of data that needs to be synchronized. This is important for the user experience, because until the initial hydration is done the client is typically unusable.

The query language is a subset of the SQL SELECT command (opens in a new tab).

Dozer URLs

Example World

On Garnet there is a World at address 0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e (opens in a new tab) that runs a slightly modified version of the React template (opens in a new tab). You can see the data schema for the World in the block explorer (opens in a new tab).

Curl queries

You can run dozer queries by communicating directly with the server's API, for example using curl (opens in a new tab).

Simple query

This query looks for some fields from a single table.

  1. Create a file, query.json, with this content.

    query.json
    [
      {
        "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
        "query": "SELECT id, description FROM app__Tasks"
      }
    ]
    ℹ️

    Dozer does not support SELECT * FROM <table>, you have to specify column names.

  2. Run this command. Install curl and jq first if necessary.

    curl https://dozer.mud.garnetchain.com/q --compressed \
         -H 'Accept-Encoding: gzip'  \
         -H 'Content-Type: application/json' \
         -d @query.json | jq

The output is a mapping with two fields, the block height for which the result is valid, and the result itself. The result is a list of query responses, here it contains just one item because we only submitted a single query. The query response is also a list. The first entry is the field names, and all the other entries are rows returned by SELECT.

{
  "block_height": 5699682,
  "result": [
    [
      [
        "id",
        "description"
      ],
      [
        "0x3100000000000000000000000000000000000000000000000000000000000000",
        "Walk the dog"
      ],
      [
        "0x3e0a112aadc5e02927fb4a91649bea565fd1baa1175aae4cb4957d6348f165cf",
        "Test"
      ],
    ]
  ]
}

Here we only care about the first result, so from now on we use this command line to tell jq to only show us that information.

curl https://dozer.mud.garnetchain.com/q --compressed  \
    -H 'Accept-Encoding: gzip' \
    -H 'Content-Type: application/json' \
    -d @query.json | jq '.result[0]'

Conditions

If we want to see only those tasks that haven't been completed we can use a WHERE clause.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT id, description FROM app__Tasks WHERE completedAt=0"
  }
]
Results
[
  ["id", "description"],
  ["0x3100000000000000000000000000000000000000000000000000000000000000", "Walk the dog"],
  ["0x3e0a112aadc5e02927fb4a91649bea565fd1baa1175aae4cb4957d6348f165cf", "Test"]
]

Limited results

If you only want to see a few results, you can use a LIMIT clause.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT id, description FROM app__Tasks LIMIT 2"
  }
]
Results
[
  ["id", "description"],
  ["0x3100000000000000000000000000000000000000000000000000000000000000", "Walk the dog"],
  ["0x3e0a112aadc5e02927fb4a91649bea565fd1baa1175aae4cb4957d6348f165cf", "Test"]
]

You can use OFFSET to get a paging effect. For example, if you use this query.json you get two results, and the last row of the first one is repeated as the first row of the second one.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT id, description FROM app__Tasks LIMIT 3"
  },
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT id, description FROM app__Tasks LIMIT 3 OFFSET 2"
  }
]
Results

Use this command to see the results of both queries.

curl https://dozer.mud.garnetchain.com/q --compressed  \
  -H 'Accept-Encoding: gzip' \
  -H 'Content-Type: application/json' -d @query.json \
  | jq '.result'

The result is:

[
  [
    ["id", "description"],
    ["0x3100000000000000000000000000000000000000000000000000000000000000", "Walk the dog"],
    ["0x3e0a112aadc5e02927fb4a91649bea565fd1baa1175aae4cb4957d6348f165cf", "Test"],
    ["0xb15fd0e41ab0bb6eb992e0a3d4f30fce6ee24a5fc9c30f725fdfc96d9d16ed95", "Do the dishes"]
  ],
  [
    ["id", "description"],
    ["0xb15fd0e41ab0bb6eb992e0a3d4f30fce6ee24a5fc9c30f725fdfc96d9d16ed95", "Do the dishes"],
    ["0xb81d5036d0b62e0f2536635cbd5d7cec1d1f0706c0c6c1a9fa74293d7b0888eb", "Take out the trash"]
  ]
]

Sorted results

If you want to control the order in which you get results, you can use an ORDER BY clause.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT description, createdAt FROM app__Tasks ORDER BY createdAt"
  }
]

Note that the sort field(s) need to be part of the selected columns.

Results
[
  ["description", "createdat"],
  ["Walk the dog", "1723495628"],
  ["Take out the trash", "1723495640"],
  ["Do the dishes", "1723495642"],
  ["Test", "1723495964"],
  ["Test from a different account", "1723576522"],
  ["Another test", "1723576522"],
  ["Yet another test", "1723646440"]
]

Multiple tables

You can join multiple tables, using the same syntax SQL uses.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT app__Creator.id, description, taskCreator FROM app__Tasks, app__Creator WHERE app__Creator.id=app__Tasks.id"
  }
]
Results
[
  ["id", "description", "taskcreator"],
  [
    "0x3e0a112aadc5e02927fb4a91649bea565fd1baa1175aae4cb4957d6348f165cf",
    "Test",
    "0x735b2f2c662ebedffa94027a7196f0559f7f18a4"
  ],
  [
    "0x727d7bfe00b6db638c69595059dc10e21c52a7912d090905a7c7dc8659efd3b8",
    "Test from a different account",
    "0x428b1853e5ec29d35c84a218ec5170efc7621b58"
  ],
  [
    "0xb15fd0e41ab0bb6eb992e0a3d4f30fce6ee24a5fc9c30f725fdfc96d9d16ed95",
    "Do the dishes",
    "0x8225d72f2c39f3729d7f3fc03c6aa8731eaeef48"
  ],
  [
    "0xb81d5036d0b62e0f2536635cbd5d7cec1d1f0706c0c6c1a9fa74293d7b0888eb",
    "Take out the trash",
    "0x8225d72f2c39f3729d7f3fc03c6aa8731eaeef48"
  ],
  [
    "0xd43394ecf79077f65cd83b534dd44d3b4e9e2aa553e95aafecd14b8529543cda",
    "Another test",
    "0x428b1853e5ec29d35c84a218ec5170efc7621b58"
  ]
]

Grouping results

You can use GROUP BY to identify different groups. For example, this query gets you the different task creators.

query.json
[
  {
    "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
    "query": "SELECT taskCreator FROM app__Creator GROUP BY taskCreator"
  }
]
Results
[
  ["taskcreator"],
  ["0x428b1853e5ec29d35c84a218ec5170efc7621b58"],
  ["0x735b2f2c662ebedffa94027a7196f0559f7f18a4"],
  ["0x8225d72f2c39f3729d7f3fc03c6aa8731eaeef48"]
]

Metadata

You can use the /tables path to get the list of either all tables, or all tables that match a string. As per the SQL standard, the wildcard is %.

  1. Create a file, tables.json.

    tables.json
    {
      "address": "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
      "query": {
        "name": "%"
      }
    }
  2. Run this command.

    curl https://dozer.mud.garnetchain.com/tables --compressed \
        -H 'Accept-Encoding: gzip'  \
        -H 'Content-Type: application/json' \
        -d @tables.json | jq
Results
[
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x746273746f72650000000000000000005461626c657300000000000000000000",
    "key_names": ["tableId"],
    "val_names": ["fieldLayout", "keySchema", "valueSchema", "abiEncodedKeyNames", "abiEncodedFieldNames"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x006003025f5f5fc4c40000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x746273746f72650000000000000000005265736f757263654964730000000000",
    "key_names": ["resourceId"],
    "val_names": ["exists"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0001010060000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x746273746f726500000000000000000053746f7265486f6f6b73000000000000",
    "key_names": ["tableId"],
    "val_names": ["hooks"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x00000001b6000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c640000000000000000004e616d6573706163654f776e65720000",
    "key_names": ["namespaceId"],
    "val_names": ["owner"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0014010061000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c6400000000000000000042616c616e6365730000000000000000",
    "key_names": ["namespaceId"],
    "val_names": ["balance"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x002001001f000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c64000000000000000000496e7374616c6c65644d6f64756c6573",
    "key_names": ["moduleAddress", "argumentsHash"],
    "val_names": ["isInstalled"],
    "key_schema": "0x00340200615f0000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0001010060000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c640000000000000000005573657244656c65676174696f6e436f",
    "key_names": ["delegator", "delegatee"],
    "val_names": ["delegationControlId"],
    "key_schema": "0x0028020061610000000000000000000000000000000000000000000000000000",
    "val_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c640000000000000000004e616d65737061636544656c65676174",
    "key_names": ["namespaceId"],
    "val_names": ["delegationControlId"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c640000000000000000005265736f757263654163636573730000",
    "key_names": ["resourceId", "caller"],
    "val_names": ["access"],
    "key_schema": "0x003402005f610000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0001010060000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c6400000000000000000053797374656d73000000000000000000",
    "key_names": ["systemId"],
    "val_names": ["system", "publicAccess"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0015020061600000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c6400000000000000000046756e6374696f6e53656c6563746f72",
    "key_names": ["worldFunctionSelector"],
    "val_names": ["systemId", "systemFunctionSelector"],
    "key_schema": "0x0004010043000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x002402005f430000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x6f74776f726c6400000000000000000046756e6374696f6e5369676e61747572",
    "key_names": ["functionSelector"],
    "val_names": ["functionSignature"],
    "key_schema": "0x0004010043000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x00000001c5000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c6400000000000000000053797374656d486f6f6b730000000000",
    "key_names": ["systemId"],
    "val_names": ["value"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x00000001b6000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c6400000000000000000053797374656d52656769737472790000",
    "key_names": ["system"],
    "val_names": ["systemId"],
    "key_schema": "0x0014010061000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462776f726c64000000000000000000496e69744d6f64756c65416464726573",
    "key_names": [],
    "val_names": ["value"],
    "key_schema": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0014010061000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x7462617070000000000000000000000043726561746f72000000000000000000",
    "key_names": ["id"],
    "val_names": ["taskCreator"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x0014010061000000000000000000000000000000000000000000000000000000",
    "query_name": null
  },
  {
    "address": "0x95f5d049b014114e2feeb5d8d994358ce4ffd06e",
    "table_id": "0x746261707000000000000000000000005461736b730000000000000000000000",
    "key_names": ["id"],
    "val_names": ["createdAt", "completedAt", "description"],
    "key_schema": "0x002001005f000000000000000000000000000000000000000000000000000000",
    "val_schema": "0x004002011f1fc500000000000000000000000000000000000000000000000000",
    "query_name": null
  }
]

To interpret the results, see the table documentation.

Typescript queries

You can query dozer from Typescript (opens in a new tab) without using MUD client synchronization.

  1. Create the project (in an empty directory) and install the software.

    pnpm create ts-node
    pnpm install
  2. Add the package that includes the dozer library.

    pnpm install @latticexyz/store-sync @latticexyz/store
  3. Replace src/main.ts with this file.

    main.ts
    import { fetchRecordsSql, selectFrom } from "@latticexyz/store-sync/dozer";
    import { defineStore } from "@latticexyz/store";
     
    const config = defineStore({
      namespace: "app",
      tables: {
        Tasks: {
          schema: {
            id: "bytes32",
            createdAt: "uint256",
            completedAt: "uint256",
            description: "string",
          },
          key: ["id"],
        },
        Creator: {
          schema: {
            id: "bytes32",
            taskCreator: "address",
          },
          key: ["id"],
        },
      },
    });
     
    const queryUncompleted = selectFrom({
      table: config.tables.app__Tasks,
      where: "completedAt = 0",
      limit: 2,
    });
     
    const queryResult = await fetchRecordsSql({
      dozerUrl: "https://dozer.mud.garnetchain.com/q",
      storeAddress: "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
      queries: [queryUncompleted],
    });
     
    console.log("\n\nTwo uncompleted tasks");
    console.log(`SQL: ${queryUncompleted.sql}\nResult:`);
    console.log(queryResult.result[0].records);
  4. Compile and execute the application.

    pnpm build && pnpm start
Explanation
import { fetchRecordsSql, selectFrom } from "@latticexyz/store-sync/dozer";
import { defineStore } from "@latticexyz/store";

Import the necessary definitions.

const config = defineStore({
  namespace: "app",
  tables: {
    ...
  },
})

Create the table configuration. The input to defineStore is the same as used in the the mud.config.ts file.

const queryUncompleted = selectFrom({
  table: config.tables.app__Tasks,
  where: "completedAt = 0",
  limit: 2,
});

Create a query using selectFrom (opens in a new tab). The queries supported by selectFrom are a subset of those dozer supports. The results come from a single table, and only WHERE and LIMIT clauses are supported.

const queryResult = await fetchRecordsSql({
  dozerUrl: "https://dozer.mud.garnetchain.com/q",
  storeAddress: "0x95F5d049B014114E2fEeB5d8d994358Ce4FFd06e",
  queries: [queryUncompleted],
});

Run the query.

console.log("\n\nTwo uncompleted tasks");
console.log(`SQL: ${queryUncompleted.sql}\nResult:`);

The SQL query that generated the resulting records.

console.log(queryResult.result[0].records);

The actual records.