Skip to main content
Version: 0.3.36

DDL Statements

DDL statements create, alter and drop the schema objects that shape node data: node types, mixins, archetypes and element types. They are the SQL equivalent of a package's nodetypes/*.yaml, archetypes/*.yaml and elementtypes/*.yaml files.

A DDL statement returns one row with result and success:

CREATE NODETYPE 'docs:Article' EXTENDS 'raisin:Page' PROPERTIES (summary String);
{"columns":["result","success"],"rows":[{"result":"NodeType 'docs:Article' created","success":true}],"row_count":1,"execution_time_ms":0}

Object names are namespace:Name and are written as string literals. The quotes are optional when the name has no characters other than letters, digits, _ and : (CREATE NODETYPE docs:Article ... also parses). Keywords are case-insensitive. The parser reports where a statement went wrong (DDL parse error at position 30: Unexpected trailing content: ...), and an unknown property type is named in the error together with the accepted list.

Other statement families that create objects have their own pages: branches (Branch statements), roles, groups and users (CREATE / ALTER / DROP ROLE | GROUP | USER, ALTER SECURITY CONFIG), AI and embedding configuration (ALTER AI CONFIG, ALTER EMBEDDING CONFIG, Vector functions) and spatial indexes (ALTER SPATIAL INDEX, Geospatial functions). There is no CREATE WORKSPACE; workspaces come from packages or the workspace API (Schema tables).

CREATE NODETYPE

CREATE NODETYPE 'ns:Name'
[ EXTENDS 'ns:Parent' ]
[ MIXINS ('ns:MixinA', 'ns:MixinB') ]
[ DESCRIPTION 'text' ]
[ ICON 'icon-name' ]
[ PROPERTIES ( property_def [, ...] ) ]
[ ALLOWED_CHILDREN ('ns:TypeA', 'ns:TypeB') ]
[ REQUIRED_NODES ('ns:TypeA') ]
[ COMPOUND_INDEX 'name' ON (column [ASC|DESC] [, ...]) ] [ ... ]
[ VERSIONABLE ] [ IMMUTABLE ] [ PUBLISHABLE ] [ AUDITABLE ] [ INDEXABLE ] [ STRICT ]

Clauses can appear in any order. The whole clause list may be wrapped in parentheses, and a bare property list directly after the name is accepted as shorthand for PROPERTIES (...):

CREATE NODETYPE 'docs:Note' (title String REQUIRED, body String FULLTEXT);

A fuller example, with the registry row it produces:

CREATE NODETYPE 'docs:Article'
EXTENDS 'raisin:Page'
DESCRIPTION 'Blog article'
ICON 'article'
PROPERTIES (
summary String FULLTEXT LABEL 'Summary',
slug String UNIQUE,
status String DEFAULT 'draft',
views Number DEFAULT 0,
tags Array OF String,
seo Object { meta_title String, meta_description String TRANSLATABLE }
)
ALLOWED_CHILDREN ('raisin:Asset')
VERSIONABLE PUBLISHABLE;

SELECT name, extends, properties, allowed_children, versionable FROM NodeTypes WHERE name LIKE 'docs:%';
{"name":"docs:Article","extends":"raisin:Page",
"properties":[
{"name":"summary","type":"String","index":["Fulltext"],"meta":{"label":"Summary"}},
{"name":"slug","type":"String","unique":true},
{"name":"status","type":"String","default":"draft"},
{"name":"views","type":"Number","default":0.0},
{"name":"tags","type":"Array","items":{"type":"String"}},
{"name":"seo","type":"Object","structure":{"meta_title":{"name":"meta_title","type":"String"},"meta_description":{"name":"meta_description","type":"String","is_translatable":true}}}],
"allowed_children":["raisin:Asset"],"versionable":true}

EXTENDS inherits the parent's properties and constraints; raisin:Page requires a title, so docs:Article does too. ALLOWED_CHILDREN limits which node types may be created under a node of this type (an empty list means no constraint). REQUIRED_NODES names child types that must exist. The flags switch on version history, immutability, publishing, audit logging, full-text indexing and strict property validation for the type. See Behaviour flags for what each one does.

IMMUTABLE rejects any later write that changes a node's properties; structural changes (path, parent, node_type) and delete stay allowed:

CREATE NODETYPE 'audit:LedgerEntry'
PROPERTIES (amount Number REQUIRED, memo String)
IMMUTABLE;

Property types and modifiers

