-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add relations support and associated tests
Introduced a comprehensive set of relationship macros to define 'belongs_to', 'has_many', 'has_one', and 'many_to_many' associations within the Cql module. Added extensive unit tests to validate these associations using mock entities and database schema. Updated core query and record handling methods to improve flexibility and ensure compatibility with the new relations. This enhancement facilitates defining and managing complex data object relationships, significantly simplifying development workflows involving relational databases.
- Loading branch information
Showing
9 changed files
with
354 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
require "./spec_helper" | ||
AcmeDB = Cql::Schema.build( | ||
:acme_db, | ||
adapter: Cql::Adapter::Postgres, | ||
uri: ENV["DATABASE_URL"]) do | ||
table :movies do | ||
primary :id, Int64, auto_increment: true | ||
text :title | ||
end | ||
|
||
table :screenplays do | ||
primary :id, Int64, auto_increment: true | ||
bigint :movie_id | ||
text :content | ||
end | ||
|
||
table :actors do | ||
primary :id, Int64, auto_increment: true | ||
text :name | ||
end | ||
|
||
table :movies_actors do | ||
primary :id, Int64, auto_increment: true | ||
bigint :movie_id | ||
bigint :actor_id | ||
end | ||
end | ||
|
||
struct Movie | ||
include Cql::Record(Movie) | ||
include Cql::Relations | ||
|
||
define AcmeDB, :movies | ||
|
||
has_one :screenplay, Screenplay | ||
many_to_many :actors, Actor, join_through: :movies_actors | ||
|
||
getter id : Int64? | ||
getter title : String | ||
|
||
def initialize(@title : String) | ||
end | ||
end | ||
|
||
struct Screenplay | ||
include Cql::Record(Screenplay) | ||
include Cql::Relations | ||
|
||
define AcmeDB, :screenplays | ||
|
||
belongs_to :movie, foreign_key: :movie_id | ||
|
||
getter id : Int64? | ||
getter content : String | ||
|
||
def initialize(@movie_id : Int64, @content : String) | ||
end | ||
end | ||
|
||
struct Actor | ||
include Cql::Record(Actor) | ||
include Cql::Relations | ||
|
||
define AcmeDB, :actors | ||
|
||
getter id : Int64? | ||
getter name : String | ||
|
||
def initialize(@name : String) | ||
end | ||
end | ||
|
||
struct MoviesActors | ||
include Cql::Record(MoviesActors) | ||
|
||
define AcmeDB, :movies_actors | ||
|
||
getter id : Int64? | ||
getter movie_id : Int64 | ||
getter actor_id : Int64 | ||
|
||
def initialize(@movie_id : Int64, @actor_id : Int64) | ||
end | ||
end | ||
|
||
describe Cql::Relations do | ||
before_each do | ||
AcmeDB.movies.create! | ||
AcmeDB.screenplays.create! | ||
AcmeDB.actors.create! | ||
AcmeDB.movies_actors.create! | ||
end | ||
|
||
# after_each do | ||
# AcmeDB.movies.drop! | ||
# AcmeDB.screenplays.drop! | ||
# AcmeDB.actors.drop! | ||
# # AcmeDB.movies_actors.drop! | ||
# end | ||
# | ||
|
||
describe "belongs_to" do | ||
it "defines the belongs_to association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
screenplay_id = Screenplay.create(movie_id: movie_id, content: "The screenplay") | ||
screenplay = Screenplay.find!(screenplay_id) | ||
movie = Movie.find!(movie_id) | ||
|
||
screenplay.movie = movie | ||
screenplay.save | ||
|
||
screenplay.movie.title.should eq "The Godfather" | ||
end | ||
|
||
it "builds the association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
screenplay_id = Screenplay.create(movie_id: movie_id, content: "The screenplay") | ||
screenplay = Screenplay.find!(screenplay_id) | ||
movie = screenplay.build_movie(title: "The Godfather") | ||
|
||
movie.title.should eq "The Godfather" | ||
end | ||
|
||
it "updates the association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
screenplay_id = Screenplay.create(movie_id: movie_id, content: "The screenplay") | ||
screenplay = Screenplay.find!(screenplay_id) | ||
|
||
screenplay.movie.title.should eq "The Godfather" | ||
screenplay.update_movie(title: "The Godfather 10") | ||
screenplay.movie.title.should eq "The Godfather 10" | ||
end | ||
|
||
it "deletes the association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
screenplay_id = Screenplay.create(movie_id: movie_id, content: "The screenplay") | ||
screenplay = Screenplay.find!(screenplay_id) | ||
|
||
screenplay.movie.title.should eq "The Godfather" | ||
screenplay.delete_movie | ||
|
||
expect_raises DB::NoResultsError do | ||
screenplay.movie | ||
end | ||
end | ||
end | ||
|
||
describe "has_one" do | ||
it "defines the has_one association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
screenplay_id = Screenplay.create(movie_id: movie_id, content: "The screenplay") | ||
movie = Movie.find!(movie_id) | ||
movie.screenplay = Screenplay.find!(screenplay_id) | ||
movie.save | ||
|
||
screenplay = movie.screenplay | ||
screenplay.movie = movie | ||
|
||
screenplay.content.should eq "The screenplay" | ||
end | ||
end | ||
|
||
describe "many_to_many" do | ||
it "defines the many_to_many association" do | ||
movie_id = Movie.create(title: "The Godfather") | ||
actor1 = Actor.create(name: "Marlon Brando") | ||
actor2 = Actor.create(name: "Al Pacino") | ||
movie = Movie.find!(movie_id) | ||
|
||
movie << Actor.find!(actor1) | ||
movie << Actor.find!(actor2) | ||
|
||
actors = movie.actors! | ||
|
||
actors.size.should eq 2 | ||
actors[0].name.should eq "Marlon Brando" | ||
actors[1].name.should eq "Al Pacino" | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
require "./relations/*" | ||
|
||
module Cql | ||
module Relations | ||
# Define class-level storage for associations | ||
@@associations = {} of Symbol => Hash(Symbol, Symbol) | ||
macro included | ||
include Cql::Relations::BelongsTo | ||
include Cql::Relations::HasMany | ||
include Cql::Relations::HasOne | ||
include Cql::Relations::ManyToMany | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module Cql::Relations | ||
module BelongsTo | ||
# Define the belongs_to association | ||
macro belongs_to(assoc, foreign_key) | ||
@{{foreign_key.id}} : {{assoc.camelcase.id}} | ||
|
||
def {{assoc.id}} : {{assoc.camelcase.id}} | ||
{{assoc.camelcase.id}}.find!(@{{foreign_key.id}}) | ||
end | ||
|
||
property {{assoc.id}}_id : Int64 | ||
|
||
def {{assoc.id}}=(record : {{assoc.camelcase.id}}) | ||
@{{foreign_key.id}} = record.id.not_nil! | ||
end | ||
|
||
def build_{{assoc.id}}(**attributes) : {{assoc.camelcase.id}} | ||
{{assoc.camelcase.id}}.new(**attributes) | ||
end | ||
|
||
def create_{{assoc.id}}(**attributes) : {{assoc.camelcase.id}} | ||
@{{foreign_key.id}} = {{assoc.camelcase.id}}.create!(**attributes) | ||
end | ||
|
||
def update_{{assoc.id}}(**attributes) : {{assoc.camelcase.id}} | ||
{{assoc.camelcase.id}}.update(@{{foreign_key.id}}, **attributes) | ||
movie | ||
end | ||
|
||
def delete_{{assoc.id}} : Bool | ||
{{assoc.camelcase.id}}.delete(@{{foreign_key.id}}).rows_affected > 0 | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
module Cql::Relations | ||
# Define the has_many association | ||
module HasMany | ||
macro has_many(name, foreign_key) | ||
def {{name.id}} : Array(T) | ||
primary_key_value = @id | ||
T.query.where({{foreign_key.id}} => primary_key_value).all | ||
end | ||
|
||
def build_{{name.id.singularize}}(attributes : Hash(Symbol, _)) : T | ||
attributes[:{{foreign_key.id}}] = @id | ||
record = T.new(attributes) | ||
record | ||
end | ||
|
||
def create_{{name.id.singularize}}(attributes : Hash(Symbol, _)) : T | ||
record = build_{{name.id.singularize}}(attributes) | ||
record.save | ||
record | ||
end | ||
|
||
def update_{{name.id.singularize}}(id : Int64, attributes : Hash(Symbol, _)) : T | ||
record = T.query.where(id: id).first | ||
record.update(attributes) | ||
record | ||
end | ||
|
||
def delete_{{name.id.singularize}}(id : Int64) : Bool | ||
record = T.query.where(id: id).first | ||
record.delete | ||
end | ||
|
||
def <<(record : T) : Bool | ||
record.{{foreign_key.id}} = @id | ||
record.save | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
module Cql::Relations | ||
# Define the has_one association | ||
module HasOne | ||
macro has_one(name, foreign_key) | ||
@@associations[:has_one] ||= {} of Symbol => Symbol | ||
@@associations[:has_one][:{{name.id}}] = :{{foreign_key.stringify.underscore.id}} | ||
|
||
def {{name.id}} : {{foreign_key.id}} | ||
{{foreign_key}}.find_by({{T.stringify.underscore.id}}_id: @id) | ||
end | ||
|
||
def {{name.id}}=(record : {{foreign_key.id}}) | ||
record.{{T.name.underscore.id}}_id = @id.not_nil! | ||
end | ||
|
||
def build_{{name.id}}(attributes : Hash(Symbol, DB::Any)) : T | ||
attributes[:{{foreign_key.stringify.underscore.id}}] = @id | ||
record = T.new(attributes) | ||
record | ||
end | ||
|
||
def create_{{name.id}}(attributes : Hash(Symbol, DB::Any)) : T | ||
attributes[:{{foreign_key.id}}] = @id | ||
record = build_{{name.id}}(attributes) | ||
record.save | ||
record | ||
end | ||
|
||
def update_{{name.id}}(attributes : Hash(Symbol, DB::Any)) : T | ||
record = {{name.id}} | ||
record.update(attributes) | ||
record | ||
end | ||
|
||
def delete_{{name.id}} : Bool | ||
record = {{name.id}} | ||
record.delete | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.