/
result_set.cr
175 lines (151 loc) · 4.41 KB
/
result_set.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
module DB
# The response of a query performed on a `Database`.
#
# See `DB` for a complete sample.
#
# Each `#read` call consumes the result and moves to the next column.
# Each column must be read in order.
# At any moment a `#move_next` can be invoked, meaning to skip the
# remaining, or even all the columns, in the current row.
# Also it is not mandatory to consume the whole `ResultSet`, hence an iteration
# through `#each` or `#move_next` can be stopped.
#
# **Note:** depending on how the `ResultSet` was obtained it might be mandatory an
# explicit call to `#close`. Check `QueryMethods#query`.
#
# ### Note to implementors
#
# 1. Override `#move_next` to move to the next row.
# 2. Override `#read` returning the next value in the row.
# 3. (Optional) Override `#read(t)` for some types `t` for which custom logic other than a simple cast is needed.
# 4. Override `#column_count`, `#column_name`.
abstract class ResultSet
include Disposable
# :nodoc:
getter statement
def initialize(@statement : DB::Statement)
end
protected def do_close
statement.release_from_result_set
end
# TODO add_next_result_set : Bool
# Iterates over all the rows
def each
while move_next
yield
end
end
# Iterates over all the columns
def each_column
column_count.times do |x|
yield column_name(x)
end
end
# Move the next row in the result.
# Return `false` if no more rows are available.
# See `#each`
abstract def move_next : Bool
# TODO def empty? : Bool, handle internally with move_next (?)
# Returns the number of columns in the result
abstract def column_count : Int32
# Returns the name of the column in `index` 0-based position.
abstract def column_name(index : Int32) : String
# Returns the name of the columns.
def column_names
Array(String).new(column_count) { |i| column_name(i) }
end
# Reads the next column value
abstract def read
# Returns the column index that corresponds to the next `#read`.
#
# If the last column of the current row has been read, it must return `#column_count`.
abstract def next_column_index : Int32
# Reads the next columns and maps them to a class
def read(type : DB::Mappable.class)
type.new(self)
end
# Reads the next column value as a **type**
def read(type : T.class) : T forall T
col_index = next_column_index
value = read
if value.is_a?(T)
value
else
raise DB::ColumnTypeMismatchError.new(
context: "#{self.class}#read",
column_index: col_index,
column_name: column_name(col_index),
column_type: value.class.to_s,
expected_type: T.to_s
)
end
end
# Read the value based on the given `enum` type, supporting both string and
# numeric column types.
#
# ```
# enum Status
# Pending
# Complete
# end
#
# db.query "SELECT 'complete'" do |rs|
# rs.read Status # => Status::Complete
# end
# ```
def read(type : Enum.class)
type.new(self)
end
# Reads the next columns and returns a tuple of the values.
def read(*types : Class)
internal_read(*types)
end
# Reads the next columns and returns a named tuple of the values.
def read(**types : Class)
internal_read(**types)
end
private def internal_read(*types : *T) forall T
{% begin %}
Tuple.new(
{% for type in T %}
read({{type.instance}}),
{% end %}
)
{% end %}
end
private def internal_read(**types : **T) forall T
{% begin %}
NamedTuple.new(
{% for name, type in T %}
{{ name }}: read({{type.instance}}),
{% end %}
)
{% end %}
end
# def read_blob
# yield ... io ....
# end
# def read_text
# yield ... io ....
# end
end
end
struct Enum
def self.new(rs : DB::ResultSet) : self
index = rs.next_column_index
case value = rs.read
when String
parse value
when Int
from_value value
else
raise DB::ColumnTypeMismatchError.new(
context: "#{self}.new(rs : DB::ResultSet)",
column_index: index,
column_name: rs.column_name(index),
column_type: value.class.to_s,
expected_type: "String | Int",
)
end
end
end