Skip to content

Commit

Permalink
primary auto: false works as a natural key
Browse files Browse the repository at this point in the history
  • Loading branch information
maiha committed Mar 27, 2018
1 parent d020b3f commit 7abb58a
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 10 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ end

This will override the default primary key of `id : Int64`.

### Natural Key

For natural keys, you can set `auto: false` option to disable auto increment insert.

```crystal
class Site < Granite::ORM::Base
adapter mysql
primary code : String, auto: false
field name : String
end
```

### SQL

To clear all the rows in the database:
Expand Down
23 changes: 23 additions & 0 deletions spec/granite_orm/fields/primary_key_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% for adapter in GraniteExample::ADAPTERS %}
module {{adapter.capitalize.id}}
describe "{{ adapter.id }} .new" do
it "works when the primary is defined as `auto: true`" do
Parent.new
end

it "works when the primary is defined as `auto: false`" do
Kvs.new
end
end

describe "{{ adapter.id }} .new(primary_key: value)" do
it "ignores the value in default" do
Parent.new(id: 1).id?.should eq(nil)
end

it "sets the value when the primary is defined as `auto: false`" do
Kvs.new(k: "foo").k?.should eq("foo")
end
end
end
{% end %}
81 changes: 81 additions & 0 deletions spec/granite_orm/transactions/save_natural_key_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
require "../../spec_helper"

{% for adapter in GraniteExample::ADAPTERS %}
module {{adapter.capitalize.id}}
describe "(Natural Key){{ adapter.id }} #save" do
it "fails when a primary key is not set" do
kv = Kvs.new
kv.save.should be_false
kv.errors.first.message.should eq "Primary key('k') cannot be null"
end

it "creates a new object when a primary key is given" do
kv = Kvs.new
kv.k = "foo"
kv.save.should be_true

kv = Kvs.find("foo").not_nil!
kv.k.should eq("foo")
end

it "updates an existing object" do
kv = Kvs.new
kv.k = "foo"
kv.v = "1"
kv.save.should be_true

kv.v = "2"
kv.save.should be_true
kv.k.should eq("foo")
kv.v.should eq("2")
end
end

describe "(Natural Key){{ adapter.id }} usecases" do
it "CRUD" do
Kvs.clear

## Create
port = Kvs.new(k: "mysql_port", v: "3306")
port.new_record?.should be_true
port.save.should be_true
port.v.should eq("3306")
Kvs.count.should eq(1)

## Read
port = Kvs.find("mysql_port")
port.v.should eq("3306")
port.new_record?.should be_false

## Update
port.v = "3307"
port.new_record?.should be_false
port.save.should be_true
port.v.should eq("3307")
Kvs.count.should eq(1)

## Delete
port.destroy.should be_true
Kvs.count.should eq(0)
end

it "creates a new record twice" do
Kvs.clear

# create a new record
port = Kvs.new(k: "mysql_port", v: "3306")
port.new_record?.should be_true
port.save.should be_true
port.v.should eq("3306")
Kvs.count.should eq(1)

# create a new record again
port = Kvs.new(k: "mysql_port", v: "3306")
port.new_record?.should be_true
port.save.should be_true
port.v.should eq("3306")
Kvs.count.should eq(2)
end
end
end
{% end %}
18 changes: 18 additions & 0 deletions spec/spec_models.cr
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,23 @@ end
end
end

class Kvs < Granite::ORM::Base
adapter {{ adapter_literal }}
table_name kvss
primary k : String, auto: false
field v : String

def self.drop_and_create
exec "DROP TABLE IF EXISTS #{ quoted_table_name }"
exec <<-SQL
CREATE TABLE #{ quoted_table_name } (
k VARCHAR(255),
v VARCHAR(255)
)
SQL
end
end

Parent.drop_and_create
Teacher.drop_and_create
Student.drop_and_create
Expand All @@ -272,5 +289,6 @@ end
Empty.drop_and_create
ReservedWord.drop_and_create
Callback.drop_and_create
Kvs.drop_and_create
end
{% end %}
2 changes: 1 addition & 1 deletion src/adapter/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ abstract class Granite::Adapter::Base
# abstract def select_one(table_name, fields, field, id, &block)

# This will insert a row in the database and return the id generated.
abstract def insert(table_name, fields, params) : Int64
abstract def insert(table_name, fields, params, lastval) : Int64

# This will update a row in the database.
abstract def update(table_name, primary_name, fields, params)
Expand Down
8 changes: 6 additions & 2 deletions src/adapter/mysql.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Granite::Adapter::Mysql < Granite::Adapter::Base
end
end

def insert(table_name, fields, params)
def insert(table_name, fields, params, lastval)
statement = String.build do |stmt|
stmt << "INSERT INTO #{quote(table_name)} ("
stmt << fields.map { |name| "#{quote(name)}" }.join(", ")
Expand All @@ -68,7 +68,11 @@ class Granite::Adapter::Mysql < Granite::Adapter::Base

