Skip to content

Commit

Permalink
docs: "How MUD models data" guide (#2243)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Feb 7, 2024
1 parent ca90074 commit 2b8c0bc
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 44 deletions.
25 changes: 25 additions & 0 deletions docs/components/MUDTable.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.table {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
background-color: hsl(var(--nextra-primary-hue) 100% 66%/0.1);
margin-top: 1em;
text-align: center;
}

.table th,
.table td {
padding: 1em 1.5em;
border-bottom: 1px solid rgba(0, 0, 0, 0.2);
}

.table th {
color: white;
background: hsl(var(--nextra-primary-hue) 100% 45%/0.5);
}
.table thead th {
background: hsl(var(--nextra-primary-hue) 100% 45%/0.75);
}

.table tbody tr:hover th,
.table tbody tr:hover td {
backdrop-filter: brightness(1.5);
}
10 changes: 10 additions & 0 deletions docs/components/MUDTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { ReactNode } from "react";
import styles from "./MUDTable.module.css";

type Props = {
children: ReactNode;
};

export function MUDTable({ children }: Props) {
return <table className={styles.table}>{children}</table>;
}
5 changes: 5 additions & 0 deletions docs/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ export default withNextra({
destination: "/config",
permanent: false,
},
{
source: "/guides/extending-world",
destination: "/guides/extending-a-world",
permanent: false,
},
];
},
});
3 changes: 2 additions & 1 deletion docs/pages/guides/_meta.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
"replicating-onchain-state": "Replicating onchain state",
"hello-world": "Hello World",
"extending-world": "Extending World",
"extending-a-world": "Extending a World",
emojimon: "Emojimon",
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CollapseCode } from "../../components/CollapseCode";

# Extending World
# Extending a World

On this page you learn how to modify a `World` that is already deployed to the blockchain.
If you want to learn how to modify a `World` before it is deployed, [see the hello world page](./hello-world).
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/guides/hello-world.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Hello World

On this page you learn how to modify a `World` before it is deployed to the blockchain.
If you want to learn how to modify a `World` that is already deployed, [see the extending world page](./extending-world).
If you want to learn how to modify a `World` that is already deployed, see the [Extending a World](./extending-a-world) guide.

The examples on this page use [the vanilla template](../templates/typescript/vanilla).
[See this page for installation instructions](../templates/typescript/getting-started).
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/guides/replicating-onchain-state.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Replicating onchain state from MUD Store events
# Replicating onchain state

This guide walks through how you might recreate onchain state from MUD's Store events. We use this pattern in our sync stack to hydrate a client or indexer from blockchain logs fetched from an RPC.

## Store: MUD's onchain storage

Before we get started, it's helpful to understand [how MUD stores its data on chain](/store/introduction). MUD introduces [a few new concepts](/store/tables): tables, records, key tuples, and fields.
Before we get started, it's helpful to understand [how MUD stores its data on chain](/store/introduction). MUD introduces [a few new concepts](/store/data-model): tables, records, key tuples, and fields.

You can think of tables like a native Solidity `mapping`, where its keys and values are [encoded in a standardized way](/store/encoding) to 1) more easily replicate and represent data offchain, like in relational databases, and 2) require less onchain storage and gas than native Solidity.

Expand Down
6 changes: 3 additions & 3 deletions docs/pages/store/_meta.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export default {
introduction: "Introduction",
"data-model": "Data model",
tables: "Tables",
"table-libraries": "Table Libraries",
"table-libraries": "Table libraries",
encoding: "Encoding",
"store-hooks": "Store Hooks",
"store-hooks": "Store hooks",
reference: "Reference",
"table-intro": { display: "hidden" }, // imported
};
138 changes: 138 additions & 0 deletions docs/pages/store/data-model.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { CollapseCode } from "../../components/CollapseCode";
import { MUDTable } from "../../components/MUDTable";

# How MUD models data

The MUD framework helps you to model your onchain state in a way that enables building applications with familiar tooling, like relational databases.

In your MUD config, you define data as a set of tables. Like relational databases, these tables have “columns” by way of two MUD concepts: a key schema and a value schema. Each one has a set of fields that map to Solidity types.

## Defining tables

Let's say we're making a game where players can move around a map. The definition for a `Position` table might look something like:

