rdbi 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,121 @@
1
+ RDBI::Schema = Struct.new(
2
+ :columns,
3
+ :tables,
4
+ :type
5
+ )
6
+
7
+ RDBI::Column = Struct.new(
8
+ :name,
9
+ :type,
10
+ :ruby_type,
11
+ :precision,
12
+ :scale,
13
+ :nullable,
14
+ :metadata,
15
+ :default,
16
+ :table
17
+ )
18
+
19
+ #
20
+ # RDBI::Schema is the metadata representation of a single schema "object", such
21
+ # as the schema for a single table or the data queried against during
22
+ # RDBI::Statement execution.
23
+ #
24
+ # RDBI::Schema is the foundation for type management via RDBI::Type, and as a
25
+ # result an incomplete schema will lead to type inconsistency. As a result, it
26
+ # is *critical* that driver authors implement RDBI::Schema properly.
27
+ #
28
+ # RDBI::Schema is a core Struct underneath the hood and will respond accordingly.
29
+ #
30
+ class RDBI::Schema
31
+ ##
32
+ # :attr_reader: columns
33
+ #
34
+ # Array of RDBI::Column objects associated with this schema.
35
+ #
36
+
37
+ ##
38
+ # :attr_reader: tables
39
+ #
40
+ # Array of table names and views associated with this schema, represented as symbols.
41
+
42
+ ##
43
+ # :attr_reader: type
44
+ #
45
+ # In some instances, the type (freeform, String) may be provided as an
46
+ # optional hint as to what kind of schema this is.
47
+ #
48
+ end
49
+
50
+ #
51
+ # RDBI::Column is the metadata representation of a single table column. You
52
+ # will typically access this via RDBI::Schema.
53
+ #
54
+ # In tables, columns can represent the columns of the schema. In queries,
55
+ # columns can represent anything that identifies the column of a result set.
56
+ # This includes aggregates, other functions, dynamic queries, etc.
57
+ #
58
+ class RDBI::Column
59
+ ##
60
+ # :attr_reader: name
61
+ #
62
+ # The name of the column, as symbol.
63
+
64
+ ##
65
+ # :attr_reader: type
66
+ #
67
+ # The database-specific type, as symbol.
68
+
69
+ ##
70
+ # :attr_reader: ruby_type
71
+ #
72
+ # The ruby target type, as symbol. This is used by RDBI::Type to convert data
73
+ # in rows.
74
+
75
+ ##
76
+ # :attr_reader: precision
77
+ #
78
+ # The precision of the type. This is typically the first number in an
79
+ # extended type form, such as +NUMBER(1)+.
80
+ #
81
+ # Precisions are not always *really* precision and this depends on the type.
82
+ # Consult your database documentation for more information.
83
+ #
84
+
85
+ ##
86
+ # :attr_reader: scale
87
+ #
88
+ # The scale of the type. This is typically the second number in an extended
89
+ # type form, such as +NUMBER(10,2)+.
90
+ #
91
+ # As with precision, this may not *really* be scale and it is recommended you
92
+ # consult your database documentation for specific, especially non-numeric,
93
+ # types.
94
+ #
95
+
96
+ ##
97
+ # :attr_reader: nullable
98
+ #
99
+ # Boolean: does this column accept null?
100
+
101
+ ##
102
+ # :attr_reader: metadata
103
+ #
104
+ # Free-form field for driver authors to provide data that lives outside of
105
+ # this specification.
106
+
107
+ ##
108
+ # :attr_reader: default
109
+ #
110
+ # The value provided to the column when it is not specified but requested for
111
+ # use, such as in +INSERT+ statements.
112
+ #
113
+
114
+ ##
115
+ # :attr_reader: table
116
+ #
117
+ # The table this column belongs to, if known, as symbol.
118
+ #
119
+ end
120
+
121
+ # vim: syntax=ruby ts=2 et sw=2 sts=2
@@ -0,0 +1,204 @@
1
+ #
2
+ # RDBI::Statement is the encapsulation of a single prepared statement (query).
3
+ # A statement can be executed with varying arguments multiple times through a
4
+ # facility called 'binding'.
5
+ #
6
+ # == About Binding
7
+ #
8
+ # Binding is the database term for facilitating placeholder replacement similar
9
+ # to formatters such as "sprintf()", but in a database-centric way:
10
+ #
11
+ # select * from my_table where some_column = ?
12
+ #
13
+ # The question mark is the placeholder here; upon execution, the user will be
14
+ # asked to provide values to fill that placeholder with.
15
+ #
16
+ # There are two major advantages to binding:
17
+ #
18
+ # * Multiple execution of the same statement with variable data
19
+ #
20
+ # For example, the above statement could be executed 12 times over an iterator:
21
+ #
22
+ # # RDBI::Database#prepare creates a prepared statement
23
+ #
24
+ # sth = dbh.prepare('select * from my_table where some_column = ?')
25
+ #
26
+ # # there is one placeholder here, so we'll use the iterator itself to feed
27
+ # # to the statement at execution time.
28
+ # #
29
+ # # This will send 12 copies of the select statement above, with 0 - 11 being
30
+ # # passed as the substitution for each placeholder. Use
31
+ # # RDBI::Database#preprocess_query to see what these queries would look
32
+ # # like.
33
+ #
34
+ # 12.times do |x|
35
+ # sth.execute(x)
36
+ # end
37
+ #
38
+ # * Protection against attacks such as SQL injection in a consistent way (see below).
39
+ #
40
+ # == Native client binding
41
+ #
42
+ # Binding is typically *not* just text replacement, it is a client-oriented
43
+ # operation that barely involves itself in the string at all. The query is
44
+ # parsed by the SQL engine, then the placeholders are requested; at this point,
45
+ # the client yields those to the database which then uses them in the
46
+ # *internal* representation of the query, which is why this is totally legal in
47
+ # a binding scenario:
48
+ #
49
+ # # RDBI::Database#execute is a way to prepare and execute statements immediately.
50
+ # dbh.execute("select * from my_table where some_column = ?", "; drop table my_table;")
51
+ #
52
+ # For purposes of instruction, this resolves to:
53
+ #
54
+ # select * from my_table where some_column = '; drop table my_table;'
55
+ #
56
+ # *BUT*, as mentioned above, the query is actually sent in two stages. It gets this:
57
+ #
58
+ # select * from my_table where some_column = ?
59
+ #
60
+ # Then a single element tuple is sent:
61
+ #
62
+ # ["; drop table my_table;"]
63
+ #
64
+ # These are combined with *post-parsing* to create a full, legal statement, so
65
+ # no grammar rules can be exploited.
66
+ #
67
+ # As a result, placeholder rules in this scenario are pretty rigid, only values
68
+ # can be used. For example, you cannot supply placeholders for:
69
+ #
70
+ # * table names
71
+ # * sql keywords and functions
72
+ # * other elements of syntax (punctuation, etc)
73
+ #
74
+ # == Preprocessing
75
+ #
76
+ # Preprocessing is a fallback mechanism we use when the underlying database
77
+ # does not support the above mechanism. It, unlike native client binding, is
78
+ # basically text replacement, so all those rules about what you can and cannot
79
+ # do go away.
80
+ #
81
+ # The downside is that if our replacement system (provided by the Epoxy class,
82
+ # which itself is provided by the epoxy gem) is unkempt, SQL injection attacks
83
+ # may be possible.
84
+ #
85
+ class RDBI::Statement
86
+ extend MethLab
87
+
88
+ # the RDBI::Database handle that created this statement.
89
+ attr_reader :dbh
90
+ # The query this statement was created for.
91
+ attr_reader :query
92
+ # A mutex for locked operations. Basically a cached copy of Mutex.new.
93
+ attr_reader :mutex
94
+ # The input type map provided during statement creation -- used for binding.
95
+ attr_reader :input_type_map
96
+
97
+ ##
98
+ # :attr_reader: last_result
99
+ #
100
+ # The last RDBI::Result this statement yielded.
101
+ attr_threaded_accessor :last_result
102
+
103
+ ##
104
+ # :attr_reader: finished
105
+ #
106
+ # Has this statement been finished?
107
+
108
+ ##
109
+ # :attr_reader: finished?
110
+ #
111
+ # Has this statement been finished?
112
+
113
+ inline(:finished, :finished?) { @finished }
114
+
115
+ ##
116
+ # :attr_reader: driver
117
+ #
118
+ # The RDBI::Driver object that this statement belongs to.
119
+
120
+ inline(:driver) { dbh.driver }
121
+
122
+ #
123
+ # Initialize a statement handle, given a text query and the RDBI::Database
124
+ # handle that created it.
125
+ #
126
+ def initialize(query, dbh)
127
+ @query = query
128
+ @dbh = dbh
129
+ @mutex = Mutex.new
130
+ @finished = false
131
+ @input_type_map = RDBI::Type.create_type_hash(RDBI::Type::In)
132
+
133
+ @dbh.open_statements.push(self)
134
+ end
135
+
136
+ #
137
+ # Execute the statement with the supplied binds.
138
+ #
139
+ def execute(*binds)
140
+ raise StandardError, "you may not execute a finished handle" if @finished
141
+
142
+ # XXX if we ever support some kind of hash type, this'll get ugly.
143
+ hashes, binds = binds.partition { |x| x.kind_of?(Hash) }
144
+
145
+ if hashes
146
+ hashes.collect! do |hash|
147
+ newhash = { }
148
+
149
+ hash.each do |key, value|
150
+ newhash[key] = RDBI::Type::In.convert(value, @input_type_map)
151
+ end
152
+
153
+ newhash
154
+ end
155
+ end
156
+
157
+ binds = (hashes || []) + binds.collect { |x| RDBI::Type::In.convert(x, @input_type_map) }
158
+
159
+ mutex.synchronize do
160
+ exec_args = *new_execution(*binds)
161
+ self.last_result = RDBI::Result.new(self, binds, *exec_args)
162
+ end
163
+ end
164
+
165
+ #
166
+ # Deallocate any internal resources devoted to the statement. It will not be
167
+ # usable after this is called.
168
+ #
169
+ # Driver implementors will want to subclass this, do their thing and call
170
+ # 'super' as their last statement.
171
+ #
172
+ def finish
173
+ mutex.synchronize do
174
+ dbh.open_statements.reject! { |x| x.object_id == self.object_id }
175
+ @finished = true
176
+ end
177
+ end
178
+
179
+ ##
180
+ # :method: new_execution
181
+ # :call-seq: new_execution(*binds)
182
+ #
183
+ # Database drivers will override this method in their respective RDBI::Statement
184
+ # subclasses. This method is called when RDBI::Statement#execute or
185
+ # RDBI::Database#execute is called.
186
+ #
187
+ # Implementations of this method must return, in order:
188
+ #
189
+ # * A RDBI::Cursor object which encapsulates the result
190
+ # * a RDBI::Schema struct which represents the kinds of data being queried
191
+ # * a +type_hash+ for on-fetch conversion which corresponds to the
192
+ # RDBI::Column information (see RDBI::Schema) and follows a structure similar
193
+ # to RDBI::Type::Out
194
+ #
195
+ # These return values are passed (along with this object and the binds passed
196
+ # to this call) to RDBI::Result.new.
197
+ #
198
+
199
+ inline(:new_execution) do |*args|
200
+ raise NoMethodError, "this method is not implemented in this driver"
201
+ end
202
+ end
203
+
204
+ # vim: syntax=ruby ts=2 et sw=2 sts=2
@@ -0,0 +1,200 @@
1
+ require 'typelib'
2
+ require 'typelib/canned'
3
+
4
+ #
5
+ # == RDBI::Type -- manage types going to and coming from your database.
6
+ #
7
+ # RDBI::Type consists of:
8
+ #
9
+ # * Checks and Conversions (facilitated by TypeLib) for ruby -> database and
10
+ # database -> ruby
11
+ # * Mappings for Input (Ruby -> DB) and Output (DB -> Ruby) conversions based
12
+ # on type.
13
+ # * Convenience methods for TypeLib and creating new mappings.
14
+ #
15
+ # == How does it all work?
16
+ #
17
+ # RDBI::Type leverages +TypeLib+ which is a filter chaining system, one which
18
+ # you'll wish to read the documentation for to understand some of the concepts
19
+ # here.
20
+ #
21
+ # === A conversion follows these steps:
22
+ #
23
+ # * Metadata on the type (more below) is located and used to reference a
24
+ # TypeLib::FilterList which contains the TypeLib::Filters (which in turn
25
+ # consist of a +Check+ and +Conversion+ proc) which will process your data.
26
+ # * Data is passed to the FilterList and it is executed, following each filter
27
+ # in turn and following any conversion passing checks request. This may very
28
+ # well mean that no checks pass and therefore your original data is returned.
29
+ # * After processing, the data is yielded back to you for further processing
30
+ # (or a subsystem such as RDBI::Result#fetch and result drivers that take
31
+ # advantage of said data)
32
+ #
33
+ # === How is metadata located?
34
+ #
35
+ # It's important first to briefly describe how RDBI terms database I/O:
36
+ #
37
+ # * Binds going to the database proper are called 'input'.
38
+ # * Data coming from the database is called 'output'.
39
+ #
40
+ # Mappings are keyed by type metadata and thusly are typed as:
41
+ #
42
+ # * Input types are the native class of the type.
43
+ # * Output types are a symbol that represents the database type. These type
44
+ # names are provided by RDBI::Column via RDBI::Schema in the response from an
45
+ # execution. See RDBI::Statement#new_execution and RDBI::Column#ruby_type.
46
+ #
47
+ # Note that in the latter case these database types are effectively normalized,
48
+ # e.g., 'timestamp with timezone' in postgres is just +:timestamp+. You will
49
+ # want to read the mappings in the source to get a full listing of what's
50
+ # supported by default.
51
+ #
52
+ # Each map will also contain +:default+, which is what is used when a proper
53
+ # lookup fails, as a fallback.
54
+ #
55
+ # === Ok, so how do I use these maps?
56
+ #
57
+ # RDBI::Type.create_type_hash is a helper to duplicate the default maps and
58
+ # return them. If you don't wish to use the default maps at all, just a plain
59
+ # old +Hash+ following the semantics above will work.
60
+ #
61
+ # To perform conversions, look at RDBI::Type::In::convert and
62
+ # RDBI::Type::Out::convert.
63
+ #
64
+ module RDBI::Type
65
+ # A filter format to assist the conversions of DateTime objects.
66
+ DEFAULT_STRFTIME_FILTER = "%Y-%m-%d %H:%M:%S %z"
67
+
68
+ # Module for canned checks that are unique to RDBI. Includes the canned
69
+ # checks from TypeLib.
70
+ module Checks
71
+ include TypeLib::Canned::Checks
72
+
73
+ IS_NULL = proc { |obj| obj.nil? }
74
+ IS_BIGDECIMAL = proc { |obj| obj.kind_of?(BigDecimal) }
75
+ IS_DATETIME = proc { |obj| obj.kind_of?(DateTime) }
76
+ IS_BOOLEAN = proc { |obj| obj.kind_of?(TrueClass) or obj.kind_of?(FalseClass) }
77
+
78
+ STR_IS_BOOLEAN = proc { |obj| obj.kind_of?(String) and obj =~ /^(t(rue)?|f(alse)?|1|0)$/i }
79
+ end
80
+
81
+ # Module for canned conversions that are unique to RDBI. Includes the canned
82
+ # conversions from TypeLib.
83
+ module Conversions
84
+ include TypeLib::Canned::Conversions
85
+
86
+ TO_NULL = proc { |obj| nil }
87
+ TO_STRING_DECIMAL = proc { |obj| obj.to_s('F') }
88
+ TO_STRING_DATETIME = proc { |obj| obj.strftime(DEFAULT_STRFTIME_FILTER) }
89
+ TO_STRING_BOOLEAN = proc { |obj| obj ? 'TRUE' : 'FALSE' }
90
+
91
+ SQL_STR_TO_BOOLEAN = proc { |obj|
92
+ case obj
93
+ when /^(t(rue)?|1)$/i
94
+ true
95
+ when /^(f(alse)?|0)$/i
96
+ false
97
+ end
98
+ }
99
+ end
100
+
101
+ # Canned +TypeLib::Filter+ objects unique to RDBI to facilitate certain
102
+ # conversions. Includes TypeLib's canned filters.
103
+ module Filters
104
+ include TypeLib::Canned::Filters
105
+
106
+ NULL = TypeLib::Filter.new(Checks::IS_NULL, Conversions::TO_NULL)
107
+ FROM_INTEGER = TypeLib::Filter.new(Checks::IS_INTEGER, Conversions::TO_STRING)
108
+ FROM_NUMERIC = TypeLib::Filter.new(Checks::IS_NUMERIC, Conversions::TO_STRING)
109
+ FROM_DECIMAL = TypeLib::Filter.new(Checks::IS_BIGDECIMAL, Conversions::TO_STRING_DECIMAL)
110
+ FROM_DATETIME = TypeLib::Filter.new(Checks::IS_DATETIME, Conversions::TO_STRING_DATETIME)
111
+ FROM_BOOLEAN = TypeLib::Filter.new(Checks::IS_BOOLEAN, Conversions::TO_STRING_BOOLEAN)
112
+
113
+ TO_BOOLEAN = TypeLib::Filter.new(Checks::STR_IS_BOOLEAN, Conversions::SQL_STR_TO_BOOLEAN)
114
+ end
115
+
116
+ # Shorthand for creating a new +TypeLib::FilterList+.
117
+ def self.filterlist(*ary)
118
+ TypeLib::FilterList.new([Filters::NULL, *ary])
119
+ end
120
+
121
+ # Shorthand to duplicate the +DEFAULTS+ hash from a module. Most frequently
122
+ # used to get a copy of the RDBI::Type::In and RDBI::Type::Out type maps.
123
+ def self.create_type_hash(klass)
124
+ hash = { }
125
+ klass::DEFAULTS.each do |key, value|
126
+ flist = filterlist()
127
+ value.each do |filter|
128
+ flist << filter
129
+ end
130
+ hash[key] = flist
131
+ end
132
+
133
+ return hash
134
+ end
135
+
136
+ #
137
+ # The default output type map. As explained in RDBI::Type, these are keyed by
138
+ # symbol and are loosely related to the type, and are compared against the
139
+ # proper RDBI::Column object to figure out which filter to call.
140
+ #
141
+ module Out
142
+ DEFAULTS = {
143
+ :integer => RDBI::Type.filterlist(Filters::STR_TO_INT),
144
+ :decimal => RDBI::Type.filterlist(Filters::STR_TO_DEC),
145
+ :datetime => RDBI::Type.filterlist(TypeLib::Canned.build_strptime_filter(DEFAULT_STRFTIME_FILTER)),
146
+ :timestamp => RDBI::Type.filterlist(TypeLib::Canned.build_strptime_filter(DEFAULT_STRFTIME_FILTER)),
147
+ :boolean => RDBI::Type.filterlist(Filters::TO_BOOLEAN),
148
+ :default => RDBI::Type.filterlist()
149
+ }
150
+
151
+ #
152
+ # Perform a conversion. Accepts the object to convert, a RDBI::Column
153
+ # object, and a type map (a +Hash+).
154
+ #
155
+ def self.convert(obj, column, type_hash)
156
+ fl = type_hash[column.ruby_type]
157
+
158
+ unless fl
159
+ fl = type_hash[:default]
160
+ end
161
+
162
+ fl.execute(obj)
163
+ end
164
+ end
165
+
166
+ #
167
+ # The default input type map. As explained in RDBI::Type, these are keyed by
168
+ # the Ruby type with the exception of +:default+ which is a fallback
169
+ # conversion. RDBI::Statement subclassers will normally provide this object
170
+ # via +@input_type_map+ at construction time.
171
+ #
172
+ module In
173
+ DEFAULTS = {
174
+ Integer => RDBI::Type.filterlist(Filters::FROM_INTEGER),
175
+ Fixnum => RDBI::Type.filterlist(Filters::FROM_INTEGER),
176
+ Float => RDBI::Type.filterlist(Filters::FROM_NUMERIC),
177
+ BigDecimal => RDBI::Type.filterlist(Filters::FROM_DECIMAL),
178
+ DateTime => RDBI::Type.filterlist(Filters::FROM_DATETIME),
179
+ TrueClass => RDBI::Type.filterlist(Filters::FROM_BOOLEAN),
180
+ FalseClass => RDBI::Type.filterlist(Filters::FROM_BOOLEAN),
181
+ :default => RDBI::Type.filterlist()
182
+ }
183
+
184
+ #
185
+ # Perform a conversion. Accepts the object to convert and a type map (a
186
+ # +Hash+).
187
+ #
188
+ def self.convert(obj, type_hash)
189
+ fl = type_hash[obj.class]
190
+
191
+ unless fl
192
+ fl = type_hash[:default]
193
+ end
194
+
195
+ fl.execute(obj)
196
+ end
197
+ end
198
+ end
199
+
200
+ # vim: syntax=ruby ts=2 et sw=2 sts=2