1. Collections

Collections are similar to a table in a traditional database. You must create a collection before adding data to Polybase.

Collections (aka database tables) define the fields and rules for a collection of records. All records created by a collection are guaranteed to follow the rules of that collection. This is in contrast to other smart contract languages, where only a single record can be associated with a collection.

Each Polybase record within a collection has its own unique identifier id.

You can view our example app Polybase Social to see it working in action.

Create a collection

You can create a collection using the client library and calling the applySchema method on your polybase instance.

You can also create a collection via the Polybase Explorer. You will need to first login and create an account.

import { Polybase } from "@polybase/client"

const db = new Polybase({ defaultNamespace: "your-namespace" })

// If you want to edit the contract code in the future,
// you must sign the request when calling applySchema for the first time
db.signer((data) => {
  return {
    h: 'eth-personal-sign',
    sig: ethPersonalSign(wallet.privateKey()), data)
  }
})

const createResponse = await db.applySchema(`
  collection CollectionName {
    id: string;
    country?: string;
    publicKey?: string;

    constructor (id: string, country: string) {
      this.id = id;
      this.country = country;

      // Assign the public key of the user making the request to this record
      this.publicKey = ctx.publicKey;
    }
  }
`, 'your-namespace') // your-namespace is optional if you have defined a default namespace

If you want your contract code to be updatable, then you need to ensure that you sign the request when creating the contract (the first time you call .applySchema())

id field is unique and mandatory on all collections, and you must assign an id using this.id = ... within the constructor function.

Define a Collection

Collections are defined using Polylang (the language for Polybase), that is very similar to Javascript/Typescript. Collections allow you define the rules and indexes for your collection records. The following is a valid collection definition.

collection CollectionName {
  id: string;
  name?: string;

  constructor (id: string, name: string) {
    this.id = id;
    this.name = name;
  }

  @index(name);
}

The above would allow you to insert a record with a name property of type string.

Constructor

The collection constructor function is called whenever you add new records to the the database. The constructor function must assign this.id and any additional required fields defined in your collection. An error will be returned if a record already exists with the assigned id is provided, as id must be unique.

Fields

You can specify the fields that are allowed in your collection. These should be at the top most part of your collection. A collection should always have a id field (id is unique). By default, all fields are required.

collection CollectionName {
  id: string;
  age: number;

  ...
}

id Field

The id fields is required on every Polybase collection. It must be assigned in the constructor. Each record must have a unique id across the entire collection.

Field Types

The following types are supported:

  • string
  • number
  • boolean
  • string[], number[] and boolean[]
  • map<string | number, T>
Additional types such as dates will be added soon.

Optional Fields

All fields are required by default, if you want to make a field optional simply append ? after the field name and before the :. For example:

collection CollectionName {
  id: string;
  required: number;
  optional?: number;

  ...
}

Functions

Field values can only be modified using functions.

collection Account {
  id: string;
  name: string;
  balance: number;
  publicKey: string;

  constructor (name: string) {
    this.id = ctx.publicKey;
    this.name = name;
    this.publicKey = ctx.publicKey;
    this.balance = 0;
  }

  // Fn ignores all above rules, so anything needed must be reimplemented
  transfer (b: record, amount: number) {
    # ctx is in global scope of fn
    # error() is in global scope of fn
    if (this.publicKey == ctx.publicKey) {
       throw error('invalid user');
    }

    # can edit both records
    this.balance -= amount
    b.balance += amount

    // min has to be reimplemented/declared b/c field @ rules do not apply
    // inside fns
    if (a.balance < 0) {
      throw error('insufficient balance');
    }
  }
}

You can call your custom functions using .call(functionName, args):

const db = new Polybase({ defaultNamespace: "your-namespace" });
const col = db.collection("account");
await col.record("id1").call("transfer", [c.record("id2"), 10]);

Context

The global variable ctx is available inside any Polybase function.

Public Key

ctx.publicKey is populated when you provide a signer function to the Polybase client.

import { Polybase } from '@polybase/client'
import { ethPersonalSign } from '@polybase/eth'

const db = new Polybase({
  signer: (data) => {
    return {
      h: 'eth-personal-sign',
      sig: ethPersonalSign(wallet.privateKey()), data)
    }
  })
})

You can then use the publicKey of the wallet used to sign the request in your collection code, for example see how in setName we check the ctx.publicKey and compare it against the records own publicKey:

collection CollectionName {
  id: string;
  name?: string;
  publicKey?: string;

  constructor (id: string) {
    this.id = id;
    this.publicKey = ctx.publicKey;
  }

  setName (name: string) {
    if (this.publicKey != ctx.publicKey) {
      error("you do not have permission to do that");
    }
    this.name = name;
  }
}

Indexes

Indexes are a list of fields in addition to the record’s id field that should be indexed. You need to ensure that all fields that are included in a where or sort clause are included in the indexes. All collection fields are automatically indexed, which means you only need to specify multi-field indexes.

const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("cities");
const records = await collectionReference
  .where("name", "==", "abc")
  .where("country", "==", "UK")
  .get();

You would need the following schema:

collection cities {
  name: string;
  country: string;

  @index(name, country);

  ...
}

If you want to order your results, you also need to include that in the index:

const db = new Polybase({ defaultNamespace: "your-namespace" });
const collectionReference = db.collection("cities");
const records = await collectionReference
  .where("name", "==", "abc")
  .sort("population", "desc")
  .get();

You would need the following schema:

collection cities {
  name: string;
  country: string;
  population: number;

  @index(name, [population, desc]);
}

Reference a collection

To reference a collection, you can call .collection("collection-name") on your Polybase instance. The returned collection instance relates to a specific collection of data and allows you .write() and .read() data from Polybase.

Relative Path (with default namespace)

The following would return a collection with path: your-namespace/cities:

const db = new Polybase({ defaultNamespace: "your-namespace" });
const collection = db.collection("cities");

Relative Path (without default namespace)

The following would return a collection with path: your-namespace/cities:

const db = new Polybase();
const collection = db.collection("your-namespace/cities");

Absolute Path

To override the default namespace, you can use an absolute path by specifying a / at the start of the path.

The following would return a collection with path: alt-namespace/cities:

const db = new Polybase({ defaultNamespace: "your-namespace" });
const collection = db.collection("/alt-namespace/cities");