lib/db_mod/statements/prepared.rb

Summary

Maintainability
A
0 mins
Test Coverage
require_relative 'parameters'

module DbMod
  module Statements
    # Provides the +def_prepared+ function which allows
    # {DbMod} modules to declare prepared SQL statements
    # that will be added to the database connection when
    # {DbMod#db_connect} is called.
    #
    # For statements that are not prepared ahead of execution,
    # see +def_statement+ in {DbMod::Statements::Statement}.
    #
    # +def_prepared+ accepts two parameters:
    # * *name* [Symbol]: The name that will be given to
    #   the prepared statement. A method will also be defined
    #   on the module with the same name which will call the
    #   statement and return the result.
    # * *sql* [String]: The SQL statement to be prepared.
    #   Parameters may be declared using the $ symbol followed
    #   by a number +($1, $2, $3)+ or a name +($one, $two, $a_b)+.
    #   The two styles may not be mixed in the same statement.
    #   The defined function can then be passed parameters
    #   that will be used when the statement is executed.
    #
    #  module MyModule
    #    include DbMod
    #
    #    def_prepared :my_prepared, <<-SQL
    #      SELECT *
    #        FROM stuff
    #       WHERE a = $1 AND b = $2
    #    SQL
    #
    #    def_prepared :my_named_prepared, <<-SQL
    #      SELECT *
    #        FROM stuff
    #       WHERE a = $a AND b = $b
    #    SQL
    #  end
    #
    #  include MyModule
    #  db_connect db: 'mydb'
    #  my_prepared(1,2)
    #  my_named_prepared(a: 1, b: 2)
    module Prepared
      # Defines a module-specific +def_prepared+ function
      # for a module that has just had {DbMod} included.
      #
      # @param mod [Module] module including {DbMod}
      # @see DbMod.included
      def self.setup(mod)
        Prepared.define_def_prepared(mod)
        Prepared.define_prepared_statements(mod)
        Prepared.define_inherited_prepared_statements(mod)
        Prepared.define_prepare_all_statements(mod)
      end

      private

      # Add a +def_prepared+ method definition to a module.
      # This method allows modules to declare named SQL statements
      # that will be prepared when the database connection is
      # established, and that can be accessed via an instance
      # method with the same name.
      #
      # @param mod [Module] a module with {DbMod} included
      # @raise ArgumentError if there is a problem parsing
      #   method parameters from the SQL statement
      def self.define_def_prepared(mod)
        class << mod
          define_method(:def_prepared) do |name, sql, &block|
            sql = sql.dup
            name = name.to_sym

            params = Parameters.parse_params! sql
            prepared_statements[name] = sql
            Prepared.define_prepared_method(self, name, params, &block)
          end
        end
      end

      # Defines +prepare_all_statements+, a module method which
      # accepts a connection object and will prepare on it all of
      # the prepared statements that have been declared on the
      # module or any of its included modules.
      #
      # @param mod [Module] module that has {DbMod} included
      # @see DbMod#db_connect
      def self.define_prepare_all_statements(mod)
        class << mod
          define_method(:prepare_all_statements) do |conn|
            inherited_prepared_statements.each do |name, sql|
              conn.prepare(name.to_s, sql)
            end
          end
        end
      end

      # Merge the prepared statements from a module
      # into a given hash. Fails if there are any
      # duplicates.
      #
      # @param statements [Hash] named list of prepared statements
      # @param klass [Class,Module] ancestor (hopefully a DbMod module)
      #   to collect prepared statements from
      # @see Prepared.define_inherited_prepared_statements
      def self.merge_statements(statements, klass)
        return unless klass.respond_to? :prepared_statements
        return if klass.prepared_statements.nil?

        klass.prepared_statements.each do |name, sql|
          fail DbMod::Exceptions::DuplicateStatementName if statements.key? name

          statements[name] = sql
        end
      end

      # Define a method in the given module with the given name
      # and parameters, that will call the prepared statement
      # with the same name.
      #
      # @param mod [Module] module declaring the method
      # @param name [Symbol] method name
      # @param params [Fixnum,Array<Symbol>]
      #   expected parameter count, or a list of argument names.
      #   An empty array produces a no-argument method.
      # @param block [Proc] A dsl block may be passed, which will be evaluated
      #   using a {Configuration::MethodConfiguration} object as scope
      # @see Configurable.def_configurable
      def self.define_prepared_method(mod, name, params, &block)
        if params == []
          define_no_args_prepared_method(mod, name, &block)
        else
          define_prepared_method_with_args(mod, name, params, &block)
        end
      end

      # Define a no-argument method with the given name
      # that will call the prepared statement with the
      # same name.
      #
      # @param mod [Module] {DbMod} enabled module
      #   where the method will be defined
      # @param name [Symbol] name of the method to be defined
      #   and the prepared query to be called.
      # @param block [Proc] see {Configuration::MethodConfiguration}
      # @yield dsl method configuration object may be passed
      def self.define_no_args_prepared_method(mod, name, &block)
        method = ->(*) { conn.exec_prepared(name.to_s) }
        Configuration.def_configurable mod, name, method, &block
      end

      # Define a method on the given module with the given name
      # and parameters that will call the prepared statement with
      # the same name. Additional method configuration settings may
      # be passed via a block.
      #
      # @param mod [Module] {DbMod} enabled module
      #   where the method will be defined
      # @param name [Symbol] name of the method to be defined
      #   and the prepared query to be called.
      # @param params [Fixnum,Array<Symbol>]
      #   expected parameter count, or a list of argument names.
      #   An empty array produces a no-argument method.
      # @param block [Proc] see {Configuration::MethodConfiguration}
      # @yield dsl method configuration object may be passed
      def self.define_prepared_method_with_args(mod, name, params, &block)
        method = ->(*args) { conn.exec_prepared(name.to_s, args) }
        Configuration.def_configurable(mod, name, method, params, &block)
      end

      # Adds +prepared_statements+ to a module. This list of named
      # prepared statements will be added to the connection when
      # {DbMod#db_connect} is called.
      #
      # @param mod [Module]
      # @see Prepared.define_inherited_prepared_statements
      def self.define_prepared_statements(mod)
        class << mod
          define_method(:prepared_statements) do
            @prepared_statements ||= {}
          end
        end
      end

      # Adds +inherited_prepared_statements+ to a module. This list
      # of named prepared statements declared on this module and all
      # included modules will be added to the connection when
      # {DbMod#db_connect} is called.
      #
      # @param mod [Module] where +inherited_prepared_statements+
      #   should be defined
      # @see Prepared.define_prepare_all_statements
      def self.define_inherited_prepared_statements(mod)
        class << mod
          define_method(:inherited_prepared_statements) do
            inherited = {}
            ancestors.each do |klass|
              Prepared.merge_statements(inherited, klass)
            end
            inherited
          end
        end
      end
    end
  end
end