Skip to content
This repository has been archived by the owner on May 13, 2019. It is now read-only.

Commit

Permalink
Added some documentation, tightened visibility.
Browse files Browse the repository at this point in the history
Also added a couple more tests.
  • Loading branch information
matthewd committed Mar 8, 2009
1 parent d5bc576 commit 7476777
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 26 deletions.
1 change: 1 addition & 0 deletions lib/sqlstate.rb
Expand Up @@ -2,6 +2,7 @@
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))

class SqlState < StandardError
# The version of the sqlstate gem.
VERSION = '0.0.1'
end

Expand Down
12 changes: 12 additions & 0 deletions lib/sqlstate/core.rb
Expand Up @@ -4,10 +4,22 @@
class SqlState < StandardError
extend SqlStateRoot

# The SQLSTATE code this class is defined as representing. A given
# instance's +sql_state+ value may differ, if the instance is a
# representation of an unrecognised code.
SQL_STATE = 'HY000'

# The default message corresponding with this exception type; used if
# no message is supplied when creating a new exception instance.
MESSAGE = 'Unrecognised SQL Error'

# The SQLSTATE code represented by this exception
attr_reader :sql_state

# Creates a new instance of this exception class. Calling #create on
# the relevant "vendor root", such as SqlState::PostgresError, is the
# most effective means of creating an instance, as this gives the
# vendor root a chance to extend it with vendor-specific fields.
def initialize(sql_state=self.class::SQL_STATE, message=self.class::MESSAGE)
@sql_state = sql_state
super(message)
Expand Down
84 changes: 64 additions & 20 deletions lib/sqlstate/modules.rb
@@ -1,10 +1,26 @@

module SqlState::SqlStateClass
# A hash mapping SQLSTATE codes to the classes that represent them.
attr_reader :sqlstate_subclasses
attr_writer :sqlstate_prefix
def self.extended(target)
target.instance_variable_set :@sqlstate_subclasses, {}
private :sqlstate_subclasses

# The first two characters in the SQLSTATE code, which identifies the
# "class" of State.
attr_accessor :sqlstate_prefix
private :sqlstate_prefix, :sqlstate_prefix=

class << self
# Invoked when this module is added to a class; initializes the
# sqlstate_subclasses hash.
def extended(target) # :nodoc:
target.instance_variable_set :@sqlstate_subclasses, {}
end
end

# Defines a particular SQLSTATE code, underneath this class, given its
# description. The constant name will be generated from the
# description. If specified, +parent+ will be used as the superclass,
# otherwise, this class will be used as both parent and superclass.
def define(suffix, description, parent=nil)
parent ||= self
sql_state = (defined?(@sqlstate_prefix) ? @sqlstate_prefix : '') + suffix
Expand All @@ -17,55 +33,83 @@ def define(suffix, description, parent=nil)
root.const_set name, klass
sqlstate_subclasses[sql_state] = klass
end
protected :define

# Looks up the given SQLSTATE code, and returns the corresponding
# class if there is an exact match registered as a child of this
# class.
def subclass_for(sql_state)
sqlstate_subclasses[sql_state]
end
private :subclass_for
end

module SqlState::SqlStateRoot
include SqlState::SqlStateClass

def self.extended(target)
target.instance_variable_set :@sqlstate_subclasses, {}
end

def set_parent_root(v)
@parent_root = v
end
def parent_root
defined?(@parent_root) ?
@parent_root :
self == SqlState ?
nil :
SqlState
class << self
# Invoked when this module is added to a class; initializes the
# sqlstate_subclasses hash.
def extended(target) # :nodoc:
SqlState::SqlStateClass.extended(target)
end
end

# Returns the Ruby Class representing the SQLSTATE Class identified by
# the first two characters in the given SQLSTATE code.
def class_for(state)
sqlstate_subclasses[state[0, 2] + '000']
end
private :class_for

# Returns the Ruby Class that best represents the given SQLSTATE code,
# trying first for an exact match, then a match based on the Class
# (first two characters), then finally falling back to this class
# itself.
def for(sql_state)
subclass = subclass_for(sql_state)
subclass ||= class_for(sql_state).subclass_for(sql_state) if class_for(sql_state)
subclass ||= parent_root.for(sql_state) if parent_root
if klass = class_for(sql_state)
subclass ||= klass.send(:subclass_for, sql_state)
end
if superclass.respond_to?(:for)
subclass ||= superclass.for(sql_state)
end
subclass || self
end

# Creates an instance representing the given SQLSTATE code. Should
# generally be used in preference to calling #for then #new, because
# it can be overridden by a vendor subclass, to allow for additional,
# vendor-specific fields, even on standard errors.
def create(sql_state, *args)
self.for(sql_state).new(sql_state, *args)
end

