module DB # Common interface for connection based statements # and for connection pool statements. module StatementMethods # 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 include Disposable protected def do_close end # :nodoc: getter connection getter command : String def initialize(@connection : Connection, @command : String) end # :nodoc: property auto_close : Bool = false # :nodoc: def release_from_result_set self.close if @auto_close self.release_connection 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