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

JSON Support 2 #108

Merged
merged 9 commits into from
Jun 19, 2019
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/docs/
/lib/
/bin/
/bin/*
/db/migrations/
/.shards/

Expand Down
2 changes: 2 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ script:
- script/test
notifications:
email: false
addons:
postgresql: "9.4"
11 changes: 11 additions & 0 deletions db/migrations/20180802180356_create_blobs.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreateBlobs::V20180802180356 < Avram::Migrator::Migration::V1
def migrate
create :blobs do
add doc : JSON::Any?, default: JSON::Any.new({ "defa'ult" => JSON::Any.new("val'ue") })
end
end

def rollback
drop :blobs
end
end
65 changes: 65 additions & 0 deletions spec/json_column_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require "./spec_helper.cr"

class BlobQuery < Blob::BaseQuery
end

class SaveBlob < Blob::SaveOperation
permit_columns :doc
end

describe "JSON Columns" do
it "should work in boxes" do
BlobBox.create
blob = BlobQuery.new.first
blob.doc.should eq JSON::Any.new({"foo" => JSON::Any.new("bar")})

blob2 = BlobBox.new.doc(JSON::Any.new(42_i64)).create
blob2.doc.should eq JSON::Any.new(42_i64)
end

it "should be nullable" do
jwoertink marked this conversation as resolved.
Show resolved Hide resolved
blob = BlobBox.new.doc(nil).create
blob = BlobQuery.new.first
blob.doc.should eq nil
end

it "should convert scalars and save forms" do
form1 = SaveBlob.new
form1.set_doc_from_param(42)
form1.doc.value.should eq JSON::Any.new(42_i64)
form1.save!
blob1 = BlobQuery.new.last
blob1.doc.should eq JSON::Any.new(42_i64)

form2 = SaveBlob.new
form2.set_doc_from_param("hey")
form2.doc.value.should eq JSON::Any.new("hey")
form2.save!
blob2 = BlobQuery.new.last
blob2.doc.should eq JSON::Any.new("hey")
end

it "should convert hashes and arrays and save forms" do
form1 = SaveBlob.new
form1.set_doc_from_param(%w[a b c])
form1.doc.value.should eq %w[a b c].map { |v| JSON::Any.new(v) }
form1.save!
blob1 = BlobQuery.new.last
blob1.doc.should eq %w[a b c].map { |v| JSON::Any.new(v) }

form2 = SaveBlob.new
form2.set_doc_from_param({"foo" => {"bar" => "baz"}})
form2.doc.value.should eq JSON::Any.new({
"foo" => JSON::Any.new({
"bar" => JSON::Any.new("baz"),
}),
})
form2.save!
blob2 = BlobQuery.new.last
blob2.doc.should eq JSON::Any.new({
"foo" => JSON::Any.new({
"bar" => JSON::Any.new("baz"),
}),
})
end
end
2 changes: 2 additions & 0 deletions spec/migrator/alter_table_statement_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe Avram::Migrator::AlterTableStatement do
add num : Int64, default: 1, index: true
add amount_paid : Float, default: 1.0, precision: 10, scale: 5
add completed : Bool, default: false
add meta : JSON::Any, default: JSON::Any.new({"default" => JSON::Any.new("value")})
add joined_at : Time, default: :now
add updated_at : Time, fill_existing_with: :now
add future_time : Time, default: Time.local
Expand All @@ -28,6 +29,7 @@ describe Avram::Migrator::AlterTableStatement do
ADD num bigint NOT NULL DEFAULT 1,
ADD amount_paid decimal(10,5) NOT NULL DEFAULT 1.0,
ADD completed boolean NOT NULL DEFAULT false,
ADD meta jsonb NOT NULL DEFAULT '{"default":"value"}',
ADD joined_at timestamptz NOT NULL DEFAULT NOW(),
ADD updated_at timestamptz,
ADD future_time timestamptz NOT NULL DEFAULT '#{Time.local.to_utc}',
Expand Down
4 changes: 4 additions & 0 deletions spec/migrator/create_table_statement_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe Avram::Migrator::CreateTableStatement do
add joined_at : Time
add amount_paid : Float, precision: 10, scale: 2
add email : String?
add meta : JSON::Any?
add reference : UUID
end

Expand All @@ -37,6 +38,7 @@ describe Avram::Migrator::CreateTableStatement do
joined_at timestamptz NOT NULL,
amount_paid decimal(10,2) NOT NULL,
email text,
meta jsonb,
reference uuid NOT NULL);
SQL
end
Expand Down Expand Up @@ -64,6 +66,7 @@ describe Avram::Migrator::CreateTableStatement do
add num : Int64, default: 1
add amount_paid : Float, default: 1.0
add completed : Bool, default: false
add meta : JSON::Any, default: JSON::Any.new(Hash(String, JSON::Any).new)
add joined_at : Time, default: :now
add future_time : Time, default: Time.local
end
Expand All @@ -80,6 +83,7 @@ describe Avram::Migrator::CreateTableStatement do
num bigint NOT NULL DEFAULT 1,
amount_paid decimal NOT NULL DEFAULT 1.0,
completed boolean NOT NULL DEFAULT false,
meta jsonb NOT NULL DEFAULT '{}',
joined_at timestamptz NOT NULL DEFAULT NOW(),
future_time timestamptz NOT NULL DEFAULT '#{Time.local.to_utc}');
SQL
Expand Down
34 changes: 34 additions & 0 deletions spec/query_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ class ChainedQuery < User::BaseQuery
end
end

class JSONQuery < Blob::BaseQuery
def static_foo
doc(JSON::Any.new({"foo" => JSON::Any.new("bar")}))
end

def foo_with_value(value : String)
doc(JSON::Any.new({"foo" => JSON::Any.new(value)}))
end
end

describe Avram::Query do
it "can chain scope methods" do
ChainedQuery.new.young.named("Paul")
Expand Down Expand Up @@ -527,4 +537,28 @@ describe Avram::Query do
result.should eq post
end
end

context "when querying jsonb" do
describe "simple where query" do
it "returns 1 result" do
blob = BlobBox.new.doc(JSON::Any.new({"foo" => JSON::Any.new("bar")})).create

query = JSONQuery.new.static_foo
query.to_sql.should eq ["SELECT blobs.id, blobs.created_at, blobs.updated_at, blobs.doc FROM blobs WHERE blobs.doc = $1", "{\"foo\":\"bar\"}"]
result = query.first
result.should eq blob

query2 = JSONQuery.new.foo_with_value("bar")
query2.to_sql.should eq ["SELECT blobs.id, blobs.created_at, blobs.updated_at, blobs.doc FROM blobs WHERE blobs.doc = $1", "{\"foo\":\"bar\"}"]
result = query2.first
result.should eq blob

query3 = JSONQuery.new.foo_with_value("baz")
query3.to_sql.should eq ["SELECT blobs.id, blobs.created_at, blobs.updated_at, blobs.doc FROM blobs WHERE blobs.doc = $1", "{\"foo\":\"baz\"}"]
expect_raises(Avram::RecordNotFoundError) do
query3.first
end
end
end
end
end
5 changes: 5 additions & 0 deletions spec/support/blob.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Blob < Avram::Model
table blobs do
column doc : JSON::Any?
end
end
5 changes: 5 additions & 0 deletions spec/support/boxes/blob_box.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class BlobBox < BaseBox
def initialize
doc JSON::Any.new({"foo" => JSON::Any.new("bar")})
end
end
6 changes: 6 additions & 0 deletions src/avram/blank_extensions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ struct Nil
nil?
end
end

struct JSON::Any
def blank?
nil?
end
end
25 changes: 25 additions & 0 deletions src/avram/charms/json_extensions.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
struct JSON::Any
module Lucky
alias ColumnType = JSON::Any
include Avram::Type

def self.from_db!(value : JSON::Any)
value
end

def self.parse(value : JSON::Any)
SuccessfulCast(JSON::Any).new value
end

def self.parse(value)
SuccessfulCast(JSON::Any).new JSON.parse(value.to_json)
end

def self.to_db(value)
value.to_json
end

class Criteria(T, V) < Avram::Criteria(T, V)
end
end
end
6 changes: 5 additions & 1 deletion src/avram/migrator/column_default_helpers.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Avram::Migrator::ColumnDefaultHelpers
alias ColumnDefaultType = String | Time | Int32 | Int64 | Float32 | Float64 | Bool | Symbol | UUID
alias ColumnDefaultType = String | Time | Int32 | Int64 | Float32 | Float64 | Bool | Symbol | UUID | JSON::Any

def value_to_string(type : String.class | Time.class | UUID.class, value : String | Time | UUID)
"'#{value}'"
Expand Down Expand Up @@ -48,4 +48,8 @@ module Avram::Migrator::ColumnDefaultHelpers
def default_value(type : UUID.class, default : UUID)
" DEFAULT #{value_to_string(type, default)}"
end

def default_value(type : JSON::Any.class, default)
" DEFAULT '#{default.to_json.gsub(/'/, "''")}'"
end
end
6 changes: 5 additions & 1 deletion src/avram/migrator/column_type_option_helpers.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Avram::Migrator::ColumnTypeOptionHelpers
alias ColumnType = String.class | Time.class | Int32.class | Int64.class | Bool.class | Float.class | UUID.class
alias ColumnType = String.class | Time.class | Int32.class | Int64.class | Bool.class | Float.class | UUID.class | JSON::Any.class

def column_type(type : String.class)
"text"
Expand Down Expand Up @@ -32,4 +32,8 @@ module Avram::Migrator::ColumnTypeOptionHelpers
def column_type(type : UUID.class)
"uuid"
end

def column_type(type : JSON::Any.class)
"jsonb"
end
end
5 changes: 4 additions & 1 deletion src/avram/query_builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ class Avram::QueryBuilder

private def param_values(params)
params.values.map do |value|
if value.nil?
case value
when Nil
nil
when JSON::Any
value.to_json
else
value.to_s
end
Expand Down
19 changes: 14 additions & 5 deletions src/avram/save_operation.cr
Original file line number Diff line number Diff line change
Expand Up @@ -238,16 +238,25 @@ abstract class Avram::SaveOperation(T)
_changes = {} of Symbol => String?
column_attributes.each do |attribute|
if attribute.changed?
_changes[attribute.name] = if attribute.value.nil?
nil
else
attribute.value.to_s
end
_changes[attribute.name] = cast_value(attribute.value)
end
end
_changes
end

private def cast_value(value : Nil)
nil
end

private def cast_value(value : Object)
case value
when JSON::Any
value.to_json
else
value.to_s
end
end

def save : Bool
if perform_save
self.save_status = SaveStatus::Saved
Expand Down