/
statement.cr
190 lines (162 loc) · 5.53 KB
/
statement.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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
module DB
# Common interface for connection based statements
# and for connection pool statements.
module StatementMethods
include Disposable
protected def do_close
end
# See `QueryMethods#scalar`
def scalar(*args_, args : Array? = nil)
query(*args_, args: args) do |rs|
rs.each do
return rs.read
end
end
raise NoResultsError.new("no results")
end
# See `QueryMethods#query`
def query(*args_, args : Array? = nil)
rs = query(*args_, args: args)
yield rs ensure rs.close
end
# See `QueryMethods#exec`
abstract def exec : ExecResult
# See `QueryMethods#exec`
abstract def exec(*args_, args : Array? = nil) : ExecResult
# See `QueryMethods#query`
abstract def query : ResultSet
# See `QueryMethods#query`
abstract def query(*args_, args : Array? = nil) : ResultSet
end
# Represents a query in a `Connection`.
# It should be created by `QueryMethods`.
#
# ### Note to implementors
#
# 1. Subclass `Statements`
# 2. `Statements` are created from a custom driver `Connection#prepare` method.
# 3. `#perform_query` executes a query that is expected to return a `ResultSet`
# 4. `#perform_exec` executes a query that is expected to return an `ExecResult`
# 6. `#do_close` is called to release the statement resources.
abstract class Statement
include StatementMethods
# :nodoc:
getter connection
getter command : String
def initialize(@connection : Connection, @command : String)
end
def release_connection
@connection.release_from_statement
end
# See `QueryMethods#exec`
def exec : DB::ExecResult
perform_exec_and_release(Slice(Any).empty)
end
# See `QueryMethods#exec`
def exec(*args_, args : Array? = nil) : DB::ExecResult
perform_exec_and_release(EnumerableConcat.build(args_, args))
end
# See `QueryMethods#query`
def query : DB::ResultSet
perform_query_with_rescue Tuple.new
end
# See `QueryMethods#query`
def query(*args_, args : Array? = nil) : DB::ResultSet
perform_query_with_rescue(EnumerableConcat.build(args_, args))
end
private def perform_exec_and_release(args : Enumerable) : ExecResult
around_query_or_exec(args) do
perform_exec(args)
end
ensure
release_connection
end
private def perform_query_with_rescue(args : Enumerable) : ResultSet
around_query_or_exec(args) do
perform_query(args)
end
rescue e : Exception
# Release connection only when an exception occurs during the query
# execution since we need the connection open while the ResultSet is open
release_connection
raise e
end
protected abstract def perform_query(args : Enumerable) : ResultSet
protected abstract def perform_exec(args : Enumerable) : ExecResult
# This method is called when executing the statement. Although it can be
# redefined, it is recommended to use the `def_around_query_or_exec` macro
# to be able to add new behaviors without loosing prior existing ones.
protected def around_query_or_exec(args : Enumerable)
yield
end
# This macro allows injecting code to be run before and after the execution
# of the request. It should return the yielded value. It must be called with 1
# block argument that will be used to pass the `args : Enumerable`.
#
# ```
# class DB::Statement
# def_around_query_or_exec do |args|
# # do something before query or exec
# res = yield
# # do something after query or exec
# res
# end
# end
# ```
macro def_around_query_or_exec(&block)
protected def around_query_or_exec(%args : Enumerable)
previous_def do
{% if block.args.size != 1 %}
{% raise "Wrong number of block arguments (given #{block.args.size}, expected: 1)" %}
{% end %}
{{ block.args.first.id }} = %args
{{ block.body }}
end
end
end
def_around_query_or_exec do |args|
emit_log(args)
yield
end
protected def emit_log(args : Enumerable)
Log.debug &.emit("Executing query", query: command, args: MetadataValueConverter.arg_to_log(args))
end
end
# This module converts DB supported values to `::Log::Metadata::Value`
#
# ### Note to implementors
#
# If the driver defines custom types to be used as arguments the default behavior
# will be converting the value via `#to_s`. Otherwise you can define overloads to
# change this behaviour.
#
# ```
# module DB::MetadataValueConverter
# def self.arg_to_log(arg : PG::Geo::Point)
# ::Log::Metadata::Value.new("(#{arg.x}, #{arg.y})::point")
# end
# end
# ```
module MetadataValueConverter
# Returns *arg* encoded as a `::Log::Metadata::Value`.
def self.arg_to_log(arg) : ::Log::Metadata::Value
::Log::Metadata::Value.new(arg.to_s)
end
# :ditto:
def self.arg_to_log(arg : Enumerable) : ::Log::Metadata::Value
::Log::Metadata::Value.new(arg.to_a.map { |a| arg_to_log(a).as(::Log::Metadata::Value) })
end
# :ditto:
def self.arg_to_log(arg : Int) : ::Log::Metadata::Value
::Log::Metadata::Value.new(arg.to_i64)
end
# :ditto:
def self.arg_to_log(arg : UInt64) : ::Log::Metadata::Value
::Log::Metadata::Value.new(arg.to_s)
end
# :ditto:
def self.arg_to_log(arg : Nil | Bool | Int32 | Int64 | Float32 | Float64 | String | Time) : ::Log::Metadata::Value
::Log::Metadata::Value.new(arg)
end
end
end