Rand Stats

Red

zef:FCO

Build Status SparrowCI

Red

Take a look at our Documentation: https://fco.github.io/Red/

Red - A WiP ORM for Raku

INSTALL

Install with (you need rakudo 2018.12-94-g495ac7c00 or newer):

zef install Red

SYNOPSIS

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 = 42
my 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 = 42
my 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 post
my $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:
2

DESCRIPTION

Red is a WiP ORM for Raku.

traits

features:

relationships

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> };

creating through a scalar relationship

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.

custom table name

model MyModel is table<custom_table_name> {}

not nullable columns by default

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
}

load object from database

MyModel.^load: 42;
MyModel.^load: id => 42;

save object on the database

$object.^save;

search for a list of object

Question.^all.grep: { .answer == 42 }; # returns a result seq

transactions and savepoints

red-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.

Explicit transaction guards

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.

phasers

Temporary table

model Bla is temp { ... }

Create table

Question.^create-table;
Question.^create-table: :if-not-exists;
Question.^create-table: :unless-exists;

IN

Question.^all.grep: *.answer ⊂ (3.14, 13, 42)

create

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>}
    ]
;

AUTHOR

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.