Skip to content

Commit

Permalink
Add support for multiple entities per query
Browse files Browse the repository at this point in the history
  • Loading branch information
jgaskins committed Oct 10, 2020
1 parent 4e5068d commit afbeb0c
Showing 1 changed file with 133 additions and 0 deletions.
133 changes: 133 additions & 0 deletions src/interro.cr
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,136 @@ module Interro
class NotFound < Exception
end
end


############# MONKEYPATCHES #######################

# :nodoc:
module DB
annotation Field
end

# :nodoc:
module Serializable
# Adding support for deserializing multiple entities in a single query. This
# is almost identical to the canonical DB::Serializable, but we had to copy
# the entire thing because it's all inside a single macro.
macro included
include ::DB::Mappable

# Define a `new` and `from_rs` directly in the type, like JSON::Serializable
# For proper overload resolution

def self.new(rs : ::DB::ResultSet)
instance = allocate
instance.initialize(__set_for_db_serializable: rs)
GC.add_finalizer(instance) if instance.responds_to?(:finalize)
instance
end

def self.from_rs(rs : ::DB::ResultSet)
objs = Array(self).new
rs.each do
objs << self.new(rs)
end
objs
ensure
rs.close
end
end

def initialize(*, __set_for_db_serializable rs : ::DB::ResultSet)
{% begin %}
{% properties = {} of Nil => Nil %}
{% for ivar in @type.instance_vars %}
{% ann = ivar.annotation(::DB::Field) %}
{% unless ann && ann[:ignore] %}
{%
properties[ivar.id] = {
type: ivar.type,
key: ((ann && ann[:key]) || ivar).id.stringify,
default: ivar.default_value,
nilable: ivar.type.nilable?,
converter: ann && ann[:converter],
}
%}
{% end %}
{% end %}

{% for name, value in properties %}
%var{name} = nil
%found{name} = false
{% end %}

rs.each_column_from_last do |col_name|
case col_name
{% for name, value in properties %}
when {{value[:key]}}
%found{name} = true
%var{name} =
{% if value[:converter] %}
{{value[:converter]}}.from_rs(rs)
{% elsif value[:nilable] || value[:default] != nil %}
rs.read(::Union({{value[:type]}} | Nil))
{% else %}
rs.read({{value[:type]}})
{% end %}

{% for name_for_check, __value in properties %}
next unless %found{name_for_check}
{% end %}
break
{% end %}
else
rs.read # Advance set, but discard result
on_unknown_db_column(col_name)
end
end

{% for key, value in properties %}
{% unless value[:nilable] || value[:default] != nil %}
if %var{key}.is_a?(Nil) && !%found{key}
raise ::DB::MappingException.new("missing result set attribute: {{(value[:key] || key).id}}")
end
{% end %}
{% end %}

{% for key, value in properties %}
{% if value[:nilable] %}
{% if value[:default] != nil %}
@{{key}} = %found{key} ? %var{key} : {{value[:default]}}
{% else %}
@{{key}} = %var{key}
{% end %}
{% elsif value[:default] != nil %}
if %var{key}.nil?
@{{key}} = {{value[:default]}}
end
{% else %}
@{{key}} = %var{key}.as({{value[:type]}})
{% end %}
{% end %}
{% end %}
end

protected def on_unknown_db_column(col_name)
raise ::DB::MappingException.new("unknown result set attribute: #{col_name}")
end

module NonStrict
protected def on_unknown_db_column(col_name)
end
end
end
end

# :nodoc:
class PG::ResultSet
# We have to monkeypatch this to support the modification in DB::Serializable
# above
def each_column_from_last
(@column_index...column_count).each do |i|
yield column_name(i)
end
end
end

0 comments on commit afbeb0c

Please sign in to comment.