From a88fdbc9adb74bf62078190fd92f1cdfe5791a8d Mon Sep 17 00:00:00 2001 From: Alberto Restifo Date: Mon, 11 Mar 2024 15:49:10 +0100 Subject: [PATCH] Support String primary key (#1000) * Recover work * Add type-checking for better compile error * Fix compile errors * Allow passing a call * Do not use a second argument, use the value argument * Fix examples --- .../20230128124355_create_discount.cr | 15 +++++++++ spec/avram/model_spec.cr | 20 +++++++++++ spec/support/factories/discount_factory.cr | 6 ++++ spec/support/models/discount.cr | 14 ++++++++ spec/support/models/line_item.cr | 1 + .../primary_keys/string_primary_key.cr | 16 +++++++++ src/avram/model.cr | 33 +++++++++++++++++++ src/avram/primary_key_methods.cr | 2 +- src/avram/primary_key_type.cr | 1 + src/avram/save_operation.cr | 4 +++ 10 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 db/migrations/20230128124355_create_discount.cr create mode 100644 spec/support/factories/discount_factory.cr create mode 100644 spec/support/models/discount.cr create mode 100644 src/avram/migrator/columns/primary_keys/string_primary_key.cr diff --git a/db/migrations/20230128124355_create_discount.cr b/db/migrations/20230128124355_create_discount.cr new file mode 100644 index 000000000..72d79b829 --- /dev/null +++ b/db/migrations/20230128124355_create_discount.cr @@ -0,0 +1,15 @@ +class CreateDiscount::V20230128124355 < Avram::Migrator::Migration::V1 + def migrate + create :discounts do + primary_key id : String + add_timestamps + add description : String + add in_cents : Int32 + add_belongs_to line_item : LineItem, on_delete: :cascade, foreign_key_type: UUID + end + end + + def rollback + drop :discounts + end +end diff --git a/spec/avram/model_spec.cr b/spec/avram/model_spec.cr index 3668f0eca..04a858cc1 100644 --- a/spec/avram/model_spec.cr +++ b/spec/avram/model_spec.cr @@ -177,6 +177,26 @@ describe Avram::Model do end end + describe "models with custom string primary key" do + it "can be saved" do + item = LineItemFactory.create + DiscountFactory.create &.line_item_id(item.id) + + discount = DiscountQuery.new.first + discount.id.should be_a String + end + + it "can be deleted" do + item = LineItemFactory.create + DiscountFactory.create &.line_item_id(item.id) + + discount = DiscountQuery.new.first + discount.delete + + Discount::BaseQuery.all.size.should eq 0 + end + end + it "can infer the table name when omitted" do InferredTableNameModel.table_name.should eq("inferred_table_name_models") end diff --git a/spec/support/factories/discount_factory.cr b/spec/support/factories/discount_factory.cr new file mode 100644 index 000000000..f55ca7a92 --- /dev/null +++ b/spec/support/factories/discount_factory.cr @@ -0,0 +1,6 @@ +class DiscountFactory < BaseFactory + def initialize + description "Awesome discount" + in_cents 99 + end +end diff --git a/spec/support/models/discount.cr b/spec/support/models/discount.cr new file mode 100644 index 000000000..66fe16d96 --- /dev/null +++ b/spec/support/models/discount.cr @@ -0,0 +1,14 @@ +class Discount < BaseModel + skip_default_columns + + table do + primary_key id : String = Random::Secure.hex + timestamps + column description : String + column in_cents : Int32 + belongs_to line_item : LineItem + end +end + +class DiscountQuery < Discount::BaseQuery +end diff --git a/spec/support/models/line_item.cr b/spec/support/models/line_item.cr index ff79dbd56..49e53251a 100644 --- a/spec/support/models/line_item.cr +++ b/spec/support/models/line_item.cr @@ -6,6 +6,7 @@ class LineItem < BaseModel timestamps column name : String has_one price : Price? + has_one discount : Discount? has_many scans : Scan has_many line_items_products : LineItemProduct has_many associated_products : Product, through: [:line_items_products, :product] diff --git a/src/avram/migrator/columns/primary_keys/string_primary_key.cr b/src/avram/migrator/columns/primary_keys/string_primary_key.cr new file mode 100644 index 000000000..ab80f5682 --- /dev/null +++ b/src/avram/migrator/columns/primary_keys/string_primary_key.cr @@ -0,0 +1,16 @@ +require "./base" + +module Avram::Migrator::Columns::PrimaryKeys + class StringPrimaryKey < Base + def initialize(@name) + end + + def column_type : String + "text" + end + + def build : String + %( #{name} #{column_type} PRIMARY KEY) + end + end +end diff --git a/src/avram/model.cr b/src/avram/model.cr index 6733a5c1d..035a4e1a0 100644 --- a/src/avram/model.cr +++ b/src/avram/model.cr @@ -99,6 +99,39 @@ abstract class Avram::Model :{{ type_declaration.var.stringify }} end + {% if type_declaration.type.stringify == "String" %} + {% value_generator = type_declaration.value %} + + {% if !value_generator || value_generator && !(value_generator.is_a?(ProcLiteral) || value_generator.is_a?(ProcPointer) || value_generator.is_a?(Call)) %} + {% raise <<-ERROR + When using a String primary_key, you must also specify a way to generate the value. + You can provide a class method, a proc or a proc pointer. + Your value generator must return a non-nullable String. + + Example: + table do + primary_key id : String = Random::Secure.hex + ... + end + + Or with a proc: + table do + primary_key id : String = -> { Random::Secure.hex } + ... + end + ERROR + %} + {% end %} + + def self.primary_key_value_generator : String + {% if value_generator.is_a?(ProcLiteral) || value_generator.is_a?(ProcPointer) %} + {{value_generator}}.call + {% else %} + {{value_generator}} + {% end %} + end + {% end %} + include Avram::PrimaryKeyMethods # If not using default 'id' primary key diff --git a/src/avram/primary_key_methods.cr b/src/avram/primary_key_methods.cr index 962f502e6..41b59be5e 100644 --- a/src/avram/primary_key_methods.cr +++ b/src/avram/primary_key_methods.cr @@ -80,7 +80,7 @@ module Avram::PrimaryKeyMethods id end - private def escape_primary_key(id : UUID) + private def escape_primary_key(id : UUID | String) PG::EscapeHelper.escape_literal(id.to_s) end end diff --git a/src/avram/primary_key_type.cr b/src/avram/primary_key_type.cr index a34351dbe..5b737a412 100644 --- a/src/avram/primary_key_type.cr +++ b/src/avram/primary_key_type.cr @@ -1,4 +1,5 @@ enum Avram::PrimaryKeyType Serial UUID + String end diff --git a/src/avram/save_operation.cr b/src/avram/save_operation.cr index 2cf96423b..d30680abf 100644 --- a/src/avram/save_operation.cr +++ b/src/avram/save_operation.cr @@ -283,6 +283,10 @@ abstract class Avram::SaveOperation(T) def after_commit(_record : T); end private def insert : T + if (t = T).responds_to?(:primary_key_value_generator) + {{ T.constant(:PRIMARY_KEY_NAME).id }}.value = t.primary_key_value_generator + end + self.created_at.value ||= Time.utc if responds_to?(:created_at) self.updated_at.value ||= Time.utc if responds_to?(:updated_at) sql = insert_sql