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

Adds in Array enums. #1009

Merged
merged 2 commits into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
13 changes: 13 additions & 0 deletions db/migrations/20240309222910_add_enums_to_bucket.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class AddEnumsToBucket::V20240309222910 < Avram::Migrator::Migration::V1
def migrate
alter table_for(Bucket) do
add enums : Array(Int32), default: [] of Int32
end
end

def rollback
alter table_for(Bucket) do
remove :enums
end
end
end
8 changes: 8 additions & 0 deletions spec/avram/array_column_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,12 @@ describe "Array Columns" do
bucket = SaveBucket.update!(bucket, numbers: nil)
bucket.numbers.should be_nil
end

it "handles Array(Enum)" do
BucketFactory.create &.enums([Bucket::Size::Large, Bucket::Size::Tub])
bucket = BucketQuery.new.last
bucket.enums.should eq([Bucket::Size::Large, Bucket::Size::Tub])
bucket = SaveBucket.update!(bucket, enums: [Bucket::Size::Small])
bucket.enums.should eq([Bucket::Size::Small])
end
end
4 changes: 3 additions & 1 deletion spec/avram/queryable_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1172,12 +1172,14 @@ describe Avram::Queryable do
context "when querying arrays" do
describe "simple where query" do
it "returns 1 result" do
bucket = BucketFactory.new.names(["pumpkin", "zucchini"]).create
bucket = BucketFactory.new.names(["pumpkin", "zucchini"]).enums([Bucket::Size::Medium]).create

query = BucketQuery.new.names(["pumpkin", "zucchini"])
query.to_sql.should eq ["SELECT #{Bucket::COLUMN_SQL} FROM buckets WHERE buckets.names = $1", "{\"pumpkin\",\"zucchini\"}"]
result = query.first
result.should eq bucket

BucketQuery.new.enums.includes(Bucket::Size::Medium).select_count.should eq(1)
end
end

Expand Down
1 change: 1 addition & 0 deletions spec/support/factories/bucket_factory.cr
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ class BucketFactory < BaseFactory
names ["Mario", "Luigi"]
floaty_numbers [0.0]
oody_things [UUID.random]
enums [Bucket::Size::ExtraSmall, Bucket::Size::Medium]
end
end
13 changes: 12 additions & 1 deletion spec/support/models/bucket.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
class Bucket < BaseModel
COLUMN_SQL = "buckets.id, buckets.created_at, buckets.updated_at, buckets.bools, buckets.small_numbers, buckets.numbers, buckets.big_numbers, buckets.names, buckets.floaty_numbers, buckets.oody_things"
COLUMN_SQL = column_names.join(", ") { |col| "buckets.#{col}" }

enum Size
ExtraSmall
Small
Medium
Large
ExtraLarge
Tub
end

table do
column bools : Array(Bool) = [] of Bool
column small_numbers : Array(Int16) = [] of Int16
Expand All @@ -8,5 +18,6 @@ class Bucket < BaseModel
column names : Array(String) = [] of String
column floaty_numbers : Array(Float64) = [] of Float64
column oody_things : Array(UUID) = [] of UUID
column enums : Array(Bucket::Size) = [] of Bucket::Size, converter: PG::EnumArrayConverter(Bucket::Size)
end
end
27 changes: 27 additions & 0 deletions src/avram/charms/enum_extensions.cr
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,37 @@ abstract struct Enum
end
end

def parse(value : Array(T))
SuccessfulCast(Array(T)).new value
end

def parse(values : Array(Int))
results = values.map { |i| parse(i) }
if results.all?(SuccessfulCast)
parse(results.map(&.value.as(T)))
else
FailedCast.new
end
end

# def parse(values : Array(String))
# results = values.map {|s| parse(s)}
# if results.all?(SuccessfulCast)
# parse(results.map(&.value.as(T)))
# else
# FailedCast.new
# end
# end
jwoertink marked this conversation as resolved.
Show resolved Hide resolved

def parse(value : T)
SuccessfulCast.new(value)
end

def to_db(values : Array(T))
encoded = values.map { |value| to_db(value) }.as(Array(String))
Copy link
Member Author

Choose a reason for hiding this comment

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

I actually don't know why this needs to be String.... but if you look at the method below, it's also converting the enum value to a string. Maybe a PQ thing? 🤔

PQ::Param.encode_array(encoded)
end

def to_db(value : T) : String
value.value.to_s
end
Expand Down
4 changes: 3 additions & 1 deletion src/avram/model.cr
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ abstract class Avram::Model
{% end %}
end

macro column(type_declaration, autogenerated = false, serialize is_serialized = false, allow_blank = false)
macro column(type_declaration, autogenerated = false, serialize is_serialized = false, allow_blank = false, converter = nil)
{% if type_declaration.type.is_a?(Union) %}
{% data_type = type_declaration.type.types.first %}
{% nilable = true %}
Expand All @@ -233,6 +233,8 @@ abstract class Avram::Model
converter: JSONConverter({{ data_type }}),
{% elsif data_type.id == Array(Float64).id %}
converter: PG::NumericArrayFloatConverter,
{% elsif converter %}
Copy link
Member Author

Choose a reason for hiding this comment

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

Originally I was going to use

{% elsif data_type.resolve < Enum %}

This would let us pick up the type and set the converter automatically; however, once I did this, it starts to resolve ALL the types. This means that in the comment model where we have belongs_to post : Post, the Post is undefined because it's resolved before crystal requires the post.cr file. It would end up being a breaking change since any model that references another later in alphabetical order would now have to be required in that file. (or you'd just require each model one by one in the proper order)

converter: {{ converter }},
{% end %}
)]
{% if data_type.is_a?(Generic) || is_serialized %}
Expand Down
15 changes: 15 additions & 0 deletions src/ext/pg/enum_array_converter.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Extends the PG shard and adds a converter for
# converting `Array(Int)` columns to `Array(Enum)`. This
# can be used with raw SQL queries.
# ```
# enum Colors
# Red
# end
# @[DB::Field(converter: PG::EnumArrayConverter(Colors))]
# property colors : Array(Colors)
# ```
module PG::EnumArrayConverter(T)
def self.from_rs(rs : DB::ResultSet)
rs.read(Array(typeof(T.values.first.value))).map { |i| T.from_value(i) }
end
end