module Sequel
  module Plugins
    # The pg_row plugin allows you to use Sequel::Model classes as composite type
    # classes, via the pg_row extension.  So if you have an address table:
    #
    #   DB.create_table(:address) do
    #     String :street
    #     String :city
    #     String :zip
    #   end
    #
    # and a company table with an address:
    #
    #   DB.create_table(:company) do
    #     String :name
    #     address :address
    #   end
    # 
    # You can create a Sequel::Model for the address table, and load the plugin,
    # which registers the row type:
    #
    #   class Address < Sequel::Model(:address)
    #     plugin :pg_row
    #   end
    #
    # Then when you select from the company table (even using a plain dataset),
    # it will return address values as instances of Address:
    #
    #   DB[:company].first
    #   # => {:name=>'MS', :address=>
    #     Address.load(:street=>'123 Foo St', :city=>'Bar Town', :zip=>'12345')}
    #
    # If you want a lot of your models to be used as row types, you can load the
    # plugin into Sequel::Model itself:
    #
    #   Sequel::Model.plugin :pg_row
    #
    # And then call register_row_type in the class
    #
    #   Address.register_row_type
    #
    # Note that automatic conversion only works with the native postgres adapter.
    # For other adapters that connect to PostgreSQL, you need to call the conversion
    # proc manually.
    #
    # In addition to returning row-valued/composite types as instances of Sequel::Model,
    # this also lets you use model instances in datasets when inserting, updating, and
    # filtering:
    #
    #   DB[:company].insert(:name=>'MS', :address=>
    #     Address.load(:street=>'123 Foo St', :city=>'Bar Town', :zip=>'12345'))
    module PgRow
      # When loading the extension, make sure the database has the pg_row extension
      # loaded, load the custom database extensions, and automatically register the
      # row type if the model has a dataset.
      def self.configure(model)
        model.db.extension(:pg_row)
        model.db.extend(DatabaseMethods)
        model.register_row_type if model.instance_variable_get(:@dataset)
      end

      module DatabaseMethods
        ESCAPE_RE = /("|\\)/.freeze
        ESCAPE_REPLACEMENT = '\\\\\1'.freeze
        COMMA = ','

        # Handle Sequel::Model instances in bound variables.
        def bound_variable_arg(arg, conn)
          case arg
          when Sequel::Model
            "(#{arg.values.values_at(*arg.columns).map{|v| bound_variable_array(v)}.join(COMMA)})"
          else
            super
          end
        end

        # If a Sequel::Model instance is given, return it as-is
        # instead of attempting to convert it.
        def row_type(db_type, v)
          if v.is_a?(Sequel::Model)
            v
          else
            super
          end
        end

        private

        # Handle Sequel::Model instances in bound variable arrays.
        def bound_variable_array(arg)
          case arg
          when Sequel::Model
            "\"(#{arg.values.values_at(*arg.columns).map{|v| bound_variable_array(v)}.join(COMMA).gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)})\""
          else
            super
          end
        end
      end

      module ClassMethods
        # Register the model's row type with the database.
        def register_row_type
          db.register_row_type(model.table_name, :converter=>self, :typecaster=>method(:new))
        end
      end

      module InstanceMethods
        ROW = 'ROW'.freeze
        CAST = '::'.freeze

        # Literalize the model instance and append it to the sql.
        def sql_literal_append(ds, sql)
          sql << ROW
          ds.literal_append(sql, values.values_at(*columns))
          sql << CAST
          ds.quote_schema_table_append(sql, model.table_name)
        end
      end
    end
  end
end
