Quick Start

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.

⚠️
Tableland is still in open beta and will be launching the production network in 2023. But, smart contracts and apps deployed on testnet and mainnet chains can and should use the Tableland during the open beta period. Developers should still proceed with caution due to the nature of open beta changes and ensure contracts that use Tableland are future-proof.

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: healthbot
  • chainId: 80001
  • tableId: 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.

⚠️
The Tableland SDK uses the modern fetch API. When working in Node, it is necessary to use a version of Node (v18+) that supports fetch, or provide global access to node-fetch to use the SDK (e.g., for Node v16 or earlier). See Example

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

  1. Install the Tableland CLI using npm:
  2. npm install -g @tableland/cli

    or yarn:

    yarn add global @tableland/cli
  3. 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.
  4. tableland init
  5. Using the CLI, create a table, insert (write) a value, and read from the newly updated table. If you didn’t run the init command, you can choose to specify these as arguments.
  6. # 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

  1. Install evm-tableland.
  2. npm i @tableland/evm
  3. Import the Tableland deployments helper into your contract, and, optionally, SQL helpers.
  4. import "@tableland/evm/contracts/utils/TablelandDeployments.sol";
    import "@tableland/evm/contracts/utils/SQLHelpers.sol";
  5. Create and write to a table, created by the contract (see this walkthrough for more details).
  6. // 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.