First off, this is probably not a bug with crecto, but crecto is what brought this issue up, so I'm filing it here just in case. Second, I apologize for the verbosity of this issue; the stacktraces are pretty big and the types involved are rather long.
I have a simple User
model inheriting Crecto::Model
(Membership
is also a simple Crecto::Model
):
class User < Crecto::Model
schema "users" do
field :uid, String, primary_key: true
field :name, String
has_many :memberships, Membership, dependent: :destroy
end
validate_required [:uid, :name]
end
and I then in another view, I'm trying to load all Membership
s for the User
:
memberships = Repo.all(user, :memberships)
But, when I try to compile this, I get a pretty scary type error (I've left out the irrelevant frames):
in src/templates/users/index.html.ecr:20: instantiating 'Repo:Module#all(User, Symbol)'
<%- memberships = Repo.all(user, :memberships) %>
^~~
in lib/crecto/src/crecto/repo.cr:41: instantiating 'Crecto::Repo::Query:Class#where(Symbol, (String | Nil))'
query = Crecto::Repo::Query.where(queryable_instance.class.foreign_key_for_association(association_name), queryable_instance.pkey_value)
^~~~~
in lib/crecto/src/crecto/repo/query.cr:61: instantiating 'Crecto::Repo::Query#where(Symbol, (String | Nil))'
self.new.where(where_sym, param)
^~~~~
in lib/crecto/src/crecto/repo/query.cr:224: instantiating 'Array(Hash(Symbol, Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)) | Hash(Symbol, Array(Int32 | Int64 | Nil)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil) | Hash(Symbol, Int32 | Int64 | Nil) | Hash(Symbol, Int32 | Int64 | String | Nil) | Hash(Symbol, Int32 | Int64 | String) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64 | String) | Hash(Symbol, Int64) | Hash(Symbol, Nil) | Hash(Symbol, String) | NamedTuple(clause: String, params: Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)))#push(Hash(Symbol, String | Nil))'
@wheres.push(Hash.zip([where_sym], [param]))
^~~~
in /usr/local/Cellar/crystal-lang/0.22.0/src/array.cr:1335: instantiating 'Pointer(Hash(Symbol, Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)) | Hash(Symbol, Array(Int32 | Int64 | Nil)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil) | Hash(Symbol, Int32 | Int64 | Nil) | Hash(Symbol, Int32 | Int64 | String | Nil) | Hash(Symbol, Int32 | Int64 | String) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64 | String) | Hash(Symbol, Int64) | Hash(Symbol, Nil) | Hash(Symbol, String | Nil) | Hash(Symbol, String) | NamedTuple(clause: String, params: Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)))#[]=(Int32, Hash(Symbol, String | Nil))'
@buffer[@size] = value
^
in /usr/local/Cellar/crystal-lang/0.22.0/src/pointer.cr:129: instantiating 'Pointer(Hash(Symbol, Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)) | Hash(Symbol, Array(Int32 | Int64 | Nil)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil) | Hash(Symbol, Int32 | Int64 | Nil) | Hash(Symbol, Int32 | Int64 | String | Nil) | Hash(Symbol, Int32 | Int64 | String) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64 | String) | Hash(Symbol, Int64) | Hash(Symbol, Nil) | Hash(Symbol, String | Nil) | Hash(Symbol, String) | NamedTuple(clause: String, params: Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)))#value=(Hash(Symbol, String | Nil))'
(self + offset).value = value
^~~~~
in /usr/local/Cellar/crystal-lang/0.22.0/src/primitives.cr:175: type must be (Hash(Symbol, Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)) | Hash(Symbol, Array(Int32 | Int64 | Nil)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil) | Hash(Symbol, Int32 | Int64 | Nil) | Hash(Symbol, Int32 | Int64 | String | Nil) | Hash(Symbol, Int32 | Int64 | String) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64 | String) | Hash(Symbol, Int64) | Hash(Symbol, Nil) | Hash(Symbol, String) | NamedTuple(clause: String, params: Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil))), not (Hash(Symbol, Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)) | Hash(Symbol, Array(Int32 | Int64 | Nil)) | Hash(Symbol, Array(Int32)) | Hash(Symbol, Array(Int64)) | Hash(Symbol, Array(String)) | Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil) | Hash(Symbol, Int32 | Int64 | Nil) | Hash(Symbol, Int32 | Int64 | String | Nil) | Hash(Symbol, Int32 | Int64 | String) | Hash(Symbol, Int32 | String) | Hash(Symbol, Int32) | Hash(Symbol, Int64 | String) | Hash(Symbol, Int64) | Hash(Symbol, Nil) | Hash(Symbol, String | Nil) | Hash(Symbol, String) | NamedTuple(clause: String, params: Array(Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)))
def value : T
It seemed like the issue was stemming from @wheres.push(Hash.zip([where_sym], [param]))
, where potentially the result of Hash.zip
wasn't the right type for @wheres
, so I attempted to forcibly cast the result to the appropriate type before the call to push
:
# src/crecto/repo/query.cr:223
def where(where_sym : Symbol, param : DbValue)
- @wheres.push(Hash.zip([where_sym], [param]))
+ @wheres.push(Hash.zip([where_sym], [param]).as(Hash(Symbol, DbValue))
self
end
which yielded a different, more interesting error:
in lib/crecto/src/crecto/repo/query.cr:224: can't cast Hash(Symbol, String | Nil) to Hash(Symbol, Bool | Float32 | Float64 | Int16 | Int32 | Int64 | JSON::Any | String | Time | Nil)
@wheres.push(Hash.zip([where_sym], [param]).as(Hash(Symbol, DbValue)))
This surprised me, because I would've thought the result of Hash.zip
would be Hash(Symbol, DbValue)
, since that's what the types of the where_sym
and param
arguments are, and is where I think the bug lies.
I also tried avoiding Hash.zip
entirely and just using a Hash Literal construct:
# src/crecto/repo/query.cr:223
def where(where_sym : Symbol, param : DbValue)
- @wheres.push(Hash.zip([where_sym], [param]))
+ @wheres.push({ where_sym => param })
self
end
but this yielded the same original error about value... type must be...
.
The last thing I tried was forcing param
to be a DbValue
again:
# src/crecto/repo/query.cr:223
def where(where_sym : Symbol, param : DbValue)
- @wheres.push(Hash.zip([where_sym], [param]))
+ @wheres.push({ where_sym => param.as(DbValue) })
self
end
which finally compiled successfully, and seems to work correctly with all of the testing I've done so far.
I haven't found anything about needing this kind of casting in Crystal elsewhere, but my searches also haven't been very effective for this issue in general. I'm wondering if param
is actually a sub-type of DbValue
, and @wheres
for some reason thinks the result is incompatible with it's type.
Sorry again for the long description, but hopefully this can get figured out/resolved soon. It seems like just a one-line change, but I can make a PR for it if you'd like :)