Skip to content

Commit

Permalink
Merge pull request #363 from imdrasil/feature/add-big-decimal-support
Browse files Browse the repository at this point in the history
Add BigDecimal support
  • Loading branch information
imdrasil authored Mar 22, 2021
2 parents 94d3971 + 39e2506 commit 2224a3f
Show file tree
Hide file tree
Showing 30 changed files with 291 additions and 81 deletions.
9 changes: 5 additions & 4 deletions docs/model_mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,15 +204,16 @@ end

To define a field converter create a class/module which implements next static methods:

- `.from_db(DB::ResultSet, Bool)` - converts field reading it from db result set (second argument describes wether field is nillable);
- `.to_db(T)` - converts field to the db format;
- `.from_hash(Hash(String, Jennifer::DBAny | T), String)` - converts field (which name is the 2nd argument) from the given hash (this method is called only if hash has required key).
- `.from_db(DB::ResultSet, NamedTuple)` - converts field reading it from db result set;
- `.to_db(T, NamedTuple)` - converts field to the db format;
- `.from_hash(Hash(String, Jennifer::DBAny | T), String, NamedTuple)` - converts field (which name is the 2nd argument) from the given hash (this method is called only if hash has required key).

There are 6 predefined converters:
There are 7 predefined converters:

- `Jennifer::Model::JSONConverter` - default converter for `JSON::Any` (it is applied automatically for `JSON::Any` fields) - takes care of JSON-string-JSON conversion;
- `Jennifer::Model::TimeZoneConverter` - default converter for `Time` - converts from UTC time to local time zone;
- `Jennifer::Model::EnumConverter` - converts string values to crystal `enum`;
-`Jennifer::Model::BigDecimalConverter` - converts numeric database type to `BigDecimal` value which allows to perform operations with specific scale;
- `Jennifer::Model::JSONSerializableConverter(T)` - converts JSON to `T` (which includes `JSON::Serializable);
- `Jennifer::Model::NumericToFloat64Converter` - converts `PG::Numeric` to `Float64` (Postgres only);
- `Jennifer::Model::PgEnumConverter` - converts `ENUM` value to `String` (Postgres only).
Expand Down
4 changes: 2 additions & 2 deletions scripts/migrations/20170119011451314_create_contacts.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class CreateContacts < Jennifer::Migration::Base
t.string :name, {:size => 30}
t.integer :age
t.integer :tags, {:array => true}
t.decimal :ballance
t.decimal :ballance, {:precision => 6, :scale => 2}
t.field :gender, :gender_enum
t.timestamps true
end
Expand All @@ -16,7 +16,7 @@ class CreateContacts < Jennifer::Migration::Base
create_table(:contacts) do |t|
t.string :name, {:size => 30}
t.integer :age
t.decimal :ballance
t.decimal :ballance, { :precision => 6, :scale => 2 }
t.enum :gender, ["male", "female"], {:default => "male"}
t.timestamps true
end
Expand Down
4 changes: 4 additions & 0 deletions spec/factories.cr
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ class ContactFactory < Factory::Jennifer::Base
argument_type (Array(Int32) | Int32 | PG::Numeric | String?)
end

mysql_only do
argument_type (Array(Int32) | Int32 | Float64 | String?)
end

attr :name, "Deepthi"
attr :age, 28
attr :description, nil
Expand Down
54 changes: 54 additions & 0 deletions spec/model/big_decimal_converter_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require "../spec_helper"

describe Jennifer::Model::BigDecimalConverter do
described_class = Jennifer::Model::BigDecimalConverter(PG::Numeric)

describe "#from_db" do
postgres_only do
it "reads numeric from result set" do
ballance = PG::Numeric.new(2i16, 0i16, 0i16, 3i16, [1234i16, 6800i16])
executed = false
Factory.create_contact(ballance: ballance)
Contact.all.select([Contact._ballance]).each_result_set do |rs|
described_class.from_db(rs, {name: "ballance", scale: 2, null: false}).should eq(BigDecimal.new(123468, 2))
executed = true
end
executed.should be_true
end
end

it "reads nil from result set" do
executed = false
Factory.create_contact(ballance: nil)
Contact.all.select([Contact._ballance]).each_result_set do |rs|
described_class.from_db(rs, {name: "ballance", scale: 2, null: true}).should be_nil
executed = true
end
executed.should be_true
end
end

describe "#to_db" do
postgres_only do
it "writes value to a result set" do
balance = described_class.to_db(BigDecimal.new(123468, 2), {name: "ballance", scale: 2})
Query["contacts"].insert(%w(name age ballance), [["test", 1, balance]])
Contact.all.last!.ballance.should eq(PG::Numeric.new(2i16, 0i16, 0i16, 2i16, [1234i16, 6800i16]))
end
end
end

describe "#from_hash" do
postgres_only do
it "converts numeric" do
balance = PG::Numeric.new(2i16, 0i16, 0i16, 2i16, [1234i16, 6800i16])
described_class.from_hash({ "ballance" => balance }, "ballance", {name: "ballance", scale: 2})
.should eq(BigDecimal.new(123468, 2))
end
end

it "accepts nil value" do
described_class.from_hash({ "ballance" => nil }, "ballance", {name: "ballance", scale: 2}).should be_nil
end
end
end
9 changes: 9 additions & 0 deletions spec/model/coercer_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
require "../spec_helper"

describe Jennifer::Model::Coercer do
described_class = Jennifer::Model::Coercer

describe BigDecimal do
it { described_class.coerce("123.12", BigDecimal?).should eq(BigDecimal.new(12312, 2)) }
end
end
2 changes: 1 addition & 1 deletion spec/model/enum_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe Jennifer::Model::EnumConverter do

describe ".from_hash" do
it "accepts string value" do
Jennifer::Model::EnumConverter(Category).from_hash({ "value" => "GOOD" }, "value").should eq(Category::GOOD)
Jennifer::Model::EnumConverter(Category).from_hash({ "value" => "GOOD" }, "value", {name: "value"}).should eq(Category::GOOD)
end
end
end
2 changes: 1 addition & 1 deletion spec/model/json_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe Jennifer::Model::JSONConverter do
describe ".from_hash" do
it "accepts string value" do
data = {latitude: 32.0, longitude: 24.5}
Jennifer::Model::JSONConverter.from_hash({ "value" => data.to_json }, "value")
Jennifer::Model::JSONConverter.from_hash({ "value" => data.to_json }, "value", { name: "value" })
.should eq(JSON.parse(data.to_json))
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/model/json_serializable_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ describe Jennifer::Model::JSONSerializableConverter do
describe ".from_hash" do
it "accepts string value" do
data = {latitude: 32.0, longitude: 24.5}
Jennifer::Model::JSONSerializableConverter(Location).from_hash({ "value" => data.to_json }, "value")
Jennifer::Model::JSONSerializableConverter(Location).from_hash({ "value" => data.to_json }, "value", {name: "value"})
.should eq(Location.new(32.0, 24.5))
end

it "accepts JSON::Any value" do
data = {latitude: 32.0, longitude: 24.5}
Jennifer::Model::JSONSerializableConverter(Location).from_hash({ "value" => JSON.parse(data.to_json) }, "value")
Jennifer::Model::JSONSerializableConverter(Location).from_hash({ "value" => JSON.parse(data.to_json) }, "value", {name: "value"})
.should eq(Location.new(32.0, 24.5))
end
end
Expand Down
54 changes: 46 additions & 8 deletions spec/model/mapping_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ postgres_only do
tags: Array(Int32)
})
end

class PgContactWithBigDecimal < ApplicationRecord
table_name "contacts"

mapping({
id: Primary32,
name: String,
ballance: { type: BigDecimal, converter: Jennifer::Model::BigDecimalConverter(PG::Numeric), scale: 2 }
}, false)
end
end

mysql_only do
class PgContactWithBigDecimal < ApplicationRecord
table_name "contacts"

mapping({
id: Primary32,
name: String,
ballance: { type: BigDecimal, converter: Jennifer::Model::BigDecimalConverter(Float64), scale: 2 }
}, false)
end
end

private module Mapping11
Expand Down Expand Up @@ -94,22 +116,22 @@ describe Jennifer::Model::Mapping do
describe "with symbol argument" do
it do
Factory.build_contact.attribute_metadata(:id)
.should eq({type: Int32, primary: true, parsed_type: "Int32?", column: "id", auto: true})
.should eq({type: Int32, primary: true, parsed_type: "Int32?", column: "id", auto: true, null: false})
Factory.build_contact.attribute_metadata(:name)
.should eq({type: String, parsed_type: "String", column: "name"})
.should eq({type: String, parsed_type: "String", column: "name", null: false})
Factory.build_address.attribute_metadata(:street)
.should eq({type: String, parsed_type: "String", column: "street"})
.should eq({type: String, parsed_type: "String", column: "street", null: false})
end
end

describe "with string argument" do
it do
Factory.build_contact.attribute_metadata("id")
.should eq({type: Int32, primary: true, parsed_type: "Int32?", column: "id", auto: true})
.should eq({type: Int32, primary: true, parsed_type: "Int32?", column: "id", auto: true, null: false})
Factory.build_contact.attribute_metadata("name")
.should eq({type: String, parsed_type: "String", column: "name"})
.should eq({type: String, parsed_type: "String", column: "name", null: false})
Factory.build_address.attribute_metadata("street")
.should eq({type: String, parsed_type: "String", column: "street"})
.should eq({type: String, parsed_type: "String", column: "street", null: false})
end
end
end
Expand Down Expand Up @@ -139,6 +161,22 @@ describe Jennifer::Model::Mapping do
contact_with_float.ballance.should eq(1.0f64)
contact_with_float.ballance.is_a?(Float64).should be_true
end

it "correctly transform data to bigdecimal" do
Factory.create_contact(ballance: PG::Numeric.new(2i16, 0i16, 0i16, 2i16, [1234i16, 6800i16])).id
record = PgContactWithBigDecimal.all.last!
record.ballance.should eq(BigDecimal.new(123468, 2))
end
end
end

mysql_only do
describe "numeric" do
it "correctly transform data to bigdecimal" do
Factory.create_contact(ballance: 1234.68f64)
record = PgContactWithBigDecimal.all.last!
record.ballance.should eq(BigDecimal.new(123468, 2))
end
end
end
end
Expand Down Expand Up @@ -384,8 +422,8 @@ describe Jennifer::Model::Mapping do

describe "user-defined mapping types" do
it "is accessible if defined in parent class" do
User::COLUMNS_METADATA[:password_digest].should eq({type: String, column: "password_digest", default: "", parsed_type: "String"})
User::COLUMNS_METADATA[:email].should eq({type: String, column: "email", default: "", parsed_type: "String"})
User::COLUMNS_METADATA[:password_digest].should eq({type: String, column: "password_digest", default: "", parsed_type: "String", null: false})
User::COLUMNS_METADATA[:email].should eq({type: String, column: "email", default: "", parsed_type: "String", null: false})
end

pending "allows to add extra options" do
Expand Down
2 changes: 1 addition & 1 deletion spec/model/numeric_to_float64_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ postgres_only do
describe ".from_hash" do
it "accepts PG::Numeric" do
value = PG::Numeric.new(1i16, 0i16, 0i16, 0i16, [3i16])
Jennifer::Model::NumericToFloat64Converter.from_hash({ "value" => value }, "value").should eq(3.0)
Jennifer::Model::NumericToFloat64Converter.from_hash({ "value" => value }, "value", {name: "value"}).should eq(3.0)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/model/pg_enum_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ postgres_only do

describe ".from_hash" do
it "accepts bytes value" do
Jennifer::Model::PgEnumConverter.from_hash({ "value" => "female".to_slice }, "value").should eq("female")
Jennifer::Model::PgEnumConverter.from_hash({ "value" => "female".to_slice }, "value", {name: "value"}).should eq("female")
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/model/time_zone_converter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe Jennifer::Model::TimeZoneConverter do
describe ".from_hash" do
it "accepts time which is already in current time zone" do
time = Time.local(location: Jennifer::Config.local_time_zone)
value = Jennifer::Model::TimeZoneConverter.from_hash({ "value" => time }, "value")
value = Jennifer::Model::TimeZoneConverter.from_hash({ "value" => time }, "value", { name: "value" })
value.should eq(time)
value.zone.should eq(Jennifer::Config.local_time_zone.lookup(Time.utc))
end
Expand Down
10 changes: 5 additions & 5 deletions spec/models.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ class EnnValidator < Jennifer::Validations::Validator
end

class InspectConverter(T)
def self.from_db(pull, nullable)
value = nillable ? pull.read(T?) : pull.read(T)
def self.from_db(pull, options)
value = options[:null] ? pull.read(T?) : pull.read(T)
"#{T}: #{value}" if value
end

def self.to_db(value : String)
def self.to_db(value : String, options)
if T == Int32
value[("#{T}".size + 2)..-1].to_i
else
value[("#{T}".size + 2)..-1]
end
end

def self.to_db(value : Nil); end
def self.to_db(value : Nil, options); end

def self.from_hash(hash : Hash, column)
def self.from_hash(hash : Hash, column, options)
value = hash[column]
"#{T}: #{value}"
end
Expand Down
31 changes: 29 additions & 2 deletions src/jennifer/adapter/mysql/schema_processor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ module Jennifer
end

private def column_definition(name, options, io) # ameba:disable Metrics/CyclomaticComplexity
type = options[:serial]? ? "serial" : (options[:sql_type]? || adapter.translate_type(options[:type].as(Symbol)))
size = options[:size]? || adapter.default_type_size(options[:type])
io << name << " " << type
io << name << " " << column_type(options)
io << "(#{size})" if size
if options[:type] == :enum
io << " ("
Expand All @@ -44,6 +43,34 @@ module Jennifer
io << " DEFAULT #{adapter_class.t(options[:default])}" if options.has_key?(:default)
io << " AUTO_INCREMENT" if options.has_key?(:auto_increment) && options[:auto_increment]
end

def column_type(options : Hash)
if options[:serial]?
"serial"
elsif options.has_key?(:sql_type)
options[:sql_type]
else
type = options[:type]
if type == :decimal
scale_opts = [] of Int32
if options.has_key?(:precision)
scale_opts << options[:precision].as(Int32)
scale_opts << options[:scale].as(Int32) if options.has_key?(:scale)
end

String.build do |io|
io << "numeric"
next if scale_opts.empty?

io << '('
scale_opts.join(io, ',')
io << ')'
end
else
adapter.translate_type(type.as(Symbol))
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ module Jennifer::Model
# end
# ```
class NumericToFloat64Converter
def self.from_db(pull, nillable)
if nillable
def self.from_db(pull, options)
if options[:null]
pull.read(PG::Numeric?).try(&.to_f64)
else
pull.read(PG::Numeric).to_f64
end
end

def self.to_db(value : Float?)
def self.to_db(value : Float?, options)
value
end

def self.from_hash(hash : Hash, column)
def self.from_hash(hash : Hash, column, options)
value = hash[column]
case value
when PG::Numeric
Expand Down
10 changes: 5 additions & 5 deletions src/jennifer/adapter/postgres/model/pg_enum_converter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ module Jennifer::Model
# end
# ```
class PgEnumConverter
def self.from_db(pull, nillable)
if nillable
def self.from_db(pull, options)
if options[:null]
value = pull.read(Bytes?)
value && String.new(value)
else
String.new(pull.read(Bytes))
end
end

def self.to_db(value : String)
def self.to_db(value : String, options)
value.to_slice
end

def self.to_db(value : Nil) : Nil
def self.to_db(value : Nil, options) : Nil
end

def self.from_hash(hash : Hash, column)
def self.from_hash(hash : Hash, column, options)
value = hash[column]
case value
when Bytes
Expand Down
Loading

0 comments on commit 2224a3f

Please sign in to comment.