Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
Revision history for Red

{{$NEXT}}
- Promote has-one, refreshable and is-handling to stable features.
- Continue accepting their historical experimental flags as no-ops for compatibility.
- schema: add named CHECK and PostgreSQL EXCLUDE constraints with driver selection
- indexes: support access methods, per-key operator classes and partial predicates
- pg: add range constructors and native range/regex expression helpers
Expand Down
26 changes: 15 additions & 11 deletions docs/tutorials/experimental.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
# Experimental features

* `is-handling`

make it possible to use the traint `is handling` that "exports" the given `ResultSeq`'s methods to the model itself

* `migrations`

make it possible to test the prototype of the migration feature
Expand All @@ -16,18 +12,26 @@

make Red use the `shortname` of the model to create the table's name instead of the full name

* `has-one`

creeates a new option to relationship: `has-one`. For more details: https://github.com/FCO/Red/issues/452

* `refreshable`
* `supply`

adds a `.^refresh` meta-method on defined model. It accepts a list of columns names or nothing at all, and "refreshes" those columns (ar all columns if no list was defined) with the actual data from the database.
adds a `.^supply` meta-method on the model returning a `Supply` of rows

## How to use experimental feartures?

when `use`ing Red, pass a list of wanted experimental features, For example:

```raku
use Red <is-handling migrations formatters shortname has-one refreshable>;
use Red <migrations formatters shortname supply>;
```

## Promoted features

The following features were promoted to stable and no longer need to be enabled.
For backwards compatibility, `use Red <has-one refreshable is-handling>` still
compiles, but the flags are now no-ops:

