# frozen-string-literal: true module Sequel class Database # --------------------- # :section: 7 - Miscellaneous methods # These methods don't fit neatly into another category. # --------------------- # Hash of extension name symbols to callable objects to load the extension # into the Database object (usually by extending it with a module defined # in the extension). EXTENSIONS = {} # The general default size for string columns for all Sequel::Database # instances. DEFAULT_STRING_COLUMN_SIZE = 255 # Empty exception regexp to class map, used by default if Sequel doesn't # have specific support for the database in use. DEFAULT_DATABASE_ERROR_REGEXPS = {}.freeze # Mapping of schema type symbols to class or arrays of classes for that # symbol. SCHEMA_TYPE_CLASSES = {:string=>String, :integer=>Integer, :date=>Date, :datetime=>[Time, DateTime].freeze, :time=>Sequel::SQLTime, :boolean=>[TrueClass, FalseClass].freeze, :float=>Float, :decimal=>BigDecimal, :blob=>Sequel::SQL::Blob}.freeze # :nocov: URI_PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER # :nocov: private_constant :URI_PARSER # Nested hook Proc; each new hook Proc just wraps the previous one. @initialize_hook = proc{|db| } # Register a hook that will be run when a new Database is instantiated. It is # called with the new database handle. def self.after_initialize(&block) raise Error, "must provide block to after_initialize" unless block Sequel.synchronize do previous = @initialize_hook @initialize_hook = proc do |db| previous.call(db) block.call(db) end end end # Apply an extension to all Database objects created in the future. def self.extension(*extensions) after_initialize{|db| db.extension(*extensions)} end # Register an extension callback for Database objects. ext should be the # extension name symbol, and mod should either be a Module that the # database is extended with, or a callable object called with the database # object. If mod is not provided, a block can be provided and is treated # as the mod object. def self.register_extension(ext, mod=nil, &block) if mod raise(Error, "cannot provide both mod and block to Database.register_extension") if block if mod.is_a?(Module) block = proc{|db| db.extend(mod)} else block = mod end end Sequel.synchronize{EXTENSIONS[ext] = block} end # Run the after_initialize hook for the given +instance+. def self.run_after_initialize(instance) @initialize_hook.call(instance) end # 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, :port => uri.port, :host => uri.hostname, :database => (m = /\/(.*)/.match(uri.path)) && (m[1]) } end private_class_method :uri_to_options def self.options_from_uri(uri) uri_options = uri_to_options(uri) uri.query.split('&').map{|s| s.split('=')}.each{|k,v| uri_options[k.to_sym] = v if k && !k.empty?} unless uri.query.to_s.strip.empty? uri_options.to_a.each{|k,v| uri_options[k] = URI_PARSER.unescape(v) if v.is_a?(String)} uri_options end private_class_method :options_from_uri # The options hash for this database attr_reader :opts # Set the timezone to use for this database, overridding Sequel.database_timezone. attr_writer :timezone # The specific default size of string columns for this Sequel::Database, usually 255 by default. attr_accessor :default_string_column_size # Whether to check the bytesize of strings before typecasting (to avoid typecasting strings that # would be too long for the given type), true by default. Strings that are too long will raise # a typecasting error. attr_accessor :check_string_typecast_bytesize # Constructs a new instance of a database connection with the specified # options hash. # # Accepts the following options: # :after_connect :: A callable object called after each new connection is made, with the # connection object (and server argument if the callable accepts 2 arguments), # useful for customizations that you want to apply to all connections. # :before_preconnect :: Callable that runs after extensions from :preconnect_extensions are loaded, # but before any connections are created. # :cache_schema :: Whether schema should be cached for this Database instance # :check_string_typecast_bytesize :: Whether to check the bytesize of strings before typecasting. # :connect_sqls :: An array of sql strings to execute on each new connection, after :after_connect runs. # :connect_opts_proc :: Callable object for modifying options hash used when connecting, designed for # cases where the option values (e.g. password) are automatically rotated on # a regular basis without involvement from the application using Sequel. # :default_string_column_size :: The default size of string columns, 255 by default. # :extensions :: Extensions to load into this Database instance. Can be a symbol, array of symbols, # or string with extensions separated by columns. These extensions are loaded after # connections are made by the :preconnect option. # :keep_reference :: Whether to keep a reference to this instance in Sequel::DATABASES, true by default. # :logger :: A specific logger to use. # :loggers :: An array of loggers to use. # :log_connection_info :: Whether connection information should be logged when logging queries. # :log_warn_duration :: The number of elapsed seconds after which queries should be logged at warn level. # :name :: A name to use for the Database object, displayed in PoolTimeout. # :preconnect :: Automatically create the maximum number of connections, so that they don't # need to be created as needed. This is useful when connecting takes a long time # and you want to avoid possible latency during runtime. # Set to :concurrently to create the connections in separate threads. Otherwise # they'll be created sequentially. # :preconnect_extensions :: Similar to the :extensions option, but loads the extensions before the # connections are made by the :preconnect option. # :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. # # For sharded connection pools, :after_connect and :connect_sqls can be specified per-shard. # # All options given are also passed to the connection pool. Additional options respected by # the connection pool are :max_connections, :pool_timeout, :servers, and :servers_hash. See the # connection pool documentation for details. def initialize(opts = OPTS) @opts ||= opts @opts = connection_pool_default_options.merge(@opts) @loggers = Array(@opts[:logger]) + Array(@opts[:loggers]) @opts[:servers] = {} if @opts[:servers].is_a?(String) @sharded = !!@opts[:servers] @opts[:adapter_class] = self.class @opts[:single_threaded] = @single_threaded = typecast_value_boolean(@opts.fetch(:single_threaded, Sequel.single_threaded)) @default_string_column_size = @opts[:default_string_column_size] || DEFAULT_STRING_COLUMN_SIZE @check_string_typecast_bytesize = typecast_value_boolean(@opts.fetch(:check_string_typecast_bytesize, true)) @schemas = {} @prepared_statements = {} @transactions = {} @transactions.compare_by_identity @symbol_literal_cache = {} @timezone = nil @dataset_class = dataset_class_default @cache_schema = typecast_value_boolean(@opts.fetch(:cache_schema, true)) @dataset_modules = [] @loaded_extensions = [] @schema_type_classes = SCHEMA_TYPE_CLASSES.dup self.sql_log_level = @opts[:sql_log_level] ? @opts[:sql_log_level].to_sym : :info self.log_warn_duration = @opts[:log_warn_duration] self.log_connection_info = typecast_value_boolean(@opts[:log_connection_info]) @pool = ConnectionPool.get_pool(self, @opts) reset_default_dataset adapter_initialize keep_reference = typecast_value_boolean(@opts[:keep_reference]) != false begin Sequel.synchronize{::Sequel::DATABASES.push(self)} if keep_reference Sequel::Database.run_after_initialize(self) initialize_load_extensions(:preconnect_extensions) if before_preconnect = @opts[:before_preconnect] before_preconnect.call(self) end if typecast_value_boolean(@opts[:preconnect]) && @pool.respond_to?(:preconnect, true) concurrent = typecast_value_string(@opts[:preconnect]) == "concurrently" @pool.send(:preconnect, concurrent) end initialize_load_extensions(:extensions) test_connection if typecast_value_boolean(@opts.fetch(:test, true)) && respond_to?(:connect, true) rescue Sequel.synchronize{::Sequel::DATABASES.delete(self)} if keep_reference raise end end # Freeze internal data structures for the Database instance. def freeze valid_connection_sql metadata_dataset @opts.freeze @loggers.freeze @pool.freeze @dataset_class.freeze @dataset_modules.freeze @schema_type_classes.freeze @loaded_extensions.freeze metadata_dataset super end # Disallow dup/clone for Database instances undef_method :dup, :clone, :initialize_copy # :nocov: if RUBY_VERSION >= '1.9.3' # :nocov: undef_method :initialize_clone, :initialize_dup 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 # Load an extension into the receiver. In addition to requiring the extension file, this # also modifies the database to work with the extension (usually extending it with a # module defined in the extension file). If no related extension file exists or the # extension does not have specific support for Database objects, an Error will be raised. # Returns self. def extension(*exts) exts.each do |ext| unless pr = Sequel.synchronize{EXTENSIONS[ext]} Sequel.extension(ext) pr = Sequel.synchronize{EXTENSIONS[ext]} end if pr if Sequel.synchronize{@loaded_extensions.include?(ext) ? false : (@loaded_extensions << ext)} pr.call(self) end else raise(Error, "Extension #{ext} does not have specific support handling individual databases (try: Sequel.extension #{ext.inspect})") end end self 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 # Returns a string representation of the Database object, including # the database type, host, database, and user, if present. def inspect s = String.new s << "#<#{self.class}" s << " database_type=#{database_type}" if database_type && database_type != adapter_scheme keys = [:host, :database, :user] opts = self.opts if !keys.any?{|k| opts[k]} && opts[:uri] opts = self.class.send(:options_from_uri, URI.parse(opts[:uri])) end keys.each do |key| val = opts[key] if val && val != '' s << " #{key}=#{val}" end end s << ">" end # Proxy the literal call to the dataset. # # DB.literal(1) # 1 # DB.literal(:a) # "a" # or `a`, [a], or a, depending on identifier quoting # DB.literal("a") # 'a' def literal(v) schema_utility_dataset.literal(v) end # Return the literalized version of the symbol if cached, or # nil if it is not cached. def literal_symbol(sym) Sequel.synchronize{@symbol_literal_cache[sym]} end # Set the cached value of the literal symbol. def literal_symbol_set(sym, lit) Sequel.synchronize{@symbol_literal_cache[sym] = lit} end # Synchronize access to the prepared statements cache. def prepared_statement(name) Sequel.synchronize{prepared_statements[name]} end # Proxy the quote_identifier method to the dataset, # useful for quoting unqualified identifiers for use # outside of datasets. def quote_identifier(v) schema_utility_dataset.quote_identifier(v) end # Return ruby class or array of classes for the given type symbol. def schema_type_class(type) @schema_type_classes[type] 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 # Cache the prepared statement object at the given name. def set_prepared_statement(name, ps) Sequel.synchronize{prepared_statements[name] = ps} end # Whether this database instance uses multiple servers, either for sharding # or for primary/replica configurations. def sharded? @sharded 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 # Allow calling private methods as per-type typecasting methods are private 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 # Per adapter initialization method, empty by default. def adapter_initialize end # 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 # An enumerable yielding pairs of regexps and exception classes, used # to match against underlying driver exception messages in # order to raise a more specific Sequel::DatabaseError subclass. def database_error_regexps DEFAULT_DATABASE_ERROR_REGEXPS end # Return the Sequel::DatabaseError subclass to wrap the given # exception in. def database_error_class(exception, opts) database_specific_error_class(exception, opts) || DatabaseError end # Return the SQLState for the given exception, if one can be determined def database_exception_sqlstate(exception, opts) nil end # Return a specific Sequel::DatabaseError exception class if # one is appropriate for the underlying exception, # or nil if there is no specific exception class. def database_specific_error_class(exception, opts) return DatabaseDisconnectError if disconnect_error?(exception, opts) if sqlstate = database_exception_sqlstate(exception, opts) if klass = database_specific_error_class_from_sqlstate(sqlstate) return klass end else database_error_regexps.each do |regexp, klss| return klss if exception.message =~ regexp end end nil end NOT_NULL_CONSTRAINT_SQLSTATES = %w'23502'.freeze.each(&:freeze) FOREIGN_KEY_CONSTRAINT_SQLSTATES = %w'23503 23506 23504'.freeze.each(&:freeze) UNIQUE_CONSTRAINT_SQLSTATES = %w'23505'.freeze.each(&:freeze) CHECK_CONSTRAINT_SQLSTATES = %w'23513 23514'.freeze.each(&:freeze) SERIALIZATION_CONSTRAINT_SQLSTATES = %w'40001'.freeze.each(&:freeze) # Given the SQLState, return the appropriate DatabaseError subclass. def database_specific_error_class_from_sqlstate(sqlstate) case sqlstate when *NOT_NULL_CONSTRAINT_SQLSTATES NotNullConstraintViolation when *FOREIGN_KEY_CONSTRAINT_SQLSTATES ForeignKeyConstraintViolation when *UNIQUE_CONSTRAINT_SQLSTATES UniqueConstraintViolation when *CHECK_CONSTRAINT_SQLSTATES CheckConstraintViolation when *SERIALIZATION_CONSTRAINT_SQLSTATES SerializationFailure end end # Return true if exception represents a disconnect error, false otherwise. def disconnect_error?(exception, opts) opts[:disconnect] end # Load extensions during initialization from the given key in opts. def initialize_load_extensions(key) case exts = @opts[key] when String extension(*exts.split(',').map(&:to_sym)) when Array extension(*exts) when Symbol extension(exts) when nil # nothing else raise Error, "unsupported Database #{key.inspect} option: #{@opts[key].inspect}" end end # Convert the given exception to an appropriate Sequel::DatabaseError # subclass, keeping message and backtrace. def raise_error(exception, opts=OPTS) if !opts[:classes] || Array(opts[:classes]).any?{|c| exception.is_a?(c)} raise Sequel.convert_exception_class(exception, database_error_class(exception, opts)) else raise exception end end # Swallow database errors, unless they are connect/disconnect errors. def swallow_database_error yield rescue Sequel::DatabaseDisconnectError, DatabaseConnectionError # Always raise disconnect errors raise rescue Sequel::DatabaseError # Don't raise other database errors. nil # else # Don't rescue other exceptions, they will be raised normally. end # Check the bytesize of a string before conversion. There is no point # trying to typecast strings that would be way too long. def typecast_check_string_length(string, max_size) if @check_string_typecast_bytesize && string.bytesize > max_size raise InvalidValue, "string too long to typecast (bytesize: #{string.bytesize}, max: #{max_size})" end string end # Check the bytesize of the string value, if value is a string. def typecast_check_length(value, max_size) typecast_check_string_length(value, max_size) if String === value value 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(typecast_check_string_length(value, 100)) when Hash Date.new(*[:year, :month, :day].map{|x| typecast_check_length(value[x] || value[x.to_s], 100).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) case value when String Sequel.typecast_to_application_timestamp(typecast_check_string_length(value, 100)) when Hash [:year, :month, :day, :hour, :minute, :second, :nanos, :offset].each do |x| typecast_check_length(value[x] || value[x.to_s], 100) end Sequel.typecast_to_application_timestamp(value) else Sequel.typecast_to_application_timestamp(value) end end if RUBY_VERSION >= '2.4' # Typecast a string to a BigDecimal alias _typecast_value_string_to_decimal BigDecimal else # :nocov: def _typecast_value_string_to_decimal(value) d = BigDecimal(value) if d.zero? # BigDecimal parsing is loose by default, returning a 0 value for # invalid input. If a zero value is received, use Float to check # for validity. begin Float(value) rescue ArgumentError raise InvalidValue, "invalid value for BigDecimal: #{value.inspect}" end end d end # :nocov: end # Typecast the value to a BigDecimal def typecast_value_decimal(value) case value when BigDecimal value when Numeric BigDecimal(value.to_s) when String _typecast_value_string_to_decimal(typecast_check_string_length(value, 1000)) else raise InvalidValue, "invalid value for BigDecimal: #{value.inspect}" end end # Typecast the value to a Float def typecast_value_float(value) Float(typecast_check_length(value, 1000)) end # Typecast the value to an Integer def typecast_value_integer(value) case value when String typecast_check_string_length(value, 100) if value =~ /\A-?0+(\d)/ Integer(value, 10) else Integer(value) end else Integer(value) end end # Typecast the value to a String def typecast_value_string(value) case value when Hash, Array raise Sequel::InvalidValue, "invalid value for String: #{value.inspect}" else value.to_s end 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.nsec/1000.0) end when String Sequel.string_to_time(typecast_check_string_length(value, 100)) when Hash SQLTime.create(*[:hour, :minute, :second].map{|x| typecast_check_length(value[x] || value[x.to_s], 100).to_i}) else raise Sequel::InvalidValue, "invalid value for Time: #{value.inspect}" end end end end