Skip to content
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
4 changes: 2 additions & 2 deletions shard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ dependencies:
development_dependencies:
mysql:
github: crystal-lang/crystal-mysql
version: ~> 0.4.0
version: ~> 0.5.0

sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.9.0
version: ~> 0.10.0

pg:
github: will/crystal-pg
Expand Down
30 changes: 10 additions & 20 deletions spec/granite/fields/timestamps_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@ require "../../spec_helper"
# Can run this spec for sqlite after https://www.sqlite.org/draft/releaselog/3_24_0.html is released.
{% for adapter in ["pg", "mysql"] %}
module {{adapter.capitalize.id}}
{%
avoid_macro_bug = 1 # https://github.com/crystal-lang/crystal/issues/5724

if adapter == "pg"
time_kind_on_read = "Time::Location::UTC".id
else
time_kind_on_read = "Time::Location.local".id
end
%}

describe "{{ adapter.id }} timestamps" do
it "consistently uses UTC for created_at" do
parent = Parent.new(name: "parent").tap(&.save)
Expand All @@ -22,7 +12,7 @@ module {{adapter.capitalize.id}}
read_timestamp = found_parent.created_at!

original_timestamp.location.should eq Time::Location::UTC
read_timestamp.location.should eq {{ time_kind_on_read }}
read_timestamp.location.should eq Time::Location::UTC
end

it "consistently uses UTC for updated_at" do
Expand All @@ -33,7 +23,7 @@ module {{adapter.capitalize.id}}
read_timestamp = found_parent.updated_at!

original_timestamp.location.should eq Time::Location::UTC
read_timestamp.location.should eq {{ time_kind_on_read }}
read_timestamp.location.should eq Time::Location::UTC
end

it "truncates the subsecond parts of created_at" do
Expand Down Expand Up @@ -73,8 +63,8 @@ module {{adapter.capitalize.id}}
parents.size.should eq 3

parents.each do |parent|
parent.updated_at.not_nil!.location.should eq {{ time_kind_on_read }}
parent.created_at.not_nil!.location.should eq {{ time_kind_on_read }}
parent.updated_at.not_nil!.location.should eq Time::Location::UTC
parent.created_at.not_nil!.location.should eq Time::Location::UTC
found_grandma.updated_at.not_nil!.epoch.should eq parent.updated_at.not_nil!.epoch
found_grandma.created_at.not_nil!.epoch.should eq parent.created_at.not_nil!.epoch
end
Expand All @@ -86,25 +76,25 @@ module {{adapter.capitalize.id}}
]

Parent.import(to_import)
import_time = Time.now
import_time = Time.utc_now.at_beginning_of_second

parent1 = Parent.find_by!(name: "ParentOne")
parent1.name.should eq "ParentOne"
parent1.created_at.not_nil!.epoch.should eq import_time.epoch
parent1.updated_at.not_nil!.epoch.should eq import_time.epoch
parent1.created_at!.should eq import_time
parent1.updated_at!.should eq import_time

to_update = Parent.all("WHERE name = ?", ["ParentOne"])
to_update.each { |parent| parent.name = "ParentOneEdited" }

sleep 1

Parent.import(to_update, update_on_duplicate: true, columns: ["name"])
update_time = Time.now
update_time = Time.utc_now.at_beginning_of_second

parent1_edited = Parent.find_by!(name: "ParentOneEdited")
parent1_edited.name.should eq "ParentOneEdited"
parent1_edited.created_at.not_nil!.epoch.should eq import_time.epoch
parent1_edited.updated_at.not_nil!.epoch.should eq update_time.epoch
parent1_edited.created_at!.should eq import_time
parent1_edited.updated_at!.should eq update_time
end
end
end
Expand Down
10 changes: 1 addition & 9 deletions spec/granite/querying/from_sql_spec.cr
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
require "../../spec_helper"