<CollapseCode>

```tsx filename="mud.config.ts" {3-11} showLineNumbers
export default mudConfig({
tables: {
Position: {
keySchema: {
player: "address",
},
valueSchema: {
x: "int32",
y: "int32",
},
},
},
});
```

</CollapseCode>

Translating the table definition above would look like this in a relational database:

<MUDTable>
<thead>
<tr>
<th>player</th>
<th>x</th>
<th>y</th>
</tr>
</thead>
<tbody>
<tr>
<th>0x1234</th>
<td>-5</td>
<td>1</td>
</tr>
<tr>
<th>0x5678</th>
<td>3</td>
<td>6</td>
</tr>
</tbody>
</MUDTable>

Because `player` in part of the key schema, we would have a unique/primary key constraint on `player`.

Let's add another table for terrain at a given position on the map. MUD allows for multiple keys in the key schema and, like databases, we call these composite keys.

<CollapseCode>

```tsx filename="mud.config.ts" {12-20} showLineNumbers
export default mudConfig({
tables: {
Position: {
keySchema: {
player: "address",
},
valueSchema: {
x: "int32",
y: "int32",
},
},
Terrain: {
keySchema: {
x: "int32",
y: "int32",
},
valueSchema: {
terrainType: "string",
},
},
},
});
```

</CollapseCode>

Similarly, the relational database representation of that table would look like:

<MUDTable>
<thead>
<tr>
<th>x</th>
<th>y</th>
<th>terrainType</th>
</tr>
</thead>
<tbody>
<tr>
<th>-1</th>
<th>0</th>
<td>grass</td>
</tr>
<tr>
<th>0</th>
<th>1</th>
<td>tree</td>
</tr>
<tr>
<th>1</th>
<th>0</th>
<td>grass</td>
</tr>
</tbody>
</MUDTable>

Because we have a composite key of `x` and `y`, we would have a unique/primary key constraint on the tuple of `(x, y)`.

## Tables on chain

Solidity and the EVM have much more limited data structures, so how can we express a relational database onchain? MUD takes the concept of tables and does a few things with them:

- [encodes each table record as a key/value pair](./encoding) before storing onchain
- [emits an event for each mutation](/guides/replicating-onchain-state)
- [provides a typed Solidity libraries](./table-libraries) to abstract this away

## Field types

Key schema fields can use all of Solidity's static-length primitive types like `uint256`, `address`, `bool`.

Value schema fields can use all of Solidity's static-length primitive types, arrays of those static-length primitive types, as well as and `string` and `bytes`.

Enums and user types are also supported and automatically map down to their Solidity primitive types.

More complex types like structs, `string[]`, and `bytes[]` are not yet supported. We'll cover the reasons why in our encoding/decoding guide.
9 changes: 4 additions & 5 deletions docs/pages/store/introduction.mdx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import TableIntro from "./table-intro.mdx";

# Introduction

