Skip to content

Instantly share code, notes, and snippets.

@pmurias
Forked from FCO/A Red Secret Santa.md
Last active December 2, 2018 23:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmurias/ed73ad5dabb746b98a4e15277a728a3a to your computer and use it in GitHub Desktop.
Save pmurias/ed73ad5dabb746b98a4e15277a728a3a to your computer and use it in GitHub Desktop.

The year is ending and We have a lot to celebrate! What is a better way to celebrate the end of the year than with our family and friend? To achieve that, here at my home, we decided to run a secret santa! So, my goal is to write a secret santa! That's something where I can use this wonderful project called Red.

Red is an ORM for perl6 still under development and not published as a module yet. But it's growing and it is close to a release.

So lets create our first table. A table that will store the people participating on our secret santa. To the code:

use Red;

model Person {
   has UInt     $.id        is serial;
   has Str      $.name      is column;
   has Str      $.email     is column;
}

my $*RED-DB = database "SQLite";

Person.^create-table;

Person.^create: :name<Fernando>,    :email<fco@aco.com>;
Person.^create: :name<Aline>,       :email<aja@aco.com>;
Person.^create: :name<Fernanda>;
Person.^create: :name<Sophia>;

.say for Person.^all.grep(*.email.defined).map: *.name;

Red maps a relational database to OOP. Each table is mapped to a Red class (model), each of its object represents a row.

The way we create a model is by using the model special word. A model is just a normal class that extends Red::Model and has a MetamodelX::Red::Model‘s object as it's metaclass. Red does not add any methods you didn’t explicit create to its models. So to interact with the database you should use the metaclass.

But lets continue.

It’s creates a new model called Person. The name of the table this model represents will be the same name as the model: “Person”. If necessary, you can change the name of the table with the is table<...> trait.

This model has 3 attributes:

2 of them are a is column trait; and one is a is serial. That means the same as is column{ :id, :auto-increment } (for a pk without auto increment use is id that means is column{ :id }). So every attribute on Person are columns. The is serial (is column{ :id }) means that its the table’s primary key.

After that it’s setting a dynamic var ($*RED-DB) for the result of database "SQLite". The database sub receive the driver‘s name and the parameters is expects.

In this case it uses the SQLite driver and if you don’t pass any argument, it will use it as a in memory database (if you want to use a file named secret-santa.db you can do database "SQLite", :database<secret-santa.db>. Or if you want to use a local Postgres, just use database "Pg"). Red uses the var $*RED-DB to know what database to use.

OK, now lets create the table! As I sad before, Red does not add any methods you didn’t explicitly ask for. So, to create the table a metaclass‘ method is used. Person.^create-table is how you create the table.

This will run:

CREATE TABLE person(
    id integer NOT NULL primary key AUTOINCREMENT,
    name varchar(255) NOT NULL,
    email varchar(255) NOT NULL
)

That creats the table.

Now we should insert some data. We do that with another meta method (.^create). .^create expect the same arguments .new would expect. Each named argument will set a attribute with the same name. .^create will create a new Person object, save it in database (with .^save: :insert) and return it.

It runs:

INSERT INTO person(
    email,
    name
) VALUES(
    'fco@aco.com',
    'Fernando'
)

Every model has a ResultSeq. That is a sequence that represents every row on the table. We can get it’s ResultSeq with .^all (or .^rs). ResultSeq has some methods to help you to get information from the table, for example: .grep will filter the rows (as it does in a normal Seq) but it doesn’t do that in memory, it returns a new ResultSeq with that filter setted. When its iterator is gotten, it runs a SQL query using every thing set on the ResultSeq.

On our example, Person.^all.grep(*.email.defined).map: *.name will run a query like:

SELECT
    person.name
FROM
    person
WHERE
    email IS NOT NULL

And it’ll print:

Fernando
Aline

Lets modify the code to make it save the wishlist for each one participating of the secret santa:

use Red;


model Person { ... }

model Wishlist {
    has UInt    $!id        is serial;
    has UInt    $!wisher-id is referencing{ Person.id };
    has Person  $.wisher    is relationship{ .wisher-id };
    has Str:D   $.name      is column is required;
    has Str     $.link      is column;
}

