- 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[]
andboolean[]
map<string | number, T>
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");