About
Protocol
Walkthroughs
Integrations
Intro to NFT Metadata
Tutorials
Smart Contracts
Concepts
Playbooks
Learn
Quick Start
The bare minimum steps to take in order to use Tableland.
The bulk of what developers will need is described in detail throughout the resources in the Walkthroughs section. But, for those looking to install and skip straight to building, here are the steps you should take.
Tables
Tables are ERC721 tokens minted by the Tableland “registry” contract on each respective chain. Every table has a unique value on the chain (an ERC721 token ID), and every table is uniquely identifiable across any chain. Namely, a table’s full “name” is in the format:
{prefix}_{chainId}_{tableId}
The prefix
is a custom value set upon table creation. The chainId
is unique to the chain, and the tableId
aligns to the aforementioned ERC721 token ID. For example, a table name might look like healthbot_5_1
—a table with prefix “healthbot” that was the first table minted on Ethereum Goerli (chain ID of 5).
SDK
Using JavaScript
1. Install the SDK
From the command line, cd
to the project’s directory and install the SDK via npm
:
npm install --save @tableland/sdk@4.0.0-pre.4
Or yarn
:
yarn add @tableland/sdk@4.0.0-pre.4
2. Read from a table
Table reads do not require an on-chain connection. You can import the Database
object, specify the chain you’re reading from, and make a read query (SELECT
statement) using prepare
, which returns the values in the table. By using the readOnly
method, you bypass any on-chain signing mechanism since signing is not required for read queries.
import { Database } from "@tableland/sdk";
const tableName = "healthbot_80001_1"; // Our pre-defined health check table
const db = Database.readOnly("maticmum"); // Polygon Mumbai testnet
const { results } = await db.prepare(`SELECT * FROM ${tableName};`).all();
console.log(results);
The healthbot table exists on each network in which Tableland is deployed on with a varying chain ID. The table name format is in the format {prefix}_{chainId}_{tableId}
, meaning:
prefix
: healthbotchainId
: 80001tableId
: 1
3. Create a table
Instead of only reading data, you can create your own table and also write to it. Do this by connecting to an instance of the Database
object, and use the same prepare
method while passing a CREATE TABLE {prefix} ...
statement. You can then run
this statement to execute it, and the lack of readOnly
leads to a signature so that a table can be created.
import { Database } from "@tableland/sdk";
// Default to grabbing a wallet connection in a browser
const db = new Database()
// This is the table's `prefix`--a custom table value prefixed as part of the table's name
const prefix: string = "my_sdk_table";
const { meta: create } = await db
.prepare(`CREATE TABLE ${prefix} (id integer primary key, name text);`)
.run();
// The table's `name` is in the format `{prefix}_{chainId}_{tableId}`
console.log(create.txn.name); // e.g., my_sdk_table_80001_311
All tables are created on-chain (as ERC721 tokens). The main takeaway: every table creation comes with an on-chain transaction. Once that transaction has been finalized (time varies, per chain), you can access the table’s name
, which will have appended the chainId
and tableId
to whatever prefix
was specified in the create statement.
4. Write to a table
Now that you’ve created a table, you now own it. It is associated with the wallet / address that created it. With ownership, you have full access control and write privileges unless otherwise specified. You’ll notice that parameter binding is possible with the ?
symbol, allowing developers to follow the SQLite convention for prepared statements and pass replace values from prepare
with those in bind
.
// Insert a row into the table
const { meta: insert } = await db
.prepare(`INSERT INTO ${name} (id, name) VALUES (?, ?);`)
.bind(0, "Bobby Tables")
.run();
// Wait for transaction finality
await insert.wait;
// Perform a read query, requesting all rows from the table
const { results } = await db.prepare(`SELECT * FROM ${name};`).all();
Static statements are still possible (e.g., specifying 0
and "Bobby Tables"
within the INSERT
statement), but binding can make things a lot easier. There are also more complex controls that table owners can implement to grant other addresses mutation privileges.
Using TypeScript
The Database
API and all related classes and modules are written in Typescript, and provide a generic interface to fully typed queries and responses (if you want). Currently, if you do not provide types, it will default to unknown
. This is probably not what you want, so passing in any
is fine, but you can do a whole lot more if you provide a concrete type.
Types can be provided on the Database constructor, on the Statement constructor (prepare
), or callers can override them on any of the query/execution APIs directly (i.e., run
, all
, first
, or raw
).
1. Install the SDK
From the command line, cd
to the project’s directory and install the SDK via npm
:
npm install --save @tableland/sdk@4.0.0-pre.4
Or yarn
:
yarn add @tableland/sdk@4.0.0-pre.4
2. Read from a table
Start by defining the type associated with the table’s schema and use this type in the database constructor.
import { Database } from "@tableland/sdk";
// This table has schema: `counter INTEGER PRIMARY KEY`
const tableName: string = "healthbot_80001_1"; // Our pre-defined health check table
interface HealthBot {
counter: number;
}
// `readOnly` returns an instance of `Database`
const db: Database<HealthBot> = Database.readOnly("maticmum"); // Polygon Mumbai testnet
// Type is inferred due to `Database` instance definition
const { results } = await db.prepare(`SELECT * FROM ${tableName};`).all();
console.log(results);
Note the generic type system for Database
is relatively sophisticated, so it should correctly determine the response shape of raw
versus all
, etc. Building on the previous example:
3. Create a table
Similarly, define your types upfront, use them in the Database
constructor,
import { Database } from "@tableland/sdk";
interface Schema {
id: number;
name: string;
}
// Default to grabbing a wallet connection in a browser
const db = new Database<Schema>()
// This is the table's `prefix`; a custom table value prefixed as part of the table's name
const prefix: string = "my_sdk_table";
const { meta: create } = await db
.prepare(`CREATE TABLE ${prefix} (id integer primary key, name text);`)
.run();
// The table's `name` is in the format `{prefix}_{chainId}_{tableId}`
console.log(create.txn.name); // e.g., my_sdk_table_80001_311
4. Write to a table
Through the setup steps above, the Database
instance is aware of the type, so subsequent writes and reads will have the proper type inference handled correctly.
// Insert a row into the table
const { meta: insert } = await db
.prepare(`INSERT INTO ${name} (id, name) VALUES (?, ?);`)
.bind(0, "Bobby Tables")
.run();
// Wait for transaction finality
await insert.wait;
// Perform a read query, requesting all rows from the table
const { results } = await db.prepare(`SELECT * FROM ${name};`).all();
CLI
- Install the Tableland CLI using
npm
: - Create a configuration file, specifying your development private key, chain ID, and provider URL (e.g., Alchemy, Infura, or Etherscan)—this creates a configuration file like
.tablelandrc
. - Using the CLI,
create
a table, insert (write
) a value, andread
from the newly updated table. If you didn’t run theinit
command, you can choose to specify these as arguments.
npm install -g @tableland/cli
or yarn
:
yarn add global @tableland/cli
tableland init
# Create a table & save its returned `name` locally
tableland create "id int primary key, name text" "quickstart" --chain=polygon-mumbai --privateKey "your_private_key"
# Write to the table by INSERTing a value
# Note: every table `name` is unique, so replace `quickstart_5_74` with your table
tableland write "INSERT INTO quickstart_5_74 VALUES (0, 'Bobby Tables')" --chain=polygon-mumbai --privateKey "your_private_key"
# Read from the table
# Note: replace the table name with yours
# Although, anyone can *read* from *any* table, so this statement is valid for anyone
tableland read "SELECT * FROM quickstart_5_74;" --chain=polygon-mumbai
Smart Contracts
- Install
evm-tableland
. - Import the Tableland deployments helper into your contract, and, optionally, SQL helpers.
- Create and write to a table, created by the contract (see this walkthrough for more details).
npm i @tableland/evm
import "@tableland/evm/contracts/utils/TablelandDeployments.sol";
import "@tableland/evm/contracts/utils/SQLHelpers.sol";
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
import "@openzeppelin/contracts/utils/Strings.sol";
import "@tableland/evm/contracts/utils/TablelandDeployments.sol";
// Need if the contract must own the table
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
contract Starter is ERC721Holder {
// The table token ID, asigned upon TablelandTaables minting a table
uint256 private _tableId;
// Table prefix for the table (custom value)
string private constant _TABLE_PREFIX = "my_table";
function create() public payable {
/* Under the hood, SQL helpers formulates:
*
* CREATE TABLE {prefix}_{chainId} (
* id integer primary key,
* name text
* );
*/
_tableId = TablelandDeployments.get().createTable(
address(this),
SQLHelpers.toCreateFromSchema(
"id integer primary key," // Notice the trailing comma
"name text",
_TABLE_PREFIX
)
);
}
// Insert data into a table
function insert() public payable {
/* Under the hood, SQL helpers formulates:
*
* INSERT INTO {prefix}_{chainId}_{tableId} (id,name) VALUES(
* 1
* '0x1234...'
* );
*/
TablelandDeployments.get().runSQL(
address(this),
_tableId,
SQLHelpers.toInsert(
_TABLE_PREFIX,
_tableId,
"id,name",
string.concat(
Strings.toString(1), // Convert to a string
",",
SQLHelpers.quote(Strings.toHexString(msg.sender)) // Wrap strings in single quotes
)
)
);
};
// Update data in the table
function update(uint256 myId, string memory myName) public payable {
// Set values to update, like the "name" column to the function input param
string memory setters = string.concat(
"name=",
SQLHelpers.quote(myName) // Wrap strings in single quotes
);
// Only update the row with the matching `id`
string memory filters = string.concat(
"id=",
Strings.toString(myId)
);
/* Under the hood, SQL helpers formulates:
*
* UPDATE {prefix}_{chainId}_{tableId} SET name=<myName> WHERE id=<id>
*/
TablelandDeployments.get().runSQL(
address(this),
_tableId,
SQLHelpers.toUpdate(
_TABLE_PREFIX,
_tableId,
setters,
filters
)
);
}
}
If those setup steps didn’t provide enough context, head over to the more detailed documentation!
Mainnets vs. Testnets Gateway
Be sure to use https://tableland.network
on mainnets, and https://testnets.tableland.network
on testnets.
At the protocol level, the Tableland network is separated such that nodes process and respond to SQL queries relative to each environment. If you were to use the testnets
gateway on a mainnet chain / contract, this would lead to issues. The testnets
gateway only queries tables that exist on testnet chains, whereas the tableland.network
gateway only queries tables that exist on mainnet chains.
← Previous
Next →