model Person is rw {
   has UInt     $.id        is serial;
   has Str      $.name      is column;
   has Str      $.email     is column;
   has Wishlist @.wishes    is relationship{ .wisher-id }
}

my $*RED-DB = database "SQLite";

Wishlist.^create-table;
Person.^create-table;

my \fernando = Person.^create: :name<Fernando>, :email<fco@aco.com>;
fernando.wishes.create: :name<Comma>,          :link<https://commaide.com>;
fernando.wishes.create: :name("perl6 books"),  :link<https://perl6book.com>;
fernando.wishes.create: :name("mac book pro"), :link<https://www.apple.com/shop/buy-mac/macbook-pro/15-inch-space-gray-2.6ghz-6-core-512gb#>;

my \aline = Person.^create: :name<Aline>, :email<aja@aco.com>;
aline.wishes.create: :name("a new closet"), :link<https://i.pinimg.com/474x/02/05/93/020593b34c205792a6a7fd7191333fc6--wardrobe-behind-bed-false-wall-wardrobe.jpg>;

my \fernanda = Person.^create: :name<Fernanda>, :email<faco@aco.com>;
fernanda.wishes.create: :name("mimikyu plush"), :link<https://www.pokemoncenter.com/mimikyu-poké-plush-%28standard-size%29---10-701-02831>;
fernanda.wishes.create: :name("camelia plush"), :link<https://farm9.static.flickr.com/8432/28947786492_80056225f3_b.jpg>;

my \sophia = Person.^create: :name<Sophia>, :email<saco@aco.com>;
sophia.wishes.create: :name("baby alive"), :link<https://www.target.com/p/baby-alive-face-paint-fairy-brunette/-/A-51304817>;

say "\n{ .name }\n{ .wishes.map({" { .name } => { .link }" }).join("\n").indent: 3 }" for Person.^all

That prints:

Fernando
    Comma => https://commaide.com
    perl6 books => https://perl6book.com
    mac book pro => https://www.apple.com/shop/buy-mac/macbook-pro/15-inch-space-gray-2.6ghz-6-core-512gb#

Aline
    a new closet => https://i.pinimg.com/474x/02/05/93/020593b34c205792a6a7fd7191333fc6--wardrobe-behind-bed-false-wall-wardrobe.jpg

Fernanda
    mimikyu plush => https://www.pokemoncenter.com/mimikyu-poké-plush-%28standard-size%29---10-701-02831
    camelia plush => https://farm9.static.flickr.com/8432/28947786492_80056225f3_b.jpg

Sophia
    baby alive => https://www.target.com/p/baby-alive-face-paint-fairy-brunette/-/A-51304817

Now we have a new model Wishlist that refers to a table named withlist. It has $!id as id, $!name and $!link are columns and there are something new! has UInt $!wisher-id is referencing{ Person.id }; is the same as has UInt $!wisher-id is column{ :references{ Person.id } }; that means is a column that’s a foreign key that references the id Person‘s column. It also has a has Person $.wisher is relationship{ .wisher-id }; it’s not a column, it’s a “virtual” field. the $ sigil means that there is only 1 wisher for a wish. And is relationship expects a Callable that will receive a model. If it’s Scalar it will receive the current model as the only argument. So, in this case it will be Wishlist. The return of the relationsip’s Callable must be a column that references some other column.

Lets see how this table is created:

CREATE TABLE wishlist(
   id integer NOT NULL primary key,
   name varchar(255) NOT NULL,
   link varchar(255) NULL,
   wisher_id integer NULL references person(id)
)

As you can see, no wisher column is created.

The Person model has changed too! Now it has a @.wishes relationship (has Wishlist @.wishes is relationship{ .wisher-id }). It uses a @ sigil so each Person can have more than one wish. The Callable passed will receive the type of the Positional attribute (Wishlist on this case) and must return a column that references some other column.

The table created is the same as before.