* `is-handling` — see `is handling` trait (exports `ResultSeq` methods to the model itself)
* `has-one` — see [relationships](relationship.md)
* `refreshable` — adds `.^refresh` on model instances (accepts a list of column
names or nothing at all, and "refreshes" those columns — or all columns if no
list was defined — with the actual data from the database)
47 changes: 46 additions & 1 deletion docs/tutorials/relationship.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ that represents the related model, a Callable positional parameter that will ret
- `@` - Represents a `has many` relationship that "stores" a `ResultSeq` of elements. [Red](https://github.com/FCO/Red) will run the callable passing the model
defined by the `:model` named parameter.

(for the experimental `has-one`, please take a look at [experimental doc](experimental.md))
- `$` with the `:has-one` option - Represents a `has one` relationship: the foreign key lives on the *other* model's table.
Accessing it returns the related object (or an undefined creation context when empty — see "creating through a scalar
relationship" below). On `.^create`, Red fills the foreign key on the related row automatically.

## So, for example:

Expand Down Expand Up @@ -55,3 +57,46 @@ $author.books.grep(*.starts-with: "Ben").elems
```

For getting the number of books written by "Chico Buarque" that starts with "Ben".

## has-one

A scalar relationship declared with `:has-one` means "the other table holds the foreign key". For example, an account
has one profile, and the `profile` table holds the `account-id`:

```raku
model Profile { ... }

model Account {
has UInt $.id is serial;
has Str $.name is column;
has Profile $.profile is relationship(*.account-id, :model<Profile>, :has-one);
}

model Profile {
has UInt $.id is serial;
has UInt $.account-id is referencing(*.id, :model<Account>);
has Str $.bio is column;
has $.account is relationship(*.account-id, :model<Account>);
}
```

## 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:

```raku
my $parent = $child.parent.^create(:name<parent>);

# For a relationship declared with :has-one:
my $profile = $account.profile.^create(:bio<hello>);
```

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`.
2 changes: 2 additions & 0 deletions lib/MetamodelX/Red/Model.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use Red::AST::CreateView;
use MetamodelX::Red::Dirtable;
use MetamodelX::Red::Comparate;
use MetamodelX::Red::Migration;
use MetamodelX::Red::Refreshable;
use MetamodelX::Red::Relationship;
use MetamodelX::Red::Describable;
use MetamodelX::Red::OnDB;
Expand All @@ -44,6 +45,7 @@ unit class MetamodelX::Red::Model is Metamodel::ClassHOW;
also does MetamodelX::Red::Dirtable;
also does MetamodelX::Red::Comparate;
#also does MetamodelX::Red::Migration;
also does MetamodelX::Red::Refreshable;
also does MetamodelX::Red::Relationship;
also does MetamodelX::Red::Describable;
also does MetamodelX::Red::OnDB;
Expand Down
2 changes: 0 additions & 2 deletions lib/MetamodelX/Red/Relationship.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,6 @@ multi method add-relationship(

#| Adds a new relationship using an attribute of type `Red::Attr::Relationship`.
multi method add-relationship(::Type Mu:U $self, Red::Attr::Relationship $attr) {
die ":has-one isn't accepted on relationship without its experimental feature (use Red <has-one>)"
if $attr.has-one and not %Red::experimentals<has-one>;
%!relationships ∪= $attr;
my $name = $attr.name.substr: 2;
if $attr.has_accessor {
Expand Down
50 changes: 37 additions & 13 deletions lib/Red.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -107,25 +107,14 @@ multi experimental("supply") {
}

multi experimental("is-handling") {
multi trait_mod:<is>(Mu:U $model, :$handling) {
for $handling<> {
my ($orig, $new) = $_ ~~ Pair ?? [.key, .value] !! [$_, $_];
$model.^add_method: $new, method (|c) { self.^all."$orig"(|c) }
}
}
Map(
'&trait_mod:<is>' => &trait_mod:<is>
)
Empty
}

multi experimental("has-one") {
Empty
}

multi experimental("refreshable") {
require ::('MetamodelX::Red::Refreshable');
@Red::experimental-roles.push: ::('MetamodelX::Red::Refreshable');

Empty
}

Expand Down Expand Up @@ -583,6 +572,7 @@ Red is a *WiP* ORM for Raku.
=item C<is relationship{}>
=item C<is table<>>
=item C<is nullable>
=item C<is handling>

=head3 features:

Expand Down Expand Up @@ -643,7 +633,7 @@ that object to create the related row and fill the foreign key automatically:
=begin code :lang<raku>
my $parent = $child.parent.^create(:name<parent>);

# For a relationship declared with :has-one (use Red <has-one>):
# For a relationship declared with :has-one:
my $profile = $account.profile.^create(:description<profile>);
=end code

Expand Down Expand Up @@ -906,6 +896,40 @@ transaction's Promise represents only execution of its block, not guard completi
model Bla is temp { ... }
=end code

=head4 is handling

The C<is handling> trait "exports" C<ResultSeq> methods to the model itself, so
they can be called directly on the model's type object. Each positional value
is the name of a C<ResultSeq> method to expose; passing a pair renames it:

=begin code :lang<raku>
model Bla is handling<map grep> { has Int $.a is column }

Bla.map(*.a); # same as Bla.^all.map(*.a)
Bla.grep(*.a < 5); # same as Bla.^all.grep(*.a < 5)

model Ble is handling(map => "update", grep => "where") { has Int $.a is column }

Ble.update(*.a); # same as Ble.^all.map(*.a), renamed
Ble.where(*.a < 5); # same as Ble.^all.grep(*.a < 5), renamed
=end code

=head4 refresh

The C<.^refresh> meta-method reloads a model instance's columns with the actual
data from the database. It accepts a list of column (attribute) names, or
nothing at all to refresh every column:

=begin code :lang<raku>
my $post = Post.^load: 42;
# ... some external process updates the row ...
$post.^refresh; # refreshes all columns
$post.^refresh: "title"; # refreshes only the "title" column
$post.^refresh: <title body>; # refreshes "title" and "body"
=end code

Refreshed columns are also marked clean on the model's dirty tracking.

=head4 Create table

=begin code :lang<raku>
Expand Down
19 changes: 11 additions & 8 deletions lib/Red/Traits.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,6 @@ multi trait_mod:<is>(
)
--> Empty
) is export {
die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
if $has-one && !%Red::experimentals<has-one>;
$attr.package.^add-relationship: $attr, &relationship, |(:$model with $model), |(:$require with $require), :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

Expand All @@ -288,8 +286,6 @@ multi trait_mod:<is>(
)
--> Empty
) is export {
die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
if $has-one && !%Red::experimentals<has-one>;
$attr.package.^add-relationship: $attr, &relationship, :model-type($model), :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

Expand All @@ -308,8 +304,6 @@ multi trait_mod:<is>(
)
--> Empty
) is export {
die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
if $has-one && !%Red::experimentals<has-one>;
$attr.package.^add-relationship: $attr, :$column, :$model, :$require, :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

Expand All @@ -326,11 +320,20 @@ multi trait_mod:<is>(
)
--> Empty
) {
die "Please, use the has-one experimental feature (use Red <has-one>) to allow using it on relationships"
if $has-one && !%Red::experimentals<has-one>;
$attr.package.^add-relationship: $attr, |@relationship, :$model, :$require, :$optional, :$no-prefetch, |(:$has-one if $has-one)
}

=head3 is handling

#| Trait that "exports" ResultSeq methods to the model itself. Each value can be a
#| method name or a pair of method name => new name to be used on the model.
multi trait_mod:<is>(Mu:U $model, :$handling --> Empty) is export {
for $handling<> {
my ($orig, $new) = $_ ~~ Pair ?? [.key, .value] !! [$_, $_];
$model.^add_method: $new, method (|c) { self.^all."$orig"(|c) }
}
}

=head3 is before-create

#| Trait to define a phaser to run before create a new record
Expand Down
2 changes: 1 addition & 1 deletion t/100-single-relationship-create.rakutest
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use Test;
use Red <has-one>;
use Red;
use X::Red::Exceptions;

my Bool $fail-update = False;
Expand Down
2 changes: 1 addition & 1 deletion t/35-create.rakutest
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use Test;
use Red <has-one>;
use Red;

model Bla {
has UInt $.id is serial;
Expand Down
2 changes: 1 addition & 1 deletion t/45-is-handling.rakutest
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use Test;

use Red < is-handling >;
use Red;

my $*RED-DEBUG = $_ with %*ENV<RED_DEBUG>;
my $*RED-DEBUG-RESPONSE = $_ with %*ENV<RED_DEBUG_RESPONSE>;
Expand Down
2 changes: 1 addition & 1 deletion t/56-refreshable.rakutest
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use Test;
use Red <refreshable>;
use Red;

model TestModel {
has Int $.id is serial;
Expand Down
2 changes: 1 addition & 1 deletion t/67-enums.rakutest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env raku

use Test;
use Red:api<2> <refreshable>;
use Red:api<2>;

# TODO: Is it breaking for Pg?
plan :skip-all("Different driver setted ($_)") with %*ENV<RED_DATABASE>;
Expand Down
Loading