property_def := name type [ modifier ... ]
type := String | Number | Boolean | Date | URL | Reference | Resource
| Composite | Element | NodeType
| Array OF type
| Object { property_def [, ...] }
TypeStored as
StringJSON string
NumberJSON number (integer or float)
BooleanJSON boolean
DateISO 8601 timestamp string
URL{"raisin:url": "..."} with optional metadata
Reference{"raisin:ref": id, "raisin:workspace": ws, "raisin:path": path}; see Path functions
ResourceA binary asset reference
Composite, ElementBlock content built from element types
NodeTypeThe name of a node type
Array OF typeJSON array of the item type (Array OF String, Array OF Object { ... })
Object { ... }Nested object with its own typed fields; nests to any depth

Modifiers, in any order after the type:

ModifierEffect
REQUIREDWrites without the property are rejected (Missing required property 'title' for NodeType ...).
UNIQUEValue must be unique within the workspace.
DEFAULT valueApplied when the property is absent. value is a quoted string, a number, true / false or NULL.
FULLTEXTIndex the field for FULLTEXT_MATCH.
VECTOREmbed the field for vector search.
PROPERTY_INDEXMaintain a property index for equality lookups.
TRANSLATABLEThe field takes per-locale values.
LABEL 'text', DESCRIPTION 'text', ORDER nEditor metadata, stored under meta.
ALLOW_ADDITIONAL_PROPERTIESOn an Object, permit keys beyond the declared fields.

The DDL type names are the property schema, not SQL expression types: a Number property is read in SQL as (properties->>'views')::INT or ::DOUBLE. See Data Types.

Compound indexes

A compound index serves queries that filter on several properties and sort on a trailing one. Declare it in CREATE NODETYPE or add it later:

ALTER NODETYPE 'docs:Article' ADD COMPOUND_INDEX 'by_status_created' ON (status, __created_at DESC);
ALTER NODETYPE 'docs:Article' DROP COMPOUND_INDEX 'by_status_created';

Index columns are property names of the type plus the system columns __node_type, __created_at and __updated_at; each may carry ASC (default) or DESC. Adding an index to a type that already has nodes schedules a background job that builds it. The planner uses the index for equality on a leading prefix of the columns with an optional ORDER BY on the next one. In SQL the timestamp columns are spelled created_at / updated_at; the __created_at spelling belongs to the index declaration only. EXPLAIN shows which scan a query gets.

ALTER NODETYPE

ALTER NODETYPE 'ns:Name' alteration [ alteration ... ]
AlterationExample
ADD PROPERTY defADD PROPERTY subtitle String FULLTEXT
DROP PROPERTY nameDROP PROPERTY legacy_field
MODIFY PROPERTY defMODIFY PROPERTY status String DEFAULT 'review'
SET DESCRIPTION = 'text', SET ICON = 'name'SET DESCRIPTION = 'Updated'
SET EXTENDS = 'ns:Parent' / SET EXTENDS = NULL
SET ALLOWED_CHILDREN = ('a', 'b'), SET REQUIRED_NODES = (...)
ADD MIXIN 'ns:Mixin', DROP MIXIN 'ns:Mixin'
SET VERSIONABLE = true, and likewise IMMUTABLE, PUBLISHABLE, AUDITABLE, INDEXABLE, STRICT
ADD COMPOUND_INDEX ..., DROP COMPOUND_INDEX 'name'see above

Several alterations may follow each other in one statement:

ALTER NODETYPE 'docs:Article'
ADD PROPERTY subtitle String FULLTEXT
DROP PROPERTY legacy_field
SET DESCRIPTION = 'Updated description';
{"result":"NodeType 'docs:Article' altered","success":true}

The SET forms take =; SET DESCRIPTION 'Updated' without it is a parse error.

DROP NODETYPE

DROP NODETYPE 'ns:Name' [ CASCADE ]

IF EXISTS is not part of the grammar. Dropping a name that does not exist still answers NodeType 'x' dropped with success: true, so check NodeTypes if you need to know whether the type was there.

CREATE MIXIN

A mixin is a reusable property set composed into node types. See Using Mixins.

CREATE MIXIN 'ns:Name'
[ DESCRIPTION 'text' ] [ ICON 'icon-name' ]
PROPERTIES ( property_def [, ...] );

ALTER MIXIN 'ns:Name' ADD PROPERTY def | DROP PROPERTY name | MODIFY PROPERTY def
| SET DESCRIPTION = 'text' | SET ICON = 'name';

