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

Supporting natural keys #166

Merged
merged 1 commit into from
Mar 28, 2018
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
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
24 changes: 24 additions & 0 deletions spec/granite_orm/fields/primary_key_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{% 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")
Kvs.new(k: "foo", v: "v").k?.should eq("foo")
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Blacksmoke16
Yep, I just added new with mass-assignment yesterday into this PR!
maiha:master + new(*mass-assignment*)maiha:natural-key

If you are using maiha:master branch, could you try again with maiha:natural-key or this pr branch? This should work!

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah perfect! Was behind a few commits i guess :)

Copy link
Member

Choose a reason for hiding this comment

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

👍

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
28 changes: 26 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 Expand Up @@ -100,4 +113,15 @@ module Granite::ORM::Transactions
def persisted?
!(new_record? || destroyed?)
end

# Returns true if this object hasn't been saved yet.
getter? new_record : Bool = true

# Returns true if this object has been destroyed.
getter? destroyed : Bool = false

# Returns true if the record is persisted.
def persisted?
!(new_record? || destroyed?)
end
end