Giter VIP home page Giter VIP logo

red's Introduction

Build Status 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

  • is column

  • is column{}

  • is id

  • is id{}

  • is serial

  • is referencing{}

  • is relationship{}

  • is table<>

  • is nullable

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

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

phasers

  • before-create

  • after-create

  • before-update

  • after-update

  • before-delete

  • after-delete

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 [email protected]

COPYRIGHT AND LICENSE

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.

red's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

red's Issues

A way to join

Today it’s “possible” to do things like:

Artist.where: Artist.id == CD.author-id AND CD.id == Track.cd-id;

But how to do left join, inner join, etc?

Make scalar relationship editable

model Post is rw {
   ...
   has Person $.author is relationship{ .author-id }
}

my $post = Post.^load: :42id;
$post.author = Person.^create: |%data;

This should create a new Person and change that Post's author-id to its id

Create a better relationship

model CD {
   has           $!artist-id is referencing{ Artist.id };
   has Artist $.author is relationship{ .artist-id };
}
model Artist {
   has UInt $.id is column{ :id };
   has CD   @.cds is relationship{ .artist-id };
}

The trait will receive a block and run it passing: if the type of the attribute is positional the attribute type, else the model’s type. It’s return should be a column that is referencing some other column. It will create a new result seq using that.

Create model subset

Something like:

Person.where({ .id > 30 }).subset: name;

And that would only select the name column and return a object with only the name attribute

How to install?

I'm a Perl 6 novice that would like to help where able. What are the installation instructions to get started?

Check table names before creating them

Some table names are not acceptable, like the ones with - or, for some reason, "commit". Document acceptable table names and check for them in the create-table meta-method.

Alternative way to create relationships

model Post {
   has UInt   $!author-id is referencing{ Person.id };
   has Person $.author = .^relates: $.author-id;
}

model Person {
   has UInt            $.id is id;
   has Post::ResultSeq $.posts = .^relates: .author-id;
}

flatmap?

How should I flat a ResultSeq?

Transaction?

Red.transaction: {
   ... # code that will run inside of the transaction
       # die to rollback
}

Support real life SQL

A lesson taught by all existing attempts at creating ORMs (in any language) but one that has yet to be learned is that while most SQL statements are rather simple, there will be use cases that require some real SQL mastery and if the ORM does not allow for the latter, it's value is greatly diminished.

It seems like every ORM starts out with the simple stuff. How can we make fetching one row from a table simple? How do we allow for boolean expressions in the where clause? Let's make joining simple tables by id key trivial.

These (and a couple more) are the things that ORMs make really simple but then real users come along and ask for more and more complicated things which then get bolted on top of the simple mechanisms. And suddenly you reach the point, where writing the SQL query is the easy part, but getting your ORM to generate that query starts getting really hard or downright impossible. Then you end up with an application that's a crude mixture of ORM code and hand crafted SQL statements combining the disadvantages of both.

The conclusion is that for the API design one should start with the absolute worst and most complicated SQL statement imaginable and find an API that allows one to express this. This statement should at least contain:

  • Common Table Expressions (preferably recursive)
  • Window Functions
  • Correlated subqueries in the select clause
  • Subqueries in the from clause
  • (Nested) calls to SQL functions with calling conventions that do not like C (i.e. extract(hour from some_timestamp_column)
  • Subqueries as arguments to function calls
  • Unions
  • Type coercion ("foo::int" or "interval '1 day'")
  • Grouping and ordering
  • Special operators like #> (Get JSON object at specified path in PostgreSQL)
  • Conditionals (CASE foo WHEN bar THEN baz ELSE qux END)

I'm sure there are a couple more interesting aspects of SQL, but this should get you started. They are all features that are in use in our code base. The trick will be to make these possible while keeping the trivial cases simple.

To finish, here's an example of a real life query from our production code that DBIx::Class is completely unequipped to handle:

with product_active(product_id, active, changetime, active_end) as (
    select product_id,
        active,
        changetime,
        lead(product_active_history.changetime, 1, now()::timestamp without time zone)
            OVER (
                PARTITION BY product_active_history.product_id
                ORDER BY product_active_history.changetime
            )
            as active_end
    from product_active_history
        join products on product_active_history.product_id = products.id
        join customers on products.customer_id = customers.id
    where products.article_id = 62
        and country_id in (1, 2, 3, 4)
        and products.customer_id not in (885, 840, 246, 362, 3233, 6378)
    order by product_active_history.product_id, product_active_history.changetime
),
valuation_date as (
    select (generate_series('2000-01-01'::date, now(), interval '1 year'))::date as day
)
select *,
    100.0 * (cancelled - lag(cancelled) over (order by day)) / online as cancelled_percent,
    100.0 * (redesign - lag(redesign) over (order by day)) / online as redesign_percent
from (
    select
        day,
        count(nullif(product_active.active_end > valuation_date.day, false)) as finished,
        count(nullif(product_active.active_end > valuation_date.day and product_active.active, false)) as online,
        count(nullif(product_active.active = false and product_active.active_end > valuation_date.day, false)) as offline,
        count(nullif(product_active.active = false and not exists (
            select * from products successor where successor.predecessor_id = product_active.product_id
        ) and product_active.active_end > valuation_date.day, false)) as cancelled,
        count(nullif(
            product_active.active = false
                and exists (select * from products successor where successor.predecessor_id = product_active.product_id)
                and product_active.active_end > valuation_date.day,
            false)) as redesign
    from
        valuation_date
        join product_active on product_active.changetime <= valuation_date.day
    group by day
) as data
order by day

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.