`Store` is an alternative to Solidity's storage engine.
It enforces a data model that can be mapped directly to a relational database, enables [automatic indexing](../services/indexer) by emitting events on each storage operation, and [packs data more tightly](./encoding) than Solidity's storage engine.
It enforces a data model that can be mapped directly to a relational database, enables [automatic indexing](../services/indexer) by emitting a common set of events for each mutation, and [packs data more tightly](./encoding) than native Solidity.
It also allows external contract storage to be read onchain without being limited by existing `view` functions and [without a new opcode](https://eips.ethereum.org/EIPS/eip-2330).

## Data model

<TableIntro />
Each piece of data in `Store` is stored as a _record_ in a _table_.
You can think of tables in two ways, either [as a relational database](./how-mud-models-data) or [as a key-value store](./tables).

## Reading and writing data

Expand Down Expand Up @@ -43,4 +42,4 @@ This allows advanced use cases like the [`World` protocol](../world/introduction
## Automatic indexing

`Store` automatically emits events on every write operation, including when a new table is registered in the `Store` at runtime.
These events allow [automatic indexers](../services/indexer) to replicate the onchain state of each table in each `Store` contract in a relational database for offchain use, without the need for custom integrations.
These events allow [automatic indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state) of each table in each `Store` contract in a relational database for offchain use, without the need for custom integrations.
8 changes: 0 additions & 8 deletions docs/pages/store/table-intro.mdx

This file was deleted.

16 changes: 11 additions & 5 deletions docs/pages/store/tables.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Callout } from "nextra/components";
import TableIntro from "./table-intro.mdx";

# Tables

<TableIntro />
Each piece of data in `Store` is stored as a _record_ in a _table_.
You can think of tables in two ways, either [as a relational database](./how-mud-models-data) or as a key-value store.

- Each table is identified by a unique `ResourceId tableId`.
- Each record in a table is identified by a unique `bytes32[] keyTuple`.
You can think of the key tuple as a composite key in a relational database, or as a nested mapping in a key-value store.
- Each table has a `ValueSchema` that defines the types of data stored in the table.
You can think of the value schema as the column types in a table in a relational database, or the type of structs stored in a key-value store.

Tables are registered in the `Store` contract at runtime.
When a table is registered, its ID, key and value types, as well as key and field names are stored in the internal `Tables` table.
This emits an event that can be used by [offchain indexers](../services/indexer) to start replicating the state of the new table.
This emits an event that can be used by [offchain indexers](../services/indexer) to start [replicating the state](/guides/replicating-onchain-state) of the new table.

The recommended way of reading from tables and writing to tables is via the [typed table libraries](./table-libraries).
However, it is also possible to use the low-level [`IStore` (external)](./reference/store) or [`StoreCore` (internal)](./reference/store-core) API directly.
Expand All @@ -18,7 +24,7 @@ There are two types of tables in `Store`: _Onchain tables_ and _offchain tables_
We often omit the prefix from onchain tables and just call them tables.

As the name suggests, **onchain tables** store their state onchain, in the `Store` contract.
In addition, an event is emitted on every write operation, to allow [offchain indexers](../services/indexer) to replicate the onchain state.
In addition, an event is emitted on every write operation, to allow [offchain indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state).

**Offchain tables** on the other hand don't store any state onchain, but only emit the events for [offchain indexers](../services/indexer).
This makes them suitable for use cases where data doesn't need to be retrieved onchain, but should still be synchronized from the `Store` contract to [offchain indexers](../services/indexer).
Expand Down Expand Up @@ -120,7 +126,7 @@ FieldLayout fieldLayout = FieldLayoutLib.encode({

### Manually register a table

Registering a table is a prerequisite for writing to the table, and allows [offchain indexers](../services/indexer) to replicate the onchain state.
Registering a table is a prerequisite for writing to the table, and allows [offchain indexers](../services/indexer) to [replicate the onchain state](/guides/replicating-onchain-state).

```solidity
import { IStore } from "@latticexyz/store/src/IStore.sol";
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/world/function-selectors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ This function takes two parameters:
This value encodes both the namespace and the name of the `System`.
- [The signature (name and parameter types)](https://docs.soliditylang.org/en/v0.8.22/abi-spec.html#function-selector) of the function within the `System`.

For example, in the [Extending World guide](/guides/extending-world#deploy-to-the-blockchain) we create a namespace called `messaging`, within it a `System` called `message`, and that `System` includes a function called `incrementMessage`.
For example, in the [Extending a World](/guides/extending-a-world#deploy-to-the-blockchain) guide we create a namespace called `messaging`, within it a `System` called `message`, and that `System` includes a function called `incrementMessage`.

This is the code we use to register the `System` and the function selector:

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/world/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ To prevent systems from being registered in the root namespace, the root namespa

`Systems` in a `World` are comparable to [`Facets` in the `Diamond` pattern](https://eips.ethereum.org/EIPS/eip-2535) in many ways.
The big difference is permissionless extensions.
Anybody can claim an unused [namespace](/world/namespaces-access-control), and put into it tables and `System` contracts to [extend the world](/guides/extending-world).
Anybody can claim an unused [namespace](/world/namespaces-access-control), and put into it tables and `System` contracts to [extend the world](/guides/extending-a-world).

![overall illustration](./world-intro.svg)

Expand Down
20 changes: 4 additions & 16 deletions docs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"target": "es2020",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
Expand All @@ -25,14 +21,6 @@
],
"strictNullChecks": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
"pages/_meta.js",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/_meta.js", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

0 comments on commit 2b8c0bc

Please sign in to comment.