DEFINE TABLE
statementThe DEFINE TABLE
statement allows you to declare your table by name, enabling you to apply strict controls to a table’s schema by making it SCHEMAFULL
, create a foreign table view, and set permissions specifying what operations can be performed on the field.
DEFINE TABLE
statement.DEFINE TABLE
statement.
SurrealQL SyntaxDEFINE TABLE [ OVERWRITE | IF NOT EXISTS ] @name [ DROP ] [ SCHEMAFULL | SCHEMALESS ] [ TYPE [ ANY | NORMAL | RELATION [ IN | FROM ] @table [ OUT | TO ] @table [ ENFORCED ]]] [ AS SELECT @projections FROM @tables [ WHERE @condition ] [ GROUP [ BY ] @groups ] ] [ CHANGEFEED @duration [ INCLUDE ORIGINAL ] ] [ PERMISSIONS [ NONE | FULL | FOR select @expression | FOR create @expression | FOR update @expression | FOR delete @expression ] ] [ COMMENT @string ]
Below shows how you can create a table using the DEFINE TABLE
statement.
-- Declare the name of a table. DEFINE TABLE reading;
The following example uses the DROP
portion of the DEFINE TABLE
statement. Marking a table as DROP
disallows creating or updating records.
DROP
tables are useful in combination with events or foreign (view) tables, as you can compute a record and essentially drop the input.
-- By marking a table as DROP, you disallow any records to be created or updated. -- Records that currently exist in the table will not automatically be deleted, you can still remove them manually. DEFINE TABLE reading DROP;
The following expression shows how you can define a CHANGEFEED
for a table. After creating, updating, and deleting records in the table as usual, using SHOW CHANGES FOR
will show the changes that have taken place during this time.
-- Define the change feed and its duration -- Optionally, append INCLUDE ORIGINAL to include info -- on the current record before a change took place DEFINE TABLE reading CHANGEFEED 3d; -- Create some records in the reading table CREATE reading SET story = "Once upon a time"; CREATE reading SET story = "there was a database"; -- Replay changes to the reading table since a certain date -- Must be after the timestamp at which the changefeed began SHOW CHANGES FOR TABLE reading SINCE u"2023-09-07T01:23:52Z" LIMIT 10; -- Alternatively, show the changes for the table since a version number SHOW CHANGES FOR TABLE reading SINCE 0 LIMIT 10;
Response without INCLUDE ORIGINAL[ { "changes": [ { "define_table": { "name": "reading" } } ], "versionstamp": 29 }, { "changes": [ { "update": { "id": "reading:h1gcbc7ykbpslellh2g2", "story": "Once upon a time" } } ], "versionstamp": 30 }, { "changes": [ { "update": { "id": "reading:l9qfcncklhnlklby1avf", "story": "there was a database" } } ], "versionstamp": 31 } ]
Response with INCLUDE ORIGINAL[ { "changes": [ { "define_table": { "name": "reading" } } ], "versionstamp": 29 }, { "changes": [ { "current": { "id": "reading:2j3rc2yw1jzspcuvfe9v", "story": "Once upon a time" }, "update": [ { "op": "replace", "path": "/", "value": null } ] } ], "versionstamp": 30 }, { "changes": [ { "current": { "id": "reading:iuiurhi0y2ka0by0skqi", "story": "there was a database" }, "update": [ { "op": "replace", "path": "/", "value": null } ] } ], "versionstamp": 31 } ]
The following example demonstrates the SCHEMAFULL
portion of the DEFINE TABLE
statement. When a table is defined as schemafull, the database strictly enforces any schema definitions that are specified using the DEFINE TABLE
statement. New fields can not be added to a SCHEMAFULL
table unless they are defined via the DEFINE FIELD
statement.
NoteSince
v2.0.0
, schemafull tables are implicitly typeNORMAL
tables by default.
-- Create schemafull user table. DEFINE TABLE user SCHEMAFULL; -- Define some fields. DEFINE FIELD firstName ON TABLE user TYPE string; DEFINE FIELD lastName ON TABLE user TYPE string; DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is::email($value); DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE; -- SEE IT IN ACTION -- 1: Add a user with all required fields and an undefined one, 'photoURI'. CREATE user CONTENT { firstName: 'Tobie', lastName: 'Hitchcock', email: 'Tobie.Hitchcock@surrealdb.com', photoURI: 'photo/yxCFi22Jw2.webp' }; -- 2: Statement will not fail but photoURI will be ignored as it is not a -- defined field. -- 3: Query the data SELECT * FROM user
The following example demonstrates the SCHEMALESS
portion of the DEFINE TABLE
statement. This allows you to explicitly state that the specified table has no schema.
-- Create schemaless user table. DEFINE TABLE user SCHEMALESS; -- Define some fields. DEFINE FIELD firstName ON TABLE user TYPE string; DEFINE FIELD lastName ON TABLE user TYPE string; DEFINE FIELD email ON TABLE user TYPE string ASSERT string::is::email($value); DEFINE INDEX userEmailIndex ON TABLE user COLUMNS email UNIQUE; -- SEE IT IN ACTION - Example 1 -- 1: Add a user with all required fields and an undefined one. CREATE user SET firstName = 'Tobie', lastName = 'Hitchcock', email = 'Tobie.Hitchcock@surrealdb.com', photoURI = 'photo/yxCFi22Jw2.webp'; -- 2: Statement will succeed because user is a SCHEMALESS table. -- SEE IT IN ACTION - Example 2 -- 1: Add a user with an invalid email address and include a new field that was never defined. CREATE user SET firstName = 'Jamie', lastName = 'Hitchcock', email = 'Jamie.Hitchcock', photoURI = 'photo/yxCFi22Jw2.webp'; -- 2: Statement will fail because the value for email was not valid.
While a DEFINE TABLE
statement represents a template for any subsequent records to be created, a DEFINE FIELD
statement pertains to concrete field data of a record. As such, access to both the field itself and its parent record are granted inside DEFINE FIELD
through the $value
and $this
parameters, respectively.
DEFINE TABLE person SCHEMAFULL; DEFINE FIELD first_name ON TABLE person TYPE string ASSERT string::len($value) < 20; DEFINE FIELD last_name ON TABLE person TYPE string ASSERT string::len($value) < 20; DEFINE FIELD name ON TABLE person VALUE $this.first_name + ' ' + $this.last_name; // Creates a `person` with the name "Bob Bobson" CREATE person SET first_name = "Bob", last_name = "Bobson";
In SurrealDB, like in other databases, you can create views. The way you create views is using the DEFINE TABLE
statement like you would for any other table, then adding the AS
clause at the end with your SELECT
query.
-- Define a table as a view which aggregates data from the review table DEFINE TABLE avg_product_review TYPE NORMAL AS SELECT count() AS number_of_reviews, math::mean(<float> rating) AS avg_review, ->product.id AS product_id, ->product.name AS product_name FROM review GROUP BY product_id, product_name; -- Query the projection SELECT * FROM avg_product_review;
There are a few important things which make our views far more powerful than a typical relational database view and a few limitations to keep in mind.
Starting with what makes them powerful. Our pre-computed table views are most similar to event-based, incrementally updating, materialised views. Let’s explain what that means.
review
table, it triggers a matching event on the avg_product_review
table view.SELECT
statement, but then materialise the result. Instead of normal views which behave like bookmarked SELECT
queries, that just look like tables to the user.SELECT
statement again.While this functionality can be replicated in many other databases, it is usually only done by expert users as it can be very complicated to set up and maintain. Therefore, the true power of our pre-computed table views is making this advanced functionality accessible to everyone.
As mentioned though, there are a few limitations to keep in mind.
SELECT
statement. Therefore indexing and query optimisation are still very important.FROM
clause. In our case, just the review
table, not the product
we are also using in the query. Meaning that if you delete a review
the avg_product_review
will reflect that in near real-time. However if you delete a product
, it will still show up in avg_product_review
.Also note that table views are not triggered when importing data.
By default, the permissions on a table will be set to NONE unless otherwise specified.
CREATE some_table; DEFINE TABLE some_other_table; INFO FOR DB;
Response{ analyzers: {}, functions: {}, models: {}, params: {}, scopes: {}, tables: { some_other_table: 'DEFINE TABLE some_other_table TYPE ANY SCHEMALESS PERMISSIONS NONE', some_table: 'DEFINE TABLE some_table TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: {}, users: {} }
The following shows how to set table level PERMISSIONS
using the DEFINE TABLE
statement. This allows you to set independent permissions for selecting, creating, updating, and deleting data.
-- Specify access permissions for the 'post' table DEFINE TABLE post SCHEMALESS PERMISSIONS FOR select -- Published posts can be selected WHERE published = true -- A user can select all their own posts OR user = $auth.id FOR create, update -- A user can create or update their own posts WHERE user = $auth.id FOR delete -- A user can delete their own posts WHERE user = $auth.id -- Or an admin can delete any posts OR $auth.admin = true ;
IF NOT EXISTS
clauseAvailable since: v1.3.0
The IF NOT EXISTS
clause can be used to define a table only if it does not already exist. You should use the IF NOT EXISTS
clause when defining a table in SurrealDB if you want to ensure that the table is only created if it does not already exist. If the table already exists, the DEFINE TABLE
statement will return an error.
It’s particularly useful when you want to safely attempt to define a table without manually checking its existence first.
On the other hand, you should not use the IF NOT EXISTS
clause when you want to ensure that the table definition is updated regardless of whether it already exists. In such cases, you might prefer using the OVERWRITE
clause, which allows you to define a table and overwrite an existing one if it already exists, ensuring that the latest version of the table definition is always in use
-- Create a TABLE if it does not already exist DEFINE TABLE IF NOT EXISTS reading
OVERWRITE
clauseAvailable since: v2.0.0
The OVERWRITE
clause can be used to define a table and overwrite an existing one if it already exists. You should use the OVERWRITE
clause when you want to modify an existing table definition. If the table already exists, the DEFINE TABLE
statement will overwrite the existing table definition with the new one.
-- Create an TABLE and overwrite if it already exists DEFINE TABLE OVERWRITE example;
TYPE
-clauseAvailable since: v1.4.0
When defining a table in SurrealDB, you can specify the type of data that can be stored in the table. This can be done using the TYPE
clause, followed by either ANY
, NORMAL
, or RELATION
.
With TYPE ANY
, you can specify a table to store any type of data, whether it’s a normal record or a relational record.
With TYPE NORMAL
, you can specify a table to only store “normal” records, and not relations. When a table is defined as TYPE NORMAL
, it will not be able to store relations this can be useful when you want to restrict the type of data that can be stored in a table in schemafull mode.
Finally, with TYPE RELATION
, you can specify a table to only store relational type content. This can be useful when you want to restrict the type of data that can be stored in a table.
DEFINE TABLE person TYPE ANY; DEFINE TABLE person;
With TYPE NORMAL
, you can specify a table to only store “normal” records, and not relations.
-- Since it's default, we can also omit the TYPE clause DEFINE TABLE person TYPE NORMAL;
With TYPE RELATION
, you can specify a table to only store relational type content, and restrict what kind of relations can be stored.
-- Just a RELATION table, no constraints on the type of table DEFINE TABLE likes TYPE RELATION; -- Define a relation table, and constrain the type of relation which can be stored DEFINE TABLE likes TYPE RELATION FROM user TO post;
-- Define a relation table, and constrain the type of relation which can be stored DEFINE TABLE assigned_to SCHEMAFULL TYPE RELATION IN tag OUT sticky PERMISSIONS FOR create, select, update, delete WHERE in.owner == $auth.id AND out.author == $auth.id;
Available since: v2.0.0
As relations are represented by standalone tables, they can be constructed before any linked records exist.
RELATE city:one->road_to->city:two SET distance = 12.4, slope = 5.4;
Output[ { distance: 12.4f, id: road_to:pacwucj25a056hhs2s5h, in: city:one, out: city:two, slope: 5.4f } ]
As such, a query on the relation will return nothing until the records it has been defined upon are created.
SELECT ->road_to->city FROM city; CREATE city:one, city:two; SELECT ->road_to->city FROM city;
Output-------- Query -------- [] -------- Query -------- [ { "->road_to": { "->city": [ city:two ] } }, { "->road_to": { "->city": [] } } ]
If this behaviour is not desirable, the ENFORCED
clause can be used on a table of TYPE RELATION
to disallow a RELATE
statement from working unless it points to existing data.
DEFINE TABLE road_to TYPE RELATION IN city OUT city ENFORCED; RELATE city:one->road_to->city:three SET distance = 5.5, slope = 30.0;
Output"The record 'city:three' does not exist"