# Defines a two-letter SQLSTATE class (and its corresponding 000
# generic state code). If a block is given, yields the newly created
# class -- extended with SqlState::SqlStateClass -- providing a simple
# way to define further state codes within this SQLSTATE class. The
# constant name will be generated from the description.
def define_class(prefix, description)
klass = define("#{prefix}000", description)
klass.send :extend, SqlState::SqlStateClass
klass.sqlstate_prefix = prefix
klass.send :sqlstate_prefix=, prefix
yield klass if block_given?
klass
end
protected :define_class

# Defines a particular SQLSTATE code, given its description. The
# constant name will be generated from the description. Will
# automatically locate the SQLSTATE class the given code should be
# defined underneath.
def define(sql_state, description)
parent = class_for(sql_state)
parent ||= parent_root && parent_root.class_for(sql_state)
if superclass.respond_to?(:class_for, true)
parent ||= superclass.send(:class_for, sql_state)
end
super(sql_state, description, parent)
end
protected :define
end

52 changes: 49 additions & 3 deletions lib/sqlstate/postgres.rb
Expand Up @@ -4,12 +4,58 @@
class SqlState::PostgresError < SqlState
extend SqlStateRoot

# The PostgreSQL-specific informational fields that are available from
# an SqlState exception that has been raised from PostgreSQL. This
# module is extended into all exceptions returned by
# SqlState::PostgresError#create.
#
# These fields are in addition to +message+, which should contain the
# single-line "primary message" from the PostgreSQL server.
#
# Any values not made available by the server should be left set to
# nil.
module Details
attr_accessor :details, :hint, :context
attr_accessor :source_file, :source_line, :source_function
attr_accessor :query, :query_position, :internal_query, :internal_position
# A potentially multi-line description, containing more specific
# information as to what caused the error. Contains only facts, not
# speculation.
attr_accessor :details

# A potentially multi-line suggestion of action that may be taken to
# resolve the issue; may be speculative.
attr_accessor :hint

attr_accessor :context


attr_accessor :source_file

attr_accessor :source_line

attr_accessor :source_function


# The user query that was executing at the time the error occurred.
attr_accessor :query

# A character (not byte) offset into the +query+, indicating
# specifically where to look for the error.
attr_accessor :query_position

# An internally constructed SQL query that was being evaluated on
# the server, as a result of the given user +query+, at the time of
# the error.
attr_accessor :internal_query

# A character (not byte) offset into the +internal_query+,
# indicating specifically where to look for the error.
attr_accessor :internal_position
end

# Creates an exception representing the given SQLSTATE code, as
# generated from PostgreSQL. Should be used in preference to calling
# #for then #new, because it adds PostgreSQL-specific fields (as
# defined in Details) to the returned exception -- even ones defined
# in the standard.
def self.create(sql_state, message=nil)
obj = super
obj.send(:extend, Details)
Expand Down
13 changes: 13 additions & 0 deletions test/test_postgres.rb
Expand Up @@ -7,6 +7,10 @@ def test_is_subclass_of_root
assert_operator SqlState::PostgresError, :<, SqlState
end

def test_name_of_subclass
assert_equal 'SqlState::PostgresError::DeadlockDetected', SqlState::PostgresError.for('40P01').name
end

def test_subclass_of_generic_class
assert_operator SqlState::PostgresError.for('40P01'), :<, SqlState::PostgresError.for('40000')
end
Expand All @@ -15,6 +19,10 @@ def test_custom_class_subclasses_vendor_root
assert_operator SqlState::PostgresError.for('P0001'), :<, SqlState::PostgresError
end

def test_standard_subclass_from_vendor_root_is_same_as_from_global_root
assert_equal SqlState::PostgresError.for('22012'), SqlState.for('22012')
end

def test_custom_subclass_doesnt_pollute_global_space
# Unknown state code returns root
assert_equal SqlState.for('40P01'), SqlState
Expand All @@ -37,4 +45,9 @@ def test_standard_subclass_doesnt_have_details_when_created_directly
assert !SqlState.create('22012').respond_to?(:details=)
end

def test_standard_subclass_is_recognised_when_created_through_postgres
assert_equal 'SqlState::DivisionByZero',
SqlState::PostgresError.for('22012').name
end

end
8 changes: 5 additions & 3 deletions test/test_sqlstate.rb
Expand Up @@ -3,9 +3,11 @@
class TestSqlstate < Test::Unit::TestCase

def test_defined_class_can_be_retrieved
root = SqlState.dup
root.define_class 'TT', 'Test Error Class'
assert_equal 'TT000', root.for('TT000')::SQL_STATE
assert_equal 'TT000',
SqlState.dup.class_eval {
define_class 'TT', 'Test Error Class'
self.for('TT000')::SQL_STATE
}
end

def test_for_returns_class
Expand Down

0 comments on commit 7476777

Please sign in to comment.