# rubocop:disable Style/IfUnlessModifier
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
# This module implements an adapter between the Simple::SQL interface
# (i.e. ask, all, first, transaction) and a raw connection.
#
# This module can be mixed onto objects that implement a raw_connection
# method, which must return a Pg::Connection.
module Simple::SQL::ConnectionAdapter
Logging = ::Simple::SQL::Logging
Encoder = ::Simple::SQL::Encoder
Decoder = ::Simple::SQL::Decoder
Scope = ::Simple::SQL::Scope
# execute one or more sql statements. This method does not allow to pass in
# arguments - since the pg client does not support this - but it allows to
# run multiple sql statements separated by ";"
def exec(sql)
Logging.yield_logged sql do
raw_connection.exec sql
end
end
# Runs a query, with optional arguments, and returns the result. If the SQL
# query returns rows with one column, this method returns an array of these
# values. Otherwise it returns an array of arrays.
#
# Example:
#
# - Simple::SQL.all("SELECT id FROM users") returns an array of id values
# - Simple::SQL.all("SELECT id, email FROM users") returns an array of
# arrays `[ , ]`.
#
# Simple::SQL.all "SELECT id, email FROM users" do |id, email|
# # do something
# end
def all(sql, *args, into: nil, &block)
result = exec_logged(sql, *args)
result = enumerate(result, into: into, &block)
if sql.is_a?(Scope) && sql.paginated?
add_page_info(sql, result)
end
result
end
# Runs a query and returns the first result row of a query.
#
# Examples:
#
# - Simple::SQL.ask "SELECT id FROM users WHERE email=$?", "foo@local"
# returns a number (or +nil+)
# - Simple::SQL.ask "SELECT id, email FROM users WHERE email=$?", "foo@local"
# returns an array [ , ] (or +nil+)
def ask(sql, *args, into: nil)
catch(:ok) do
all(sql, *args, into: into) { |row| throw :ok, row }
nil
end
end
private
def add_page_info(scope, results)
raise ArgumentError, "expect Array but get a #{results.class.name}" unless results.is_a?(Array)
raise ArgumentError, "per must be > 0" unless scope.per > 0
# optimization: add empty case (page <= 1 && results.empty?)
if scope.page <= 1 && results.empty?
Scope::PageInfo.attach(results, total_count: 0, per: scope.per, page: 1)
else
sql = "SELECT COUNT(*) FROM (#{scope.order_by(nil).to_sql(pagination: false)}) simple_sql_count"
total_count = ask(sql, *scope.args)
Scope::PageInfo.attach(results, total_count: total_count, per: scope.per, page: scope.page)
end
end
def exec_logged(sql_or_scope, *args)
if sql_or_scope.is_a?(Scope)
raise ArgumentError, "You cannot call .all with a scope and additional arguments" unless args.empty?
sql = sql_or_scope.to_sql
args = sql_or_scope.args
else
sql = sql_or_scope
end
Logging.yield_logged sql, *args do
raw_connection.exec_params(sql, Encoder.encode_args(raw_connection, args))
end
end
def enumerate(result, into:, &block)
decoder = Decoder.new(self, result, into: into)
if block
result.each_row do |row|
yield decoder.decode(row)
end
self
else
ary = []
result.each_row { |row| ary << decoder.decode(row) }
ary
end
end
public
def resolve_type(ftype, fmod)
@resolved_types ||= {}
@resolved_types[[ftype, fmod]] ||= raw_connection.exec("SELECT format_type($1,$2)", [ftype, fmod]).getvalue(0, 0)
end
end