macro build_review_emitter(driver)
{%
timestamp = if driver == "sqlite"
"Time.now.to_s(Granite::DATETIME_FORMAT)".id
else
"Time.now".id
end
%}

FieldEmitter.new.tap do |e|
e._set_values(
[
Expand All @@ -19,7 +11,7 @@ macro build_review_emitter(driver)
nil, # sentiment
nil, # interest
true, # published
{{ timestamp }} # created_at
Time.now, # created_at
]
)
end
Expand Down
1 change: 1 addition & 0 deletions spec/granite/transactions/import_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module {{adapter.capitalize.id}}
describe "using the defualt primary key" do
context "with an AUTO INCREMENT PK" do
it "should import 3 new objects" do
Parent.clear
to_import = [
Parent.new(name: "ImportParent1"),
Parent.new(name: "ImportParent2"),
Expand Down
8 changes: 3 additions & 5 deletions src/adapter/mysql.cr
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ class Granite::Adapter::Mysql < Granite::Adapter::Base

def import(table_name : String, primary_name : String, auto : String, fields, model_array, **options)
params = [] of DB::Any
now = Time.utc_now

statement = String.build do |stmt|
stmt << "INSERT"
Expand All @@ -80,12 +79,11 @@ class Granite::Adapter::Mysql < Granite::Adapter::Base
stmt << ") VALUES "

model_array.each do |model|
model.updated_at = now if model.responds_to? :updated_at
model.created_at = now if model.responds_to? :created_at
model.set_timestamps
next unless model.valid?
stmt << '('
stmt << "("
stmt << Array.new(fields.size, '?').join(',')
params.concat fields.map { |field| model.to_h[field] }
params.concat fields.map { |field| model.read_attribute field }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

neat, i like that better.

stmt << "),"
end
end.chomp(',')
Expand Down
7 changes: 3 additions & 4 deletions src/adapter/pg.cr
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class Granite::Adapter::Pg < Granite::Adapter::Base

def import(table_name : String, primary_name : String, auto : String, fields, model_array, **options)
params = [] of DB::Any
now = Time.utc_now
now = Time.utc_now.at_beginning_of_second
# PG fails when inserting null into AUTO INCREMENT PK field.
# If AUTO INCREMENT is TRUE AND all model's pk are nil, remove PK from fields list for AUTO INCREMENT to work properly
fields.reject! { |field| field == primary_name } if model_array.all? { |m| m.to_h[primary_name].nil? } && auto == "true"
Expand All @@ -84,12 +84,11 @@ class Granite::Adapter::Pg < Granite::Adapter::Base
stmt << ") VALUES "

model_array.each do |model|
model.updated_at = now if model.responds_to? :updated_at
model.created_at = now if model.responds_to? :created_at
model.set_timestamps
next unless model.valid?
stmt << '('
stmt << fields.map_with_index { |_f, idx| "$#{index + idx + 1}" }.join(',')
params.concat fields.map { |field| model.to_h[field] }
params.concat fields.map { |field| model.read_attribute field }
stmt << "),"
index += fields.size
end
Expand Down
8 changes: 3 additions & 5 deletions src/adapter/sqlite.cr
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Granite::Adapter::Sqlite < Granite::Adapter::Base
end
end
end

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

def import(table_name : String, primary_name : String, auto : String, fields, model_array, **options)
params = [] of DB::Any
now = Time.now.to_utc

statement = String.build do |stmt|
stmt << "INSERT "
Expand All @@ -85,11 +84,10 @@ class Granite::Adapter::Sqlite < Granite::Adapter::Base

model_array.each do |model|
next unless model.valid?
model.updated_at = now if model.responds_to? :updated_at
model.created_at = now if model.responds_to? :created_at
model.set_timestamps
stmt << '('
stmt << Array.new(fields.size, '?').join(',')
params.concat fields.map { |field| model.to_h[field] }
params.concat fields.map { |field| model.read_attribute field }
stmt << "),"
end
end.chomp(',')
Expand Down
2 changes: 1 addition & 1 deletion src/granite/base.cr
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class Granite::Base
set_attributes(args.to_h)
end

def initialize(args : Hash(Symbol | String, String | JSON::Any))
def initialize(args : Hash(Symbol | String, DB::Any))
set_attributes(args)
end

Expand Down
26 changes: 17 additions & 9 deletions src/granite/fields.cr
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,7 @@ module Granite::Fields
def content_values
parsed_params = [] of DB::Any
{% for name, options in CONTENT_FIELDS %}
{% if options[:type].id == Time.id %}
parsed_params << {{name.id}}.try(&.to_s(Granite::DATETIME_FORMAT))
{% else %}
parsed_params << {{name.id}}
{% end %}
parsed_params << {{name.id}}
{% end %}
return parsed_params
end
Expand Down Expand Up @@ -101,6 +97,18 @@ module Granite::Fields
end
end

def read_attribute(attribute_name : Symbol | String) : DB::Any
{% begin %}
case attribute_name.to_s
{% for name, options in FIELDS %}
when "{{ name }}" then @{{ name.id }}
{% end %}
else
raise "Cannot read attribute #{attribute_name}, invalid attribute"
end
{% end %}
end

def set_attributes(args : Hash(String | Symbol, Type))
args.each do |k, v|
cast_to_field(k, v.as(Type))
Expand Down Expand Up @@ -142,10 +150,10 @@ module Granite::Fields
@{{_name.id}} = value.is_a?(JSON::Any) ? value.as_bool : ["1", "yes", "true", true].includes?(value)
{% elsif type.id == Time.id %}
if value.is_a?(Time)
@{{_name.id}} = value
elsif value.to_s =~ TIME_FORMAT_REGEX
@{{_name.id}} = Time.parse(value.to_s, Granite::DATETIME_FORMAT)
end
@{{_name.id}} = value
elsif value.to_s =~ TIME_FORMAT_REGEX
@{{_name.id}} = Time.parse(value.to_s, Granite::DATETIME_FORMAT)
end
{% else %}
@{{_name.id}} = value.is_a?(JSON::Any) ? value.as_s : value.to_s
{% end %}
Expand Down
13 changes: 1 addition & 12 deletions src/granite/querying.cr
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,7 @@ module Granite::Querying
# Loading from DB means existing records.
@new_record = false
\{% for name, options in FIELDS %}
\{% type = options[:type] %}
\{% if type.id.stringify == "Time" %}
if @@adapter.class.name == "Granite::Adapter::Sqlite"
# sqlite3 does not have timestamp type - timestamps are stored as str
# will break for null timestamps
self.\{{name.id}} = Time.parse(result.read(String), Granite::DATETIME_FORMAT)
else
self.\{{name.id}} = result.read(Union(\{{type.id}} | Nil))
end
\{% else %}
self.\{{name.id}} = result.read(Union(\{{type.id}} | Nil))
\{% end %}
self.\{{name.id}} = result.read(Union(\{{options[:type].id}} | Nil))
\{% end %}
self
end
Expand Down
12 changes: 10 additions & 2 deletions src/granite/transactions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,16 @@ module Granite::Transactions
end
end

def set_timestamps(*, to time = Time.now, mode = :create)
if mode == :create
@created_at = time.to_utc.at_beginning_of_second
end

@updated_at = time.to_utc.at_beginning_of_second
end

private def __create
@created_at = @updated_at = Time.now.to_utc
set_timestamps
fields = self.class.content_fields.dup
params = content_values
if value = @{{primary_name}}
Expand Down Expand Up @@ -99,7 +107,7 @@ module Granite::Transactions
end

private def __update
@updated_at = Time.now.to_utc
set_timestamps mode: :update
fields = self.class.content_fields
params = content_values + [@{{primary_name}}]

Expand Down