From 35e14a7ca553563805f8b3c46645410d0e898484 Mon Sep 17 00:00:00 2001 From: eliasjpr Date: Fri, 23 Aug 2024 09:46:48 -0400 Subject: [PATCH] Refactor Record to use class inheritance Refactored Record and relation structures to utilize class inheritance instead of 'include' for module inclusion. Replaced `define` with `db_context` to set schema and table context more explicitly. Simplified type references and improved readability by using `@type` in macros. These changes make the codebase cleaner and more maintainable while enhancing the explicitness of class relationships and table definitions. --- spec/record_spec.cr | 11 ++--- spec/relations_spec.cr | 36 +++++--------- src/record.cr | 92 +++++++++++++++++------------------ src/relations/has_one.cr | 24 ++++----- src/relations/many_to_many.cr | 16 +++--- src/schema.cr | 15 ++++++ 6 files changed, 95 insertions(+), 99 deletions(-) diff --git a/spec/record_spec.cr b/spec/record_spec.cr index 8ced30e..a6f0377 100644 --- a/spec/record_spec.cr +++ b/spec/record_spec.cr @@ -18,10 +18,8 @@ AcmeDB = Cql::Schema.define( end end -struct Post - include Cql::Record(Post, Int64) - - define AcmeDB, :posts +struct Post < Cql::Record(Int64) + db_context AcmeDB, :posts getter id : Int64? getter title : String @@ -32,9 +30,8 @@ struct Post end end -struct Comment - include Cql::Record(Comment, Int64) - define AcmeDB, :comments +struct Comment < Cql::Record(Int64) + db_context AcmeDB, :comments getter id : Int64? getter post_id : Int64 diff --git a/spec/relations_spec.cr b/spec/relations_spec.cr index 627d055..14066ab 100644 --- a/spec/relations_spec.cr +++ b/spec/relations_spec.cr @@ -1,8 +1,6 @@ require "./spec_helper" -AcmeDB2 = Cql::Schema.define( - :acme_db, - adapter: Cql::Adapter::Postgres, - uri: ENV["DATABASE_URL"]) do + +AcmeDB2 = Cql::Schema.define(:acme_db, adapter: Cql::Adapter::Postgres, uri: ENV["DATABASE_URL"]) do table :movies do primary :id, Int64, auto_increment: true text :title @@ -32,10 +30,8 @@ AcmeDB2 = Cql::Schema.define( end end -struct Actor - include Cql::Record(Actor, Int64) - - define AcmeDB2, :actors +struct Actor < Cql::Record(Int64) + db_context AcmeDB2, :actors getter id : Int64? getter name : String @@ -44,10 +40,8 @@ struct Actor end end -struct Movie - include Cql::Record(Movie, Int64) - - define AcmeDB2, :movies +struct Movie < Cql::Record(Int64) + db_context AcmeDB2, :movies has_one :screenplay, Screenplay many_to_many :actors, Actor, join_through: :movies_actors @@ -60,10 +54,8 @@ struct Movie end end -struct Director - include Cql::Record(Director, Int64) - - define AcmeDB2, :directors +struct Director < Cql::Record(Int64) + db_context AcmeDB2, :directors getter id : Int64? getter name : String @@ -73,10 +65,8 @@ struct Director end end -struct Screenplay - include Cql::Record(Screenplay, Int64) - - define AcmeDB2, :screenplays +struct Screenplay < Cql::Record(Int64) + db_context AcmeDB2, :screenplays belongs_to :movie, foreign_key: :movie_id @@ -87,10 +77,8 @@ struct Screenplay end end -struct MoviesActors - include Cql::Record(MoviesActors, Int64) - - define AcmeDB2, :movies_actors +struct MoviesActors < Cql::Record(Int64) + db_context AcmeDB2, :movies_actors getter id : Int64? getter movie_id : Int64 diff --git a/src/record.cr b/src/record.cr index 4142062..f6ed3e0 100644 --- a/src/record.cr +++ b/src/record.cr @@ -20,10 +20,8 @@ module Cql # end # end # - # struct Post - # include Cql::Record(Post) - # - # define AcmeDB, :posts + # struct Post < Cql::Record(Int64) + # db_context AcmeDB, :posts # # getter id : Int64? # getter title : String @@ -34,9 +32,8 @@ module Cql # end # end # - # struct Comment - # include Cql::Record(Comment) - # define AcmeDB, :comments + # struct Comment < Cql::Record(Int64) + # db_context AcmeDB, :comments # # getter id : Int64? # getter post_id : Int64 @@ -46,8 +43,8 @@ module Cql # end # end # ``` - module Record(T, Pk) - macro included + abstract struct Record(Pk) + macro inherited include DB::Serializable include DB::Serializable::NonStrict include Cql::Relations @@ -62,12 +59,11 @@ module Cql # **Example** Defining the schema and table # # ``` - # struct User - # include Cql::Record(User) - # define AcmeDB, :users + # struct User < Cql::Record(Int64) + # db_context AcmeDB, :users # end # ``` - def self.define(schema : Cql::Schema, table : Symbol) + def self.db_context(schema : Cql::Schema, table : Symbol) @@schema = schema @@table = table end @@ -116,7 +112,7 @@ module Cql # ``` # def self.build(**fields) - T.new(**fields) + new(**fields) end # Return a new query object for the current table @@ -125,15 +121,15 @@ module Cql # **Example** Fetching all records # # ``` - # user_repo.query.all(T) - # user_repo.query.where(active: true).all(T) + # user_repo.query.all({{@type.id}}) + # user_repo.query.where(active: true).all({{@type.id}}) # ``` def self.query - Cql::Query.new(T.schema).from(T.table) + Cql::Query.new({{@type.id}}.schema).from({{@type.id}}.table) end # Fetch all records of type T - # - **@return** [Array(T)] The records + # - **@return** [Array({{@type.id}})] The records # # **Example** Fetching all records # @@ -141,7 +137,7 @@ module Cql # user_repo.all # ``` def self.all - query.all(T) + query.all({{@type.id}}) end # Find a record by ID, return nil if not found @@ -154,7 +150,7 @@ module Cql # user_repo.find(1) # ``` def self.find(id) - query.where(id: id).first(T) + query.where(id: id).first({{@type.id}}) rescue DB::NoResultsError nil end @@ -169,7 +165,7 @@ module Cql # user_repo.find!(1) # ``` def self.find!(id) - query.where(id: id).first!(T) + query.where(id: id).first!({{@type.id}}) end # Find a record by specific fields @@ -182,7 +178,7 @@ module Cql # user_repo.find_by(email: " [email protected]") # ``` def self.find_by(**fields) - query.where(**fields).first(T) + query.where(**fields).first({{@type.id}}) end # Find a record by specific fields, raise an error if not found @@ -194,12 +190,12 @@ module Cql # user_repo.find_by!(email: " [email protected]") # ``` def self.find_by!(**fields) - query.where(**fields).first!(T) + query.where(**fields).first!({{@type.id}}) end # Find all records matching specific fields # - **@param** fields [Hash(Symbol, DB::Any)] The fields to match - # - **@return** [Array(T)] The records + # - **@return** [Array({{@type.id}})] The records # # **Example** Fetching all active users # @@ -207,7 +203,7 @@ module Cql # user_repo.find_all_by(active: true) # ``` def self.find_all_by(**fields) - query.where(**fields).all(T) + query.where(**fields).all({{@type.id}}) end # Return a new insert object for the current table @@ -219,7 +215,7 @@ module Cql # user_repo.insert.values(name: "Alice", email: " [email protected]").commit # ``` def self.insert - Cql::Insert.new(T.schema).into(T.table) + Cql::Insert.new({{@type.id}}.schema).into({{@type.id}}.table) end # Create a new record with given attributes @@ -246,7 +242,7 @@ module Cql insert.values(**fields).last_insert_id end - def self.create(record : T) + def self.create(record : {{@type.id}}) attrs = record.attributes attrs.delete(:id) create(attrs) @@ -261,7 +257,7 @@ module Cql # user_repo.update.set(active: true).where(id: 1).commit # ``` def self.update - Cql::Update.new(T.schema).table(T.table) + Cql::Update.new({{@type.id}}.schema).table({{@type.id}}.table) end # Update a record by ID with given attributes @@ -317,7 +313,7 @@ module Cql # # User.update(1, bob) # ``` - def self.update(id, record : T) + def self.update(id, record : {{@type.id}}) update.set(record.attributes).where(id: id).commit end @@ -330,7 +326,7 @@ module Cql # ``` # user_repo.update(email: " [email protected]", active: true) # ``` - def self.update(record : T) + def self.update(record : {{@type.id}}) attrs = record.attributes attrs.delete(:id) update.set(attrs).where(id: record.id).commit @@ -373,7 +369,7 @@ module Cql # user_repo.delete.where(id: 1).commit # ``` def self.delete - Cql::Delete.new(T.schema).from(T.table) + Cql::Delete.new({{@type.id}}.schema).from({{@type.id}}.table) end # Delete a record by ID @@ -433,7 +429,7 @@ module Cql # user_repo.exists?(email: " [email protected]") # ``` def self.exists?(**fields) - query.select.where(**fields).limit(1).first(T) != nil + query.select.where(**fields).limit(1).first({{@type.id}}) != nil rescue DB::NoResultsError false end @@ -447,7 +443,7 @@ module Cql # user_repo.first # ``` def self.first - query.order(id: :asc).limit(1).first(T) + query.order(id: :asc).limit(1).first({{@type.id}}) end # Fetch the last record in the table @@ -459,13 +455,13 @@ module Cql # user_repo.last # ``` def self.last - query.order(id: :desc).limit(1).first(T) + query.order(id: :desc).limit(1).first({{@type.id}}) end # Paginate results based on page number and items per page # - **@param** page_number [Int32] The page number to fetch # - **@param** per_page [Int32] The number of items per page - # - **@return** [Array(T)] The records for the page + # - **@return** [Array({{@type.id}})] The records for the page # # **Example** Paginating results # @@ -474,12 +470,12 @@ module Cql # ``` def self.page(page_number, per_page = 10) offset = (page_number - 1) * per_page - query.limit(per_page).offset(offset).all(T) + query.limit(per_page).offset(offset).all({{@type.id}}) end # Limit the number of results per page # - **@param** per_page [Int32] The number of items per page - # - **@return** [Array(T)] The records for the page + # - **@return** [Array({{@type.id}})] The records for the page # # **Example** Limiting results per page # @@ -487,7 +483,7 @@ module Cql # user_repo.per_page(10) # ``` def self.per_page(per_page) - query.limit(per_page).all(T) + query.limit(per_page).all({{@type.id}}) end end @@ -501,8 +497,8 @@ module Cql # user.reload! # ``` def reload! - record = T.find!(id) - {% for ivar in T.instance_vars %} + record = {{@type.id}}.find!(id) + {% for ivar in @type.instance_vars %} @{{ ivar }} = record.{{ ivar }} {% end %} end @@ -529,9 +525,9 @@ module Cql # ``` def save if @id.nil? - @id = T.create(self).as(Pk) + @id = {{@type.id}}.create(self).as(Pk) else - T.update(self) + {{@type.id}}.update(self) end end @@ -544,7 +540,7 @@ module Cql # user.delete # ``` def update(fields : Hash(Symbol, DB::Any)) - T.update(fields, where) + {{@type.id}}.update(fields, where) end # Update the record with the given fields @@ -557,7 +553,7 @@ module Cql # user.update(name: "Alice", email: " [email protected]") # ``` def update(**fields) - T.update(id, **fields) + {{@type.id}}.update(id, **fields) end # Update the record with the given record object @@ -575,7 +571,7 @@ module Cql # bob.update # ``` def update - T.update(id, self) unless id.nil? + {{@type.id}}.update(id, self) unless id.nil? end # Delete the record from the database @@ -587,7 +583,7 @@ module Cql # user.delete # ``` def delete - T.delete(id) unless id.nil? + {{@type.id}}.delete(id) unless id.nil? end # Define instance-level methods for querying and manipulating data @@ -602,7 +598,7 @@ module Cql # ``` def attributes hash = Hash(Symbol, DB::Any).new - {% for ivar in T.instance_vars %} + {% for ivar in @type.instance_vars %} {% unless ivar.annotation(DB::Field) && ivar.annotation(DB::Field).named_args[:ignore] %} hash[:{{ ivar }}] = {{ ivar }} {% end %} @@ -621,7 +617,7 @@ module Cql # ``` def attributes(attrs : Hash(Symbol, DB::Any)) attrs.each do |key, value| - {% for ivar in T.instance_vars %} + {% for ivar in @type.instance_vars %} @{{ivar.id}} = value if key == :{{ivar.id}} && value.is_a?({{ivar.type}}) {% end %} end diff --git a/src/relations/has_one.cr b/src/relations/has_one.cr index 65a4097..3bea2f7 100644 --- a/src/relations/has_one.cr +++ b/src/relations/has_one.cr @@ -1,35 +1,35 @@ module Cql::Relations # Define the has_one association module HasOne - macro has_one(name, type) - def {{name.id}} : {{type.id}} - {{type.id}}.find_by({{T.stringify.underscore.id}}_id: @id) + macro has_one(name, kind) + def {{name.id}} : {{kind.id}} + {{kind.id}}.find_by({{@type.stringify.underscore.id}}_id: @id) end - def {{name.id}}=(record : {{type.id}}) - record.{{T.name.underscore.id}}_id = @id.not_nil! + def {{name.id}}=(record : {{kind.id}}) + record.{{@type.name.underscore.id}}_id = @id.not_nil! end - def build_{{name.id}}(**attributes) : {{type.id}} - attr = attributes.merge({{T.stringify.underscore.id}}_id: @id.not_nil!) - record = {{type.id}}.new(**attr) + def build_{{name.id}}(**attributes) : {{kind.id}} + attr = attributes.merge({{@type.stringify.underscore.id}}_id: @id.not_nil!) + record = {{kind.id}}.new(**attr) record end - def create_{{name.id}}(**attributes) : {{type.id}} - record = build_{{type.stringify.underscore.id}}(**attributes) + def create_{{name.id}}(**attributes) : {{kind.id}} + record = build_{{kind.stringify.underscore.id}}(**attributes) record.save {{name.id}} end - def update_{{name.id}}(**attributes) : {{type.id}} + def update_{{name.id}}(**attributes) : {{kind.id}} record = {{name.id}} record.update(**attributes) record end def delete_{{name.id}} : Bool - {{type.id}}.delete({{name.id}}.id).rows_affected > 0 + {{kind.id}}.delete({{name.id}}.id).rows_affected > 0 end end end diff --git a/src/relations/many_to_many.cr b/src/relations/many_to_many.cr index 85c4d12..df1deb4 100644 --- a/src/relations/many_to_many.cr +++ b/src/relations/many_to_many.cr @@ -31,17 +31,17 @@ module Cql::Relations # property actor_id : Int64 # end # ``` - macro many_to_many(name, type, join_through, cascade = false) + macro many_to_many(name, klass, join_through, cascade = false) @[DB::Field(ignore: true)] - getter {{name.id}} : Cql::Relations::ManyCollection({{type.id}}, {{join_through.camelcase.id}}, Pk) do - Cql::Relations::ManyCollection({{type.id}}, {{join_through.camelcase.id}}, Pk).new( - key: :{{T.name.underscore.id}}_id, + getter {{name.id}} : Cql::Relations::ManyCollection({{klass.id}}, {{join_through.camelcase.id}}, Pk) do + Cql::Relations::ManyCollection({{klass.id}}, {{join_through.camelcase.id}}, Pk).new( + key: :{{@type.name.underscore.id}}_id, id: @id.not_nil!, - target_key: :{{type.stringify.underscore.id}}_id, + target_key: :{{klass.stringify.underscore.id}}_id, cascade: {{cascade.id}}, - query: {{type.id}}.query - .inner(:{{join_through.id}}) { ({{join_through.id}}.{{type.stringify.underscore.id}}_id == {{T.id}}.schema.{{name.id}}.expression.id)} - .where{({{join_through.id}}.{{T.name.underscore.id}}_id == @id.not_nil!)} + query: {{klass.id}}.query + .inner(:{{join_through.id}}) { ({{join_through.id}}.{{klass.stringify.underscore.id}}_id == {{@type.id}}.schema.{{name.id}}.expression.id)} + .where{({{join_through.id}}.{{@type.name.underscore.id}}_id == @id.not_nil!)} ) end end diff --git a/src/schema.cr b/src/schema.cr index 41d47c1..e652198 100644 --- a/src/schema.cr +++ b/src/schema.cr @@ -90,6 +90,21 @@ module Cql @gen = Expression::Generator.new(@adapter) end + # Builds the schema. This method creates the tables in the schema. + # + # **Example** + # + # ``` + # schema.build + # ``` + def build + @tables.each do |name, table| + sql = table.create_sql + Log.debug { sql } + exec(sql) + end + end + # Executes a SQL statement. # # - **@param** sql [String] the SQL statement to execute