Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support String primary key #1000

Merged
merged 6 commits into from
Mar 11, 2024
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
15 changes: 15 additions & 0 deletions db/migrations/20230128124355_create_discount.cr
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions spec/avram/model_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions spec/support/factories/discount_factory.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class DiscountFactory < BaseFactory
def initialize
description "Awesome discount"
in_cents 99
end
end
14 changes: 14 additions & 0 deletions spec/support/models/discount.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Discount < BaseModel
skip_default_columns

table do
primary_key id : String = Random::Secure.hex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

timestamps
column description : String
column in_cents : Int32
belongs_to line_item : LineItem
end
end

class DiscountQuery < Discount::BaseQuery
end
1 change: 1 addition & 0 deletions spec/support/models/line_item.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
16 changes: 16 additions & 0 deletions src/avram/migrator/columns/primary_keys/string_primary_key.cr
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions src/avram/model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/avram/primary_key_methods.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/avram/primary_key_type.cr
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
enum Avram::PrimaryKeyType
Serial
UUID
String
end
4 changes: 4 additions & 0 deletions src/avram/save_operation.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
albertorestifo marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down