DROP MIXIN 'ns:Name' [ CASCADE ];
CREATE MIXIN 'docs:SEO' DESCRIPTION 'Search metadata' PROPERTIES (meta_title String, canonical_url URL);
ALTER NODETYPE 'docs:Article' ADD MIXIN 'docs:SEO';
SELECT name, mixins FROM NodeTypes WHERE name LIKE 'docs:%';
{"name":"docs:Article","mixins":["docs:SEO"]}
{"name":"docs:SEO","mixins":null}

Mixins are registered alongside node types, so they appear in NodeTypes. They are applied in order after the EXTENDS parent and before the type's own properties; a later mixin wins on a property conflict. CREATE NODETYPE ... MIXINS ('docs:SEO') attaches a mixin at creation time.

CREATE ARCHETYPE

An archetype is a page template bound to a base node type.

CREATE ARCHETYPE 'ns:Name'
[ BASE_NODE_TYPE 'ns:Type' ] [ EXTENDS 'ns:Parent' ]
[ TITLE 'text' ] [ DESCRIPTION 'text' ] [ ICON 'icon-name' ]
[ FIELDS ( property_def [, ...] ) ]
[ PUBLISHABLE ];

ALTER ARCHETYPE 'ns:Name' ADD FIELD def | DROP FIELD name | MODIFY FIELD def
| SET TITLE = 'text' | SET DESCRIPTION = 'text' | SET ICON = 'name'
| SET BASE_NODE_TYPE = 'ns:Type' | SET EXTENDS = 'ns:Parent' | SET PUBLISHABLE = true;

DROP ARCHETYPE 'ns:Name' [ CASCADE ];
CREATE ARCHETYPE 'docs:BlogPost' BASE_NODE_TYPE 'docs:Article' TITLE 'Blog Post' FIELDS (hero Element, body Composite);
SELECT name, base_node_type, title, fields FROM Archetypes;
{"name":"docs:BlogPost","base_node_type":"docs:Article","title":"Blog Post",
"fields":[{"$type":"SectionField","name":"hero"},
{"$type":"SectionField","name":"body","multiple":true}]}

FIELDS is stored, as are BASE_NODE_TYPE, TITLE, DESCRIPTION, ICON and PUBLISHABLE. ADD FIELD, DROP FIELD and MODIFY FIELD on ALTER ARCHETYPE all take effect: ADD FIELD on a name that already exists replaces it, and DROP or MODIFY on a name that does not exist is an error rather than a silent no-op.

The = in the SET clauses is optional, so SET DESCRIPTION 'text' and SET DESCRIPTION = 'text' both work.

CREATE ELEMENTTYPE

An element type is a content block used inside Composite and Element properties.

CREATE ELEMENTTYPE 'ns:Name'
[ DESCRIPTION 'text' ] [ ICON 'icon-name' ]
[ FIELDS ( property_def [, ...] ) ];

ALTER ELEMENTTYPE 'ns:Name' ADD FIELD def | DROP FIELD name | MODIFY FIELD def
| SET DESCRIPTION = 'text' | SET ICON = 'name' | SET PUBLISHABLE = true;

DROP ELEMENTTYPE 'ns:Name' [ CASCADE ];
CREATE ELEMENTTYPE 'docs:Hero' DESCRIPTION 'Hero block' FIELDS (heading String REQUIRED TRANSLATABLE, image Resource);
SELECT name, description, fields FROM ElementTypes WHERE name LIKE 'docs:%';
{"name":"docs:Hero","description":"Hero block",
"fields":[{"$type":"TextField","name":"heading","required":true,"translatable":true},
{"$type":"MediaField","name":"image"}]}

FIELDS is stored, and ALTER ELEMENTTYPE's ADD FIELD / DROP FIELD / MODIFY FIELD behave as they do for archetypes.

How a DDL type becomes a field

Fields are a tagged set ($type), while the DDL type vocabulary is the property one. The mapping:

DDL typeField
String, URLTextField
NumberNumberField
BooleanBooleanField
DateDateField
Reference, NodeTypeReferenceField
ResourceMediaField
ObjectJsonObjectField
ElementSectionField, accepting any element type
CompositeSectionField, multiple
Array<T>the field for T, multiple

REQUIRED, TRANSLATABLE, the index list, the default value, the label and the description all carry through. Two things the DDL grammar cannot yet say: which element types a SectionField accepts, and ENCRYPTED. Declare those in package YAML.

Reading the result

The schema tables show what a DDL statement stored:

SELECT name, extends, mixins, properties, allowed_children FROM NodeTypes WHERE name LIKE 'docs:%';
SELECT name, base_node_type, title FROM Archetypes;
SELECT name, description FROM ElementTypes;

Use LIKE rather than = on name; see Schema Tables.