Take a look at our Documentation: https://fco.github.io/Red/
Install with (you need rakudo 2018.12-94-g495ac7c00 or newer):
zef install Red
use Red:api<2>;
model Person {...}
model Post is rw {
has Int $.id is serial;
has Int $!author-id is referencing( *.id, :model(Person) );
has Str $.title is column{ :unique };
has Str $.body is column;
has Person $.author is relationship{ .author-id };
has Bool $.deleted is column = False;
has DateTime $.created is column .= now;
has Set $.tags is column{
:type<string>,
:deflate{ .keys.join: "," },
:inflate{ set(.split: ",") }
} = set();
method delete { $!deleted = True; self.^save }
}
model Person is rw {
has Int $.id is serial;
has Str $.name is column;
has Post @.posts is relationship{ .author-id };
method active-posts { @!posts.grep: not *.deleted }
}
my $*RED-DB = database "SQLite";
Person.^create-table;-- Equivalent to the following query:
CREATE TABLE person(
id integer NOT NULL primary key
AUTOINCREMENT,
name varchar(255) NOT NULL
)Post.^create-table;-- Equivalent to the following query:
CREATE TABLE post(
id integer NOT NULL primary key AUTOINCREMENT,
author_id integer NULL references person(id),
title varchar(255) NOT NULL,
body varchar(255) NOT NULL,
deleted integer NOT NULL,
created varchar(32) NOT NULL,
tags varchar(255) NOT NULL,
UNIQUE (title)
)my Post $post1 = Post.^load: :42id;-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
post.id = 42my Post $post1 = Post.^load: 42;-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
post.id = 42my Post $post1 = Post.^load: :title("my title");-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
post.title = ‘my title’my $person = Person.^create: :name<Fernando>;-- Equivalent to the following query:
INSERT INTO person(
name
)
VALUES(
$1
) RETURNING *
-- BIND: ["Fernando"]RETURNS:
Person.new(name => "Fernando")say $person.posts;-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
post.author_id = ?
-- BIND: [1]say Person.new(:2id)
.active-posts
.grep: { .created > now }-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
(
post.author_id = ?
AND (
post.deleted == 0
OR post.deleted IS NULL
)
)
AND post.created > 1554246698.448671
-- BIND: [2]my $now = now;
say Person.new(:3id)
.active-posts
.grep: { .created > $now }-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
(
post.author_id = ?
AND (
post.deleted == 0
OR post.deleted IS NULL
)
)
AND post.created > ?
-- BIND: [
-- 3,
-- Instant.from-posix(
-- <399441421363/257>,
-- Bool::False
-- )
-- ]Person.^create:
:name<Fernando>,
:posts[
{
:title("My new post"),
:body("A long post")
},
]
;-- Equivalent to the following query:
INSERT INTO person(
name
)
VALUES(
?
) RETURNING *
-- BIND: ["Fernando"]
INSERT INTO post(
created,
title,
author_id,
tags,
deleted,
body
)
VALUES(
?,
?,
?,
?,
?,
?
) RETURNING *
-- BIND: [
-- "2019-04-02T22:55:13.658596+01:00",
-- "My new post",
-- 1,
-- "",
-- Bool::False,
-- "A long post"
-- ]my $post = Post.^load: :title("My new post");-- Equivalent to the following query:
SELECT
post.id,
post.author_id as "author-id",
post.title,
post.body,
post.deleted,
post.created,
post.tags
FROM
post
WHERE
post.title = ‘My new post’
-- BIND: []RETURNS:
Post.new(
title => "My new post",
body => "A long post",
deleted => 0,
created => DateTime.new(
2019,
4,
2,
23,
7,
46.677388,
:timezone(3600)
),
tags => Set.new("")
)say $post.body;PRINTS:
A long postmy $author = $post.author;RETURNS:
Person.new(name => "Fernando")$author.name = "John Doe";
$author.^save;-- Equivalent to the following query:
UPDATE person SET
name = ‘John Doe’
WHERE id = 1$author.posts.create:
:title("Second post"),
:body("Another long post");-- Equivalent to the following query:
INSERT INTO post(
title,
body,
created,
tags,
deleted,
author_id
)
VALUES(
?,
?,
?,
?,
?,
?
) RETURNING *
-- BIND: [
-- "Second post",
-- "Another long post",
-- "2019-04-02T23:28:09.346442+01:00",
-- "",
-- Bool::False,
-- 1
-- ]$author.posts.elems;-- Equivalent to the following query:
SELECT
count(*) as "data_1"
FROM
post
WHERE
post.author_id = ?
-- BIND: [1]RETURNS:
2Red is a WiP ORM for Raku.
-
is column -
is column{} -
is id -
is id{} -
is serial -
is referencing{} -
is relationship{} -
is table<> -
is nullable
Red will infer relationship data if you use type constraints on your properties.
# Single file e.g. Schema.pm6
model Related { ... }
# belongs to
model MyModel {
has Int $!related-id is referencing( *.id, :model<Related> );
has Related $.related is relationship{ .id };
}
# has one/has many
model Related {
has Int $.id is serial;
has MyModel @.my-models is relationship{ .related-id };
}If you want to put your schema into multiple files, you can create an "indirect" relationship, and Red will look up the related models as necessary.
# MyModel.pm6
model MyModel {
has Int $!related-id is referencing{ :model<Related>, :column<id> };
has $.related is relationship({ .id }, :model<Related>);
}
# Related.pm6
model Related {
has Int $.id is serial;
has @.my-models is relationship({ .related-id }, :model<MyModel>);
}If Red can’t find where your model is defined you can override where it looks with require:
has Int $!related-id is referencing{ :model<Related>, :column<id>,
:require<MyApp::Schema::Related> };An empty scalar relationship returns an undefined, false type object carrying the relationship's creation context, rather than Nil. Call .^create on that object to create the related row and fill the foreign key automatically:
my $parent = $child.parent.^create(:name<parent>);
# For a relationship declared with :has-one (use Red <has-one>):
my $profile = $account.profile.^create(:description<profile>);For a belongs-to relationship, Red saves the foreign key on the existing source row. For :has-one, Red supplies the source key to the new related row. The source must already be saved. Both writes participate in one transaction; a failed operation rolls back its writes and restores the source's attribute and dirty state. Inside a transaction, the operation uses a savepoint.
Existing relationships continue to return their related instances. An empty relationship still tests false and undefined, but code that checks specifically for Nil should use .defined instead. Creation uses .^create, while plural relationships continue to use .create.
Independent schema declarations use ordinary attributes that do not become columns or appear in row data. Existing is indexed, is unique, is index{...} and is unique-index{...} forms remain supported.
model Measurement {
has UInt $.id is serial;
has Rat $.percent is column;
has Str $.name is column;
has Int $.active is column;
has $!percent-valid is check{ sql-and(.percent >= 0, .percent <= 1) };
has $!name-tree is index({ .name }, :using<btree>);
has $!name-search is index({ .name },
:using<gin>, :opclass<gin_trgm_ops>, :driver<Pg>);
has $!active-name is unique-index({ .name },
:where{ .active == 1 }, :driver<Pg>);
}:using selects an access method. PostgreSQL supports btree, hash, gin, gist, spgist and brin; the common API supports btree. :opclass selects a PostgreSQL operator class. For multiple keys, supply a list with one string per key, using an empty string for the default class. A class can be qualified with its schema. :where defines a partial index on PostgreSQL or SQLite; MySQL rejects an unguarded partial index.
:driver<Pg> explicitly selects a declaration only for that backend. The names are Pg, SQLite and MySQL, or a custom driver's final class-name component. Without this option an unsupported declaration raises X::Red::Index; Red does not substitute another index method or weaken a constraint. Conditional constraints are intentionally absent on the other backends.
CHECK is included by .^create-table and schema.create and is enforced by the database for both inserts and updates, including writes outside Red. Use sql-and, sql-or and sql-not for compound predicates. Raku's short-circuit && and || are rejected in schema predicates rather than changing SQL's three-valued logic. SQL's usual NULL semantics apply: a CHECK is not a substitute for NOT NULL.
.^create-indexes adds indexes to existing tables and checks existing names, key order, uniqueness, access method, operator classes and partial predicates. It rejects incompatible definitions instead of silently accepting them. It does not add or alter CHECK or EXCLUDE constraints on an existing table; those require an explicit schema migration.
sql-operator builds an AST for PostgreSQL range and regex operators such as @> , <@ , && and ~*. Values are bound in queries and quoted as literals in schema DDL. sql-range builds a native range constructor, defaulting to numrange with [) bounds. Its :type can also select int4range, int8range, tsrange, tstzrange or daterange.
model TrackSegment {
has UInt $.id is serial;
has Int $.track-id is column;
has Str $.percent-range is column{ :type<numrange> };
has $!range-valid is check(
{ sql-operator('@>', sql-range(0, 1, :bounds<[]>), .percent-range) },
:driver<Pg>
);
has $!no-overlap is exclude(
{ .track-id => '=', .percent-range => '&&' },
:using<gist>, :driver<Pg>
);
}
my @segments = TrackSegment.^all.grep({
sql-operator('&&', .percent-range, sql-range(0.2, 0.4))
});The exclusion constraint rejects overlapping ranges on the same track. Native range columns and operators require PostgreSQL. Bounds containment permits empty ranges; reject them with a separate constraint if the domain requires it. Applications must provision needed extensions explicitly: pg_trgm supplies gin_trgm_ops, and btree_gist supplies GiST classes for scalar keys such as the integer track identifier. Red does not install extensions automatically.
model MyModel is table<custom_table_name> {}Red, by default, has not nullable columns, to change it:
#| This makes this model’s columns nullable by default
model MyModel is nullable {
has Int $.col1 is column; #= this column is nullable
has Int $.col2 is column{ :!nullable }; #= this one is not nullable
}MyModel.^load: 42;
MyModel.^load: id => 42;$object.^save;Question.^all.grep: { .answer == 42 }; # returns a result seqred-do :transaction commits when its block succeeds and rolls back when it fails. Nested transactional blocks on the same connection use savepoints. They can see uncommitted outer changes, and their successful changes remain subject to an outer rollback. Catch an inner exception to roll back only that inner block and continue the outer transaction; an exception that propagates out rolls back the outer block too.
red-do :transaction, {
Person.^create: :name<Alice>;
try {
red-do :transaction, {
Person.^create: :name<Bob>;
# A failure here rolls back Bob, leaving Alice in the outer transaction.
}
}
};Drivers provide savepoint($name), rollback-to-savepoint($name) and release-savepoint($name) on the current connection. Rolling back to a savepoint leaves that savepoint active; releasing it does not commit the surrounding transaction. Savepoints do not coordinate concurrent tasks. Await asynchronous database work before leaving an automatic transactional block, and avoid overlapping access to its connection.
Declare a named :$guard parameter to take explicit control of a transaction. The block receives a Red::Transaction::Guard; a positional parameter, if declared, still receives the transaction's driver. A slurpy named parameter alone does not enable manual control. Blocks that declare neither :$guard nor the named placeholder $:guard retain automatic commit and rollback. Automatic scopes retain Raku's KEEP/UNDO behavior: a nonlocal return or fail unwinds the scope and rolls it back.
my $work = red-do :transaction, -> :$guard! {
$guard.start: -> :&commit {
Person.^create: :name<Alice>;
commit();
}
};
await $work;You can also use Raku's named placeholder $:guard instead of writing - :$guard!>. Using it inside a block implicitly declares the required named parameter, so red-do recognizes it as a manual transaction. Both direct commit and asynchronous work are supported:
red-do :transaction, {
Person.^create: :name<Alice>;
$:guard.commit;
};
my $work = red-do :transaction, {
$:guard.start: -> :&commit {
Person.^create: :name<Bob>;
commit();
}
};
await $work;Each guard starts with one participant, which red-do releases automatically when its block exits, including with :async. The last release rolls back an unfinished scope. Do not release that initial participant yourself.
.do(&block) reserves a participant, runs the block immediately and releases when it finishes, returning its result. .start(&block) reserves before scheduling and returns a Promise; completion releases the participant. Failed scheduling returns the reservation. Both restore the guard's transaction context and inject :&commit and :&rollback only when explicitly declared in the block's signature. An escaping exception immediately rolls back and propagates (breaking the Promise for .start). A Promise returned by .do does not extend its participation; use .start for asynchronous work. Returning a pending Promise from .do emits X::Red::Transaction::AsyncResult, a resumable warning that suggests .start without awaiting or replacing the result. Already completed Promises do not warn. To silence an intentional pending result, handle this type in CONTROL and call .resume.
For explicit lifetime control, retain() adds a participant and release() removes one. Both return the guard. Each additional participant must be released exactly once. To use a guard after its red-do block returns, retain it or schedule work with .start before exiting. For manually scheduled work, reserve before scheduling, release in its LEAVE, and return the reservation if scheduling fails.
commit() confirms immediately, without waiting for participants. Explicit rollback() and exceptions escaping the block also close an open scope immediately. Pending participants must still release after closure; these releases execute no SQL. Retaining a closed guard, releasing with no remaining participants, or committing a closed guard raises X::Red::Transaction. Explicit rollback remains idempotent. An exception after commit cannot undo it. Garbage collection provides no cleanup guarantee.
A nested guard controls a savepoint with its own participant count. Its last release rolls back only that savepoint. Its parent cannot run queries, open another child, or commit until that child closes. Parent rollback also invalidates all descendants. Drivers, statements, iterators and lazy queries belonging to a closed scope reject further database access. Ordinary driver use outside the scope is unaffected; direct access to a native database handle bypasses these guards.
Guards keep the reserved session usable by asynchronous work that inherits the block's context. Participant bookkeeping is synchronized, but does not permit simultaneous database operations: overlapping database access is rejected with X::Red::Transaction. No returned Promise is required to retain a manual transaction. With :async, an automatic transaction's Promise resolves after commit; a manual transaction's Promise represents only execution of its block, not guard completion.
-
before-create -
after-create -
before-update -
after-update -
before-delete -
after-delete
model Bla is temp { ... }Question.^create-table;
Question.^create-table: :if-not-exists;
Question.^create-table: :unless-exists;Question.^all.grep: *.answer ⊂ (3.14, 13, 42)Post.^create: :body("bla ble bli blo blu"), :title("qwer");
model Tree {
has UInt $!id is id;
has Str $.value is column;
has UInt $!parent-id is referencing{ Tree.id };
has Tree $.parent is relationship{ .parent-id };
has Tree @.kids is relationship{ .parent-id };
}
Tree.^create-table: :if-not-exists;
Tree.^create:
:value<Bla>,
:parent{:value<Ble>},
:kids[
{:value<Bli>},
{:value<Blo>},
{:value<Blu>}
]
;Fernando Correa de Oliveira fernandocorrea@gmail.com
Copyright 2018 Fernando Correa de Oliveira
This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.