We created a new Person as we did before: my \fernando = Person.^create: :name, :emailfco@aco.com; and now we can use the relationship (wishes) to create a new wish (fernando.wishes.create: :name, :link<https://commaide.com>). That creates a new wish for Fernando running the following SQL:

INSERT INTO wishlist(
   name,
   link,
   wisher_id
) VALUES(
   'Comma',
   'https://commaide.com',
   1
)

view raw2-1.sql hosted with ❤ by GitHub Had you seen? wisher_id is 1… 1 is Fernando’s id. Once you have created the wish from the Fernando’s .wishes(), it already knows that it belongs to Fernando.

And then we define wishes for every person we create.

Then we loop over every Person in database (Person.^all) and print its name and loop over that person’s wishes and print its name and link.

OK, we can save who is on it… Get what they want… but and the draw? Who should I give a gift to? To do that we change our program again:

use lib <lib>;
use Red;

model Person { ... }

model Wishlist {
    has UInt    $!id        is id;
    has UInt    $!wisher-id is referencing{ Person.id };
    has Person  $.wisher    is relationship{ .wisher-id };
    has Str:D   $.name      is column is required;
    has Str     $.link      is column;
}

model Person is rw {
   has UInt     $.id        is id;
   has Str      $.name      is column;
   has Str      $.email     is column;
   has UInt     $!pair-id   is referencing{ ::?CLASS.^alias.id };
   has ::?CLASS $.pair      is relationship{ .pair-id };
   has Wishlist @.wishes    is relationship{ .wisher-id }

   method draw(::?CLASS:U:) {
      my @people = self.^all.pick: *;
      for flat @people.rotor: 2 => -1 -> $p1, $p2 {
         $p1.pair = $p2;
         $p1.^save;
      }
      given @people.tail {
         .pair = @people.head;
         .^save
      }
   }
}

my $*RED-DB = database "SQLite";

Wishlist.^create-table;
Person.^create-table;

my \fernando = Person.^create: :name<Fernando>, :email<fco@aco.com>;
fernando.wishes.create: :name<Comma>,            :link<https://commaide.com>;
fernando.wishes.create: :name("perl6 books"),    :link<https://perl6book.com>;
fernando.wishes.create: :name("mac book pro"),   :link<https://www.apple.com/shop/buy-mac/macbook-pro/15-inch-space-gray-2.6ghz-6-core-512gb#>;

my \aline = Person.^create: :name<Aline>, :email<aja@aco.com>;
aline.wishes.create: :name("a new closet"), :link<https://i.pinimg.com/474x/02/05/93/020593b34c205792a6a7fd7191333fc6--wardrobe-behind-bed-false-wall-wardrobe.jpg>;

my \fernanda = Person.^create: :name<Fernanda>, :email<faco@aco.com>;
fernanda.wishes.create: :name("mimikyu plush"), :link<https://www.pokemoncenter.com/mimikyu-poké-plush-%28standard-size%29---10-701-02831>;
fernanda.wishes.create: :name("camelia plush"), :link<https://farm9.static.flickr.com/8432/28947786492_80056225f3_b.jpg>;

my \sophia = Person.^create: :name<Sophia>,   :email<saco@aco.com>;
sophia.wishes.create: :name("baby alive"),      :link<https://www.target.com/p/baby-alive-face-paint-fairy-brunette/-/A-51304817>;

Person.draw;

say "{ .name } -> { .pair.name }\n\tWishlist: { .pair.wishes.map(*.name).join: ", " }" for Person.^all

Now Person has 2 new attributes ($!pair-id and $.pair) and a new method (draw). $!pair-id is a foreign key that references to the field id on the same table (Person) so we have to use an alias (.^alias). The other one is the relationship ($.pair) that uses that fk.

The new method (draw) is where the magic happens. It uses the method .pick: * that on normal Positional would shuffle the list. And it does the same here, with the query:

SELECT
   person.email , person.id , person.name , person.pair_id as "pair-id"
FROM
   person
ORDER BY
   random()

Once we shuffle the list, we use .rotor to get 2 items and go 1 back, so we save what is the pair of each person, but the last one, that is set to the first one.

And this is the output of our final code:

Fernando -> Sophia
	Wishlist: baby alive
Aline -> Fernanda
	Wishlist: mimikyu plush, camelia plush
Fernanda -> Fernando
	Wishlist: COMMA, perl6 books, mac book pro
Sophia -> Aline
	Wishlist: a new closet
Categories: 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment