module Sequel class Database # --------------------- # :section: 7 - Miscellaneous methods # These methods don't fit neatly into another category. # --------------------- # Converts a uri to an options hash. These options are then passed # to a newly created database object. def self.uri_to_options(uri) { :user => uri.user, :password => uri.password, :host => uri.host, :port => uri.port, :database => (m = /\/(.*)/.match(uri.path)) && (m[1]) } end private_class_method :uri_to_options # The options hash for this database attr_reader :opts # Set the timezone to use for this database, overridding Sequel.database_timezone. attr_writer :timezone # Constructs a new instance of a database connection with the specified # options hash. # # Accepts the following options: # :default_schema :: The default schema to use, should generally be nil # :disconnection_proc :: A proc used to disconnect the connection # :identifier_input_method :: A string method symbol to call on identifiers going into the database # :identifier_output_method :: A string method symbol to call on identifiers coming from the database # :logger :: A specific logger to use # :loggers :: An array of loggers to use # :quote_identifiers :: Whether to quote identifiers # :servers :: A hash specifying a server/shard specific options, keyed by shard symbol # :single_threaded :: Whether to use a single-threaded connection pool # :sql_log_level :: Method to use to log SQL to a logger, :info by default. # # All options given are also passed to the connection pool. If a block # is given, it is used as the connection_proc for the ConnectionPool. def initialize(opts = {}, &block) @opts ||= opts @opts = connection_pool_default_options.merge(@opts) @loggers = Array(@opts[:logger]) + Array(@opts[:loggers]) self.log_warn_duration = @opts[:log_warn_duration] @opts[:disconnection_proc] ||= proc{|conn| disconnect_connection(conn)} block ||= proc{|server| connect(server)} @opts[:servers] = {} if @opts[:servers].is_a?(String) @opts[:adapter_class] = self.class @opts[:single_threaded] = @single_threaded = typecast_value_boolean(@opts.fetch(:single_threaded, @@single_threaded)) @schemas = {} @default_schema = @opts.fetch(:default_schema, default_schema_default) @prepared_statements = {} @transactions = {} @identifier_input_method = nil @identifier_output_method = nil @quote_identifiers = nil @timezone = nil @dataset_class = dataset_class_default @dataset_modules = [] self.sql_log_level = @opts[:sql_log_level] ? @opts[:sql_log_level].to_sym : :info @pool = ConnectionPool.get_pool(@opts, &block) ::Sequel::DATABASES.push(self) end # If a transaction is not currently in process, yield to the block immediately. # Otherwise, add the block to the list of blocks to call after the currently # in progress transaction commits (and only if it commits). # Options: # :server :: The server/shard to use. def after_commit(opts={}, &block) raise Error, "must provide block to after_commit" unless block synchronize(opts[:server]) do |conn| if h = @transactions[conn] raise Error, "cannot call after_commit in a prepared transaction" if h[:prepare] (h[:after_commit] ||= []) << block else yield end end end # If a transaction is not currently in progress, ignore the block. # Otherwise, add the block to the list of the blocks to call after the currently # in progress transaction rolls back (and only if it rolls back). # Options: # :server :: The server/shard to use. def after_rollback(opts={}, &block) raise Error, "must provide block to after_rollback" unless block synchronize(opts[:server]) do |conn| if h = @transactions[conn] raise Error, "cannot call after_rollback in a prepared transaction" if h[:prepare] (h[:after_rollback] ||= []) << block end end end # Cast the given type to a literal type # # DB.cast_type_literal(Float) # double precision # DB.cast_type_literal(:foo) # foo def cast_type_literal(type) type_literal(:type=>type) end # Convert the given timestamp from the application's timezone, # to the databases's timezone or the default database timezone if # the database does not have a timezone. def from_application_timestamp(v) Sequel.convert_output_timestamp(v, timezone) end # Return true if already in a transaction given the options, # false otherwise. Respects the :server option for selecting # a shard. def in_transaction?(opts={}) synchronize(opts[:server]){|conn| !!@transactions[conn]} end # Returns a string representation of the database object including the # class name and connection URI and options used when connecting (if any). def inspect a = [] a << uri.inspect if uri if (oo = opts[:orig_opts]) && !oo.empty? a << oo.inspect end "#<#{self.class}: #{a.join(' ')}>" end # Proxy the literal call to the dataset. # # DB.literal(1) # 1 # DB.literal(:a) # a # DB.literal('a') # 'a' def literal(v) schema_utility_dataset.literal(v) end # Default serial primary key options, used by the table creation # code. def serial_primary_key_options {:primary_key => true, :type => Integer, :auto_increment => true} end # Whether the database supports CREATE TABLE IF NOT EXISTS syntax, # false by default. def supports_create_table_if_not_exists? false end # Whether the database supports DROP TABLE IF EXISTS syntax, # default is the same as #supports_create_table_if_not_exists?. def supports_drop_table_if_exists? supports_create_table_if_not_exists? end # Whether the database and adapter support prepared transactions # (two-phase commit), false by default. def supports_prepared_transactions? false end # Whether the database and adapter support savepoints, false by default. def supports_savepoints? false end # Whether the database and adapter support savepoints inside prepared transactions # (two-phase commit), default is false. def supports_savepoints_in_prepared_transactions? supports_prepared_transactions? && supports_savepoints? end # Whether the database and adapter support transaction isolation levels, false by default. def supports_transaction_isolation_levels? false end # The timezone to use for this database, defaulting to Sequel.database_timezone. def timezone @timezone || Sequel.database_timezone end # Convert the given timestamp to the application's timezone, # from the databases's timezone or the default database timezone if # the database does not have a timezone. def to_application_timestamp(v) Sequel.convert_timestamp(v, timezone) end # Typecast the value to the given column_type. Calls # typecast_value_#{column_type} if the method exists, # otherwise returns the value. # This method should raise Sequel::InvalidValue if assigned value # is invalid. def typecast_value(column_type, value) return nil if value.nil? meth = "typecast_value_#{column_type}" begin respond_to?(meth, true) ? send(meth, value) : value rescue ArgumentError, TypeError => e raise Sequel.convert_exception_class(e, InvalidValue) end end # Returns the URI use to connect to the database. If a URI # was not used when connecting, returns nil. def uri opts[:uri] end # Explicit alias of uri for easier subclassing. def url uri end private # Returns true when the object is considered blank. # The only objects that are blank are nil, false, # strings with all whitespace, and ones that respond # true to empty? def blank_object?(obj) return obj.blank? if obj.respond_to?(:blank?) case obj when NilClass, FalseClass true when Numeric, TrueClass false when String obj.strip.empty? else obj.respond_to?(:empty?) ? obj.empty? : false end end # Which transaction errors to translate, blank by default. def database_error_classes [] end # Return true if exception represents a disconnect error, false otherwise. def disconnect_error?(exception, opts) opts[:disconnect] end # Convert the given exception to a DatabaseError, keeping message # and traceback. def raise_error(exception, opts={}) if !opts[:classes] || Array(opts[:classes]).any?{|c| exception.is_a?(c)} raise Sequel.convert_exception_class(exception, disconnect_error?(exception, opts) ? DatabaseDisconnectError : DatabaseError) else raise exception end end # Typecast the value to an SQL::Blob def typecast_value_blob(value) value.is_a?(Sequel::SQL::Blob) ? value : Sequel::SQL::Blob.new(value) end # Typecast the value to true, false, or nil def typecast_value_boolean(value) case value when false, 0, "0", /\Af(alse)?\z/i, /\Ano?\z/i false else blank_object?(value) ? nil : true end end # Typecast the value to a Date def typecast_value_date(value) case value when DateTime, Time Date.new(value.year, value.month, value.day) when Date value when String Sequel.string_to_date(value) when Hash Date.new(*[:year, :month, :day].map{|x| (value[x] || value[x.to_s]).to_i}) else raise InvalidValue, "invalid value for Date: #{value.inspect}" end end # Typecast the value to a DateTime or Time depending on Sequel.datetime_class def typecast_value_datetime(value) Sequel.typecast_to_application_timestamp(value) end # Typecast the value to a BigDecimal def typecast_value_decimal(value) case value when BigDecimal value when String, Numeric BigDecimal.new(value.to_s) else raise InvalidValue, "invalid value for BigDecimal: #{value.inspect}" end end # Typecast the value to a Float def typecast_value_float(value) Float(value) end # Used for checking/removing leading zeroes from strings so they don't get # interpreted as octal. LEADING_ZERO_RE = /\A0+(\d)/.freeze if RUBY_VERSION >= '1.9' # Typecast the value to an Integer def typecast_value_integer(value) (value.is_a?(String) && value =~ LEADING_ZERO_RE) ? Integer(value, 10) : Integer(value) end else # Replacement string when replacing leading zeroes. LEADING_ZERO_REP = "\\1".freeze # Typecast the value to an Integer def typecast_value_integer(value) Integer(value.is_a?(String) ? value.sub(LEADING_ZERO_RE, LEADING_ZERO_REP) : value) end end # Typecast the value to a String def typecast_value_string(value) value.to_s end # Typecast the value to a Time def typecast_value_time(value) case value when Time if value.is_a?(SQLTime) value else SQLTime.create(value.hour, value.min, value.sec, value.respond_to?(:nsec) ? value.nsec/1000.0 : value.usec) end when String Sequel.string_to_time(value) when Hash SQLTime.create(*[:hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i}) else raise Sequel::InvalidValue, "invalid value for Time: #{value.inspect}" end end end end