open do |db|
db.exec statement, params
return db.scalar(last_val()).as(Int64)
if lastval
return db.scalar(last_val()).as(Int64)
else
return -1_i64
end
end
end

Expand Down
8 changes: 6 additions & 2 deletions src/adapter/pg.cr
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base
end
end

def insert(table_name, fields, params)
def insert(table_name, fields, params, lastval)
statement = String.build do |stmt|
stmt << "INSERT INTO #{quote(table_name)} ("
stmt << fields.map { |name| "#{quote(name)}" }.join(", ")
Expand All @@ -68,7 +68,11 @@ class Granite::Adapter::Pg < Granite::Adapter::Base

open do |db|
db.exec statement, params
return db.scalar(last_val()).as(Int64)
if lastval
return db.scalar(last_val()).as(Int64)
else
return -1_i64
end
end
end

Expand Down
8 changes: 6 additions & 2 deletions src/adapter/sqlite.cr
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Granite::Adapter::Sqlite < Granite::Adapter::Base
end
end

def insert(table_name, fields, params)
def insert(table_name, fields, params, lastval)
statement = String.build do |stmt|
stmt << "INSERT INTO #{quote(table_name)} ("
stmt << fields.map { |name| "#{quote(name)}" }.join(", ")
Expand All @@ -66,7 +66,11 @@ class Granite::Adapter::Sqlite < Granite::Adapter::Base

open do |db|
db.exec statement, params
return db.scalar(last_val()).as(Int64)
if lastval
return db.scalar(last_val()).as(Int64)
else
return -1_i64
end
end
end

Expand Down
5 changes: 5 additions & 0 deletions src/granite_orm/fields.cr
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ module Granite::ORM::Fields
private def cast_to_field(name, value : Type)
{% unless FIELDS.empty? %}
case name.to_s
when "{{PRIMARY[:name]}}"
{% if !PRIMARY[:auto] %}
@{{PRIMARY[:name]}} = value.as({{PRIMARY[:type]}})
{% end %}

{% for _name, type in FIELDS %}
when "{{_name.id}}"
return @{{_name.id}} = nil if value.nil?
Expand Down
15 changes: 14 additions & 1 deletion src/granite_orm/table.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Granite::ORM::Table
macro included
macro inherited
SETTINGS = {} of Nil => Nil
PRIMARY = {name: id, type: Int64}
PRIMARY = {name: id, type: Int64, auto: true}
end
end

Expand All @@ -27,14 +27,23 @@ module Granite::ORM::Table
{% PRIMARY[:type] = decl.type %}
end

# specify the primary key column and type and auto_increment
macro primary(decl, auto)
{% PRIMARY[:name] = decl.var %}
{% PRIMARY[:type] = decl.type %}
{% PRIMARY[:auto] = auto %}
end

macro __process_table
{% name_space = @type.name.gsub(/::/, "_").underscore.id %}
{% table_name = SETTINGS[:table_name] || name_space + "s" %}
{% primary_name = PRIMARY[:name] %}
{% primary_type = PRIMARY[:type] %}
{% primary_auto = PRIMARY[:auto] %}

@@table_name = "{{table_name}}"
@@primary_name = "{{primary_name}}"
@@primary_auto = "{{primary_auto}}"

property? {{primary_name}} : Union({{primary_type.id}} | Nil)

Expand All @@ -51,6 +60,10 @@ module Granite::ORM::Table
@@primary_name
end

def self.primary_auto
@@primary_auto
end

def self.quoted_table_name
@@adapter.quote(table_name)
end
Expand Down
17 changes: 15 additions & 2 deletions src/granite_orm/transactions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Granite::ORM::Transactions
macro __process_transactions
{% primary_name = PRIMARY[:name] %}
{% primary_type = PRIMARY[:type] %}
{% primary_auto = PRIMARY[:auto] %}

@updated_at : Time?
@created_at : Time?
Expand Down Expand Up @@ -38,10 +39,22 @@ module Granite::ORM::Transactions
end
begin
{% if primary_type.id == "Int32" %}
@{{primary_name}} = @@adapter.insert(@@table_name, fields, params).to_i32
@{{primary_name}} = @@adapter.insert(@@table_name, fields, params, lastval: true).to_i32
{% elsif primary_type.id == "Int64" %}
@{{primary_name}} = @@adapter.insert(@@table_name, fields, params, lastval: true)
{% elsif primary_auto == true %}
{% raise "Failed to define #{@type.name}#save: Primary key must be Int(32|64), or set `auto: false` for natural keys.\n\n primary #{primary_name} : #{primary_type}, auto: false\n" %}
{% else %}
@{{primary_name}} = @@adapter.insert(@@table_name, fields, params)
if @{{primary_name}}
@@adapter.insert(@@table_name, fields, params, lastval: false)
else
message = "Primary key('{{primary_name}}') cannot be null"
errors << Granite::ORM::Error.new("{{primary_name}}", message)
raise DB::Error.new
end
{% end %}
rescue err : DB::Error
raise err
rescue err
raise DB::Error.new(err.message)
end
Expand Down

0 comments on commit 7abb58a

Please sign in to comment.