Skip to content

Commit

Permalink
Support String primary key (#1000)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
albertorestifo authored Mar 11, 2024
1 parent 68cc0c6 commit a88fdbc
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 1 deletion.
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
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
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

0 comments on commit a88fdbc

Please sign in to comment.