diff --git a/Changes b/Changes index 9ba974fb..7c631107 100644 --- a/Changes +++ b/Changes @@ -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 diff --git a/docs/tutorials/experimental.md b/docs/tutorials/experimental.md index 87040bde..391d452d 100644 --- a/docs/tutorials/experimental.md +++ b/docs/tutorials/experimental.md @@ -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 @@ -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 ; +use Red ; ``` + +## Promoted features + +The following features were promoted to stable and no longer need to be enabled. +For backwards compatibility, `use Red ` 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) diff --git a/docs/tutorials/relationship.md b/docs/tutorials/relationship.md index a5dfa11b..23d202c2 100644 --- a/docs/tutorials/relationship.md +++ b/docs/tutorials/relationship.md @@ -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: @@ -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, :has-one); +} + +model Profile { + has UInt $.id is serial; + has UInt $.account-id is referencing(*.id, :model); + has Str $.bio is column; + has $.account is relationship(*.account-id, :model); +} +``` + +## 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); + +# For a relationship declared with :has-one: +my $profile = $account.profile.^create(:bio); +``` + +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`. diff --git a/lib/MetamodelX/Red/Model.rakumod b/lib/MetamodelX/Red/Model.rakumod index 729014cb..918b4f18 100644 --- a/lib/MetamodelX/Red/Model.rakumod +++ b/lib/MetamodelX/Red/Model.rakumod @@ -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; @@ -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; diff --git a/lib/MetamodelX/Red/Relationship.rakumod b/lib/MetamodelX/Red/Relationship.rakumod index bf851e50..894ab937 100644 --- a/lib/MetamodelX/Red/Relationship.rakumod +++ b/lib/MetamodelX/Red/Relationship.rakumod @@ -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 )" - if $attr.has-one and not %Red::experimentals; %!relationships ∪= $attr; my $name = $attr.name.substr: 2; if $attr.has_accessor { diff --git a/lib/Red.rakumod b/lib/Red.rakumod index adca34a3..743f6eaa 100644 --- a/lib/Red.rakumod +++ b/lib/Red.rakumod @@ -107,15 +107,7 @@ multi experimental("supply") { } multi experimental("is-handling") { - multi trait_mod:(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:' => &trait_mod: - ) + Empty } multi experimental("has-one") { @@ -123,9 +115,6 @@ multi experimental("has-one") { } multi experimental("refreshable") { - require ::('MetamodelX::Red::Refreshable'); - @Red::experimental-roles.push: ::('MetamodelX::Red::Refreshable'); - Empty } @@ -583,6 +572,7 @@ Red is a *WiP* ORM for Raku. =item C =item C> =item C +=item C =head3 features: @@ -643,7 +633,7 @@ that object to create the related row and fill the foreign key automatically: =begin code :lang my $parent = $child.parent.^create(:name); -# For a relationship declared with :has-one (use Red ): +# For a relationship declared with :has-one: my $profile = $account.profile.^create(:description); =end code @@ -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 trait "exports" C 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 method to expose; passing a pair renames it: + +=begin code :lang +model Bla is handling { 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 +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: ; # 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> diff --git a/lib/Red/Traits.rakumod b/lib/Red/Traits.rakumod index 449ff4bd..42048718 100644 --- a/lib/Red/Traits.rakumod +++ b/lib/Red/Traits.rakumod @@ -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) } @@ -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) } @@ -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) } @@ -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 diff --git a/t/100-single-relationship-create.rakutest b/t/100-single-relationship-create.rakutest index 3210f863..9178a434 100644 --- a/t/100-single-relationship-create.rakutest +++ b/t/100-single-relationship-create.rakutest @@ -1,5 +1,5 @@ use Test; -use Red <has-one>; +use Red; use X::Red::Exceptions; my Bool $fail-update = False; diff --git a/t/35-create.rakutest b/t/35-create.rakutest index 3c7b4399..db1283c8 100644 --- a/t/35-create.rakutest +++ b/t/35-create.rakutest @@ -1,5 +1,5 @@ use Test; -use Red <has-one>; +use Red; model Bla { has UInt $.id is serial; diff --git a/t/45-is-handling.rakutest b/t/45-is-handling.rakutest index 7a23403e..65ca0f22 100644 --- a/t/45-is-handling.rakutest +++ b/t/45-is-handling.rakutest @@ -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>; diff --git a/t/56-refreshable.rakutest b/t/56-refreshable.rakutest index c5a5bd9c..73b0f408 100644 --- a/t/56-refreshable.rakutest +++ b/t/56-refreshable.rakutest @@ -1,5 +1,5 @@ use Test; -use Red <refreshable>; +use Red; model TestModel { has Int $.id is serial; diff --git a/t/67-enums.rakutest b/t/67-enums.rakutest index dc3fec69..9ce0965a 100644 --- a/t/67-enums.rakutest +++ b/t/67-enums.rakutest @@ -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>;