diff --git a/db/migrations/20240309222910_add_enums_to_bucket.cr b/db/migrations/20240309222910_add_enums_to_bucket.cr new file mode 100644 index 000000000..2a3e0813c --- /dev/null +++ b/db/migrations/20240309222910_add_enums_to_bucket.cr @@ -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 diff --git a/spec/avram/array_column_spec.cr b/spec/avram/array_column_spec.cr index 7c0848f40..c5a80d42c 100644 --- a/spec/avram/array_column_spec.cr +++ b/spec/avram/array_column_spec.cr @@ -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 diff --git a/spec/avram/queryable_spec.cr b/spec/avram/queryable_spec.cr index 3c690042c..88574a14a 100644 --- a/spec/avram/queryable_spec.cr +++ b/spec/avram/queryable_spec.cr @@ -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 diff --git a/spec/support/factories/bucket_factory.cr b/spec/support/factories/bucket_factory.cr index c5f3d030c..3dd78db73 100644 --- a/spec/support/factories/bucket_factory.cr +++ b/spec/support/factories/bucket_factory.cr @@ -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 diff --git a/spec/support/models/bucket.cr b/spec/support/models/bucket.cr index 480c4d659..31dddafc9 100644 --- a/spec/support/models/bucket.cr +++ b/spec/support/models/bucket.cr @@ -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 @@ -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 diff --git a/src/avram/charms/enum_extensions.cr b/src/avram/charms/enum_extensions.cr index 4c4d38112..60e3348be 100644 --- a/src/avram/charms/enum_extensions.cr +++ b/src/avram/charms/enum_extensions.cr @@ -28,10 +28,28 @@ 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(value : T) SuccessfulCast.new(value) end + def to_db(values : Array(T)) + encoded = values.map { |value| to_db(value) }.as(Array(String)) + PQ::Param.encode_array(encoded) + end + def to_db(value : T) : String value.value.to_s end diff --git a/src/avram/model.cr b/src/avram/model.cr index 6733a5c1d..59d6ca685 100644 --- a/src/avram/model.cr +++ b/src/avram/model.cr @@ -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 %} @@ -233,6 +233,8 @@ abstract class Avram::Model converter: JSONConverter({{ data_type }}), {% elsif data_type.id == Array(Float64).id %} converter: PG::NumericArrayFloatConverter, + {% elsif converter %} + converter: {{ converter }}, {% end %} )] {% if data_type.is_a?(Generic) || is_serialized %} diff --git a/src/ext/pg/enum_array_converter.cr b/src/ext/pg/enum_array_converter.cr new file mode 100644 index 000000000..93ffee140 --- /dev/null +++ b/src/ext/pg/enum_array_converter.cr @@ -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