sequel_core 1.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.
- data/CHANGELOG +1003 -0
- data/COPYING +18 -0
- data/README +81 -0
- data/Rakefile +176 -0
- data/bin/sequel +41 -0
- data/lib/sequel_core.rb +59 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +68 -0
- data/lib/sequel_core/adapters/ado.rb +100 -0
- data/lib/sequel_core/adapters/db2.rb +158 -0
- data/lib/sequel_core/adapters/dbi.rb +126 -0
- data/lib/sequel_core/adapters/informix.rb +87 -0
- data/lib/sequel_core/adapters/jdbc.rb +108 -0
- data/lib/sequel_core/adapters/mysql.rb +269 -0
- data/lib/sequel_core/adapters/odbc.rb +145 -0
- data/lib/sequel_core/adapters/odbc_mssql.rb +93 -0
- data/lib/sequel_core/adapters/openbase.rb +90 -0
- data/lib/sequel_core/adapters/oracle.rb +99 -0
- data/lib/sequel_core/adapters/postgres.rb +519 -0
- data/lib/sequel_core/adapters/sqlite.rb +192 -0
- data/lib/sequel_core/array_keys.rb +296 -0
- data/lib/sequel_core/connection_pool.rb +152 -0
- data/lib/sequel_core/core_ext.rb +59 -0
- data/lib/sequel_core/core_sql.rb +191 -0
- data/lib/sequel_core/database.rb +433 -0
- data/lib/sequel_core/dataset.rb +409 -0
- data/lib/sequel_core/dataset/convenience.rb +321 -0
- data/lib/sequel_core/dataset/sequelizer.rb +354 -0
- data/lib/sequel_core/dataset/sql.rb +586 -0
- data/lib/sequel_core/exceptions.rb +45 -0
- data/lib/sequel_core/migration.rb +191 -0
- data/lib/sequel_core/model.rb +8 -0
- data/lib/sequel_core/pretty_table.rb +73 -0
- data/lib/sequel_core/schema.rb +8 -0
- data/lib/sequel_core/schema/schema_generator.rb +131 -0
- data/lib/sequel_core/schema/schema_sql.rb +131 -0
- data/lib/sequel_core/worker.rb +58 -0
- data/spec/adapters/informix_spec.rb +139 -0
- data/spec/adapters/mysql_spec.rb +330 -0
- data/spec/adapters/oracle_spec.rb +130 -0
- data/spec/adapters/postgres_spec.rb +189 -0
- data/spec/adapters/sqlite_spec.rb +345 -0
- data/spec/array_keys_spec.rb +679 -0
- data/spec/connection_pool_spec.rb +356 -0
- data/spec/core_ext_spec.rb +67 -0
- data/spec/core_sql_spec.rb +301 -0
- data/spec/database_spec.rb +812 -0
- data/spec/dataset_spec.rb +2381 -0
- data/spec/migration_spec.rb +261 -0
- data/spec/pretty_table_spec.rb +66 -0
- data/spec/rcov.opts +4 -0
- data/spec/schema_generator_spec.rb +86 -0
- data/spec/schema_spec.rb +230 -0
- data/spec/sequelizer_spec.rb +448 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/worker_spec.rb +96 -0
- metadata +162 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
# A ConnectionPool manages access to database connections by keeping
|
5
|
+
# multiple connections and giving threads exclusive access to each
|
6
|
+
# connection.
|
7
|
+
class ConnectionPool
|
8
|
+
attr_reader :mutex
|
9
|
+
|
10
|
+
# The maximum number of connections.
|
11
|
+
attr_reader :max_size
|
12
|
+
|
13
|
+
# The proc used to create a new connection.
|
14
|
+
attr_accessor :connection_proc
|
15
|
+
|
16
|
+
attr_reader :available_connections, :allocated, :created_count
|
17
|
+
|
18
|
+
# Constructs a new pool with a maximum size. If a block is supplied, it
|
19
|
+
# is used to create new connections as they are needed.
|
20
|
+
#
|
21
|
+
# pool = ConnectionPool.new(10) {MyConnection.new(opts)}
|
22
|
+
#
|
23
|
+
# The connection creation proc can be changed at any time by assigning a
|
24
|
+
# Proc to pool#connection_proc.
|
25
|
+
#
|
26
|
+
# pool = ConnectionPool.new(10)
|
27
|
+
# pool.connection_proc = proc {MyConnection.new(opts)}
|
28
|
+
def initialize(max_size = 4, &block)
|
29
|
+
@max_size = max_size
|
30
|
+
@mutex = Mutex.new
|
31
|
+
@connection_proc = block
|
32
|
+
|
33
|
+
@available_connections = []
|
34
|
+
@allocated = {}
|
35
|
+
@created_count = 0
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the number of created connections.
|
39
|
+
def size
|
40
|
+
@created_count
|
41
|
+
end
|
42
|
+
|
43
|
+
# Assigns a connection to the current thread, yielding the connection
|
44
|
+
# to the supplied block.
|
45
|
+
#
|
46
|
+
# pool.hold {|conn| conn.execute('DROP TABLE posts')}
|
47
|
+
#
|
48
|
+
# Pool#hold is re-entrant, meaning it can be called recursively in
|
49
|
+
# the same thread without blocking.
|
50
|
+
#
|
51
|
+
# If no connection is available, Pool#hold will block until a connection
|
52
|
+
# is available.
|
53
|
+
def hold
|
54
|
+
t = Thread.current
|
55
|
+
if (conn = owned_connection(t))
|
56
|
+
return yield(conn)
|
57
|
+
end
|
58
|
+
while !(conn = acquire(t))
|
59
|
+
sleep 0.001
|
60
|
+
end
|
61
|
+
begin
|
62
|
+
yield conn
|
63
|
+
ensure
|
64
|
+
release(t)
|
65
|
+
end
|
66
|
+
rescue Exception => e
|
67
|
+
# if the error is not a StandardError it is converted into RuntimeError.
|
68
|
+
raise e.is_a?(StandardError) ? e : e.message
|
69
|
+
end
|
70
|
+
|
71
|
+
# Removes all connection currently available, optionally yielding each
|
72
|
+
# connection to the given block. This method has the effect of
|
73
|
+
# disconnecting from the database. Once a connection is requested using
|
74
|
+
# #hold, the connection pool creates new connections to the database.
|
75
|
+
def disconnect(&block)
|
76
|
+
@mutex.synchronize do
|
77
|
+
@available_connections.each {|c| block[c]} if block
|
78
|
+
@available_connections = []
|
79
|
+
@created_count = @allocated.size
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
# Returns the connection owned by the supplied thread, if any.
|
85
|
+
def owned_connection(thread)
|
86
|
+
@mutex.synchronize {@allocated[thread]}
|
87
|
+
end
|
88
|
+
|
89
|
+
# Assigns a connection to the supplied thread, if one is available.
|
90
|
+
def acquire(thread)
|
91
|
+
@mutex.synchronize do
|
92
|
+
if conn = available
|
93
|
+
@allocated[thread] = conn
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns an available connection. If no connection is available,
|
99
|
+
# tries to create a new connection.
|
100
|
+
def available
|
101
|
+
@available_connections.pop || make_new
|
102
|
+
end
|
103
|
+
|
104
|
+
# Creates a new connection if the size of the pool is less than the
|
105
|
+
# maximum size.
|
106
|
+
def make_new
|
107
|
+
if @created_count < @max_size
|
108
|
+
@created_count += 1
|
109
|
+
@connection_proc ? @connection_proc.call : \
|
110
|
+
(raise Error, "No connection proc specified")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Releases the connection assigned to the supplied thread.
|
115
|
+
def release(thread)
|
116
|
+
@mutex.synchronize do
|
117
|
+
@available_connections << @allocated[thread]
|
118
|
+
@allocated.delete(thread)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# A SingleThreadedPool acts as a replacement for a ConnectionPool for use
|
124
|
+
# in single-threaded applications. ConnectionPool imposes a substantial
|
125
|
+
# performance penalty, so SingleThreadedPool is used to gain some speed.
|
126
|
+
class SingleThreadedPool
|
127
|
+
attr_reader :conn
|
128
|
+
attr_writer :connection_proc
|
129
|
+
|
130
|
+
# Initializes the instance with the supplied block as the connection_proc.
|
131
|
+
def initialize(&block)
|
132
|
+
@connection_proc = block
|
133
|
+
end
|
134
|
+
|
135
|
+
# Yields the connection to the supplied block. This method simulates the
|
136
|
+
# ConnectionPool#hold API.
|
137
|
+
def hold
|
138
|
+
@conn ||= @connection_proc.call
|
139
|
+
yield @conn
|
140
|
+
rescue Exception => e
|
141
|
+
# if the error is not a StandardError it is converted into RuntimeError.
|
142
|
+
raise e.is_a?(StandardError) ? e : e.message
|
143
|
+
end
|
144
|
+
|
145
|
+
# Disconnects from the database. Once a connection is requested using
|
146
|
+
# #hold, the connection is reestablished.
|
147
|
+
def disconnect(&block)
|
148
|
+
block[@conn] if block && @conn
|
149
|
+
@conn = nil
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# Enumerable extensions.
|
2
|
+
module Enumerable
|
3
|
+
# Invokes the specified method for each item, along with the supplied
|
4
|
+
# arguments.
|
5
|
+
def send_each(sym, *args)
|
6
|
+
each {|i| i.send(sym, *args)}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Range extensions
|
11
|
+
class Range
|
12
|
+
# Returns the interval between the beginning and end of the range.
|
13
|
+
def interval
|
14
|
+
last - first
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Object extensions
|
19
|
+
class Object
|
20
|
+
def is_one_of?(*classes)
|
21
|
+
classes.each {|c| return c if is_a?(c)}
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Sequel
|
27
|
+
# Facilitates time calculations by providing methods to convert from larger
|
28
|
+
# time units to seconds, and to convert relative time intervals to absolute
|
29
|
+
# ones. This module duplicates some of the functionality provided by Rails'
|
30
|
+
# ActiveSupport::CoreExtensions::Numeric::Time module.
|
31
|
+
module NumericExtensions
|
32
|
+
MINUTE = 60
|
33
|
+
HOUR = 3600
|
34
|
+
DAY = 86400
|
35
|
+
WEEK = DAY * 7
|
36
|
+
|
37
|
+
# Converts self from minutes to seconds
|
38
|
+
def minutes; self * MINUTE; end; alias_method :minute, :minutes
|
39
|
+
# Converts self from hours to seconds
|
40
|
+
def hours; self * HOUR; end; alias_method :hour, :hours
|
41
|
+
# Converts self from days to seconds
|
42
|
+
def days; self * DAY; end; alias_method :day, :days
|
43
|
+
# Converts self from weeks to seconds
|
44
|
+
def weeks; self * WEEK; end; alias_method :week, :weeks
|
45
|
+
|
46
|
+
# Returns the time at now - self.
|
47
|
+
def ago(t = Time.now); t - self; end
|
48
|
+
alias_method :before, :ago
|
49
|
+
|
50
|
+
# Returns the time at now + self.
|
51
|
+
def from_now(t = Time.now); t + self; end
|
52
|
+
alias_method :since, :from_now
|
53
|
+
|
54
|
+
# Extends the Numeric class with numeric extensions.
|
55
|
+
def self.enable
|
56
|
+
Numeric.send(:include, self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
# Array extensions
|
2
|
+
class Array
|
3
|
+
# Concatenates an array of strings into an SQL string. ANSI SQL and C-style
|
4
|
+
# comments are removed, as well as excessive white-space.
|
5
|
+
def to_sql
|
6
|
+
map {|l| (l =~ /^(.*)--/ ? $1 : l).chomp}.join(' '). \
|
7
|
+
gsub(/\/\*.*\*\//, '').gsub(/\s+/, ' ').strip
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
module Sequel
|
12
|
+
# LiteralString is used to represent literal SQL expressions. An
|
13
|
+
# LiteralString is copied verbatim into an SQL statement. Instances of
|
14
|
+
# LiteralString can be created by calling String#expr.
|
15
|
+
class LiteralString < ::String
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# String extensions
|
20
|
+
class String
|
21
|
+
# Converts a string into an SQL string by removing comments.
|
22
|
+
# See also Array#to_sql.
|
23
|
+
def to_sql
|
24
|
+
split($/).to_sql
|
25
|
+
end
|
26
|
+
|
27
|
+
# Splits a string into separate SQL statements, removing comments
|
28
|
+
# and excessive white-space.
|
29
|
+
def split_sql
|
30
|
+
to_sql.split(';').map {|s| s.strip}
|
31
|
+
end
|
32
|
+
|
33
|
+
# Converts a string into an LiteralString, in order to override string
|
34
|
+
# literalization, e.g.:
|
35
|
+
#
|
36
|
+
# DB[:items].filter(:abc => 'def').sql #=>
|
37
|
+
# "SELECT * FROM items WHERE (abc = 'def')"
|
38
|
+
#
|
39
|
+
# DB[:items].filter(:abc => 'def'.lit).sql #=>
|
40
|
+
# "SELECT * FROM items WHERE (abc = def)"
|
41
|
+
#
|
42
|
+
def lit
|
43
|
+
Sequel::LiteralString.new(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
alias_method :expr, :lit
|
47
|
+
|
48
|
+
# Converts a string into a Time object.
|
49
|
+
def to_time
|
50
|
+
begin
|
51
|
+
Time.parse(self)
|
52
|
+
rescue Exception => e
|
53
|
+
raise Sequel::Error::InvalidValue, "Invalid time value '#{self}' (#{e.message})"
|
54
|
+
end
|
55
|
+
# Why does Time.parse('0000-00-00') bork and not return nil or some such?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
module Sequel
|
61
|
+
module SQL
|
62
|
+
class Expression
|
63
|
+
def lit; self; end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ColumnExpr < Expression
|
67
|
+
attr_reader :l, :op, :r
|
68
|
+
def initialize(l, op, r = nil); @l, @op, @r = l, op, r; end
|
69
|
+
|
70
|
+
def to_s(ds)
|
71
|
+
@r ? \
|
72
|
+
"#{ds.literal(@l)} #{@op} #{ds.literal(@r)}" : \
|
73
|
+
"#{ds.literal(@l)} #{@op}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class QualifiedColumnRef < Expression
|
78
|
+
def initialize(t, c); @t, @c = t, c; end
|
79
|
+
|
80
|
+
def to_s(ds)
|
81
|
+
"#{@t}.#{ds.literal(@c)}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Function < Expression
|
86
|
+
def initialize(f, *args); @f, @args = f, args; end
|
87
|
+
|
88
|
+
def to_s(ds)
|
89
|
+
args = @args.empty? ? '' : ds.literal(@args)
|
90
|
+
"#{@f}(#{args})"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Subscript < Expression
|
95
|
+
def initialize(f, sub); @f, @sub = f, sub; end
|
96
|
+
def |(sub)
|
97
|
+
unless Array === sub
|
98
|
+
sub = [sub]
|
99
|
+
end
|
100
|
+
Subscript.new(@f, @sub << sub)
|
101
|
+
end
|
102
|
+
|
103
|
+
COMMA_SEPARATOR = ", ".freeze
|
104
|
+
|
105
|
+
def to_s(ds)
|
106
|
+
"#{@f}[#{@sub.join(COMMA_SEPARATOR)}]"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
class ColumnAll < Expression
|
111
|
+
def initialize(t); @t = t; end
|
112
|
+
def to_s(ds); "#{@t}.*"; end
|
113
|
+
end
|
114
|
+
|
115
|
+
module ColumnMethods
|
116
|
+
AS = 'AS'.freeze
|
117
|
+
DESC = 'DESC'.freeze
|
118
|
+
ASC = 'ASC'.freeze
|
119
|
+
|
120
|
+
def as(a); ColumnExpr.new(self, AS, a); end
|
121
|
+
alias_method :AS, :as
|
122
|
+
|
123
|
+
def desc; ColumnExpr.new(self, DESC); end
|
124
|
+
alias_method :DESC, :desc
|
125
|
+
|
126
|
+
def asc; ColumnExpr.new(self, ASC); end
|
127
|
+
alias_method :ASC, :asc
|
128
|
+
|
129
|
+
def all; Sequel::SQL::ColumnAll.new(self); end
|
130
|
+
alias_method :ALL, :all
|
131
|
+
|
132
|
+
def cast_as(t)
|
133
|
+
if t.is_a?(Symbol)
|
134
|
+
t = t.to_s.lit
|
135
|
+
end
|
136
|
+
Sequel::SQL::Function.new(:cast, self.as(t))
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class Object
|
143
|
+
include Sequel::SQL::ColumnMethods
|
144
|
+
end
|
145
|
+
|
146
|
+
class Symbol
|
147
|
+
def [](*args); Sequel::SQL::Function.new(self, *args); end
|
148
|
+
def |(sub)
|
149
|
+
unless Array === sub
|
150
|
+
sub = [sub]
|
151
|
+
end
|
152
|
+
Sequel::SQL::Subscript.new(self, sub)
|
153
|
+
end
|
154
|
+
|
155
|
+
COLUMN_REF_RE1 = /^(\w+)__(\w+)___(\w+)/.freeze
|
156
|
+
COLUMN_REF_RE2 = /^(\w+)___(\w+)$/.freeze
|
157
|
+
COLUMN_REF_RE3 = /^(\w+)__(\w+)$/.freeze
|
158
|
+
|
159
|
+
# Converts a symbol into a column name. This method supports underscore
|
160
|
+
# notation in order to express qualified (two underscores) and aliased
|
161
|
+
# (three underscores) columns:
|
162
|
+
#
|
163
|
+
# ds = DB[:items]
|
164
|
+
# :abc.to_column_ref(ds) #=> "abc"
|
165
|
+
# :abc___a.to_column_ref(ds) #=> "abc AS a"
|
166
|
+
# :items__abc.to_column_ref(ds) #=> "items.abc"
|
167
|
+
# :items__abc___a.to_column_ref(ds) #=> "items.abc AS a"
|
168
|
+
#
|
169
|
+
def to_column_ref(ds)
|
170
|
+
case s = to_s
|
171
|
+
when COLUMN_REF_RE1
|
172
|
+
"#{$1}.#{ds.quote_column_ref($2)} AS #{ds.quote_column_ref($3)}"
|
173
|
+
when COLUMN_REF_RE2
|
174
|
+
"#{ds.quote_column_ref($1)} AS #{ds.quote_column_ref($2)}"
|
175
|
+
when COLUMN_REF_RE3
|
176
|
+
"#{$1}.#{ds.quote_column_ref($2)}"
|
177
|
+
else
|
178
|
+
ds.quote_column_ref(s)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Converts missing method calls into functions on columns, if the
|
183
|
+
# method name is made of all upper case letters.
|
184
|
+
def method_missing(sym)
|
185
|
+
if ((s = sym.to_s) =~ /^([A-Z]+)$/)
|
186
|
+
Sequel::SQL::Function.new(s.downcase, self)
|
187
|
+
else
|
188
|
+
super
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,433 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
# A Database object represents a virtual connection to a database.
|
5
|
+
# The Database class is meant to be subclassed by database adapters in order
|
6
|
+
# to provide the functionality needed for executing queries.
|
7
|
+
class Database
|
8
|
+
attr_reader :opts, :pool
|
9
|
+
attr_accessor :logger
|
10
|
+
|
11
|
+
# Constructs a new instance of a database connection with the specified
|
12
|
+
# options hash.
|
13
|
+
#
|
14
|
+
# Sequel::Database is an abstract class that is not useful by itself.
|
15
|
+
def initialize(opts = {}, &block)
|
16
|
+
Model.database_opened(self)
|
17
|
+
@opts = opts
|
18
|
+
|
19
|
+
# Determine if the DB is single threaded or multi threaded
|
20
|
+
@single_threaded = opts[:single_threaded] || @@single_threaded
|
21
|
+
# Construct pool
|
22
|
+
if @single_threaded
|
23
|
+
@pool = SingleThreadedPool.new(&block)
|
24
|
+
else
|
25
|
+
@pool = ConnectionPool.new(opts[:max_connections] || 4, &block)
|
26
|
+
end
|
27
|
+
@pool.connection_proc = block || proc {connect}
|
28
|
+
|
29
|
+
@logger = opts[:logger]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Connects to the database. This method should be overriden by descendants.
|
33
|
+
def connect
|
34
|
+
raise NotImplementedError, "#connect should be overriden by adapters"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Disconnects from the database. This method should be overriden by
|
38
|
+
# descendants.
|
39
|
+
def disconnect
|
40
|
+
raise NotImplementedError, "#disconnect should be overriden by adapters"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns true if the database is using a multi-threaded connection pool.
|
44
|
+
def multi_threaded?
|
45
|
+
!@single_threaded
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns true if the database is using a single-threaded connection pool.
|
49
|
+
def single_threaded?
|
50
|
+
@single_threaded
|
51
|
+
end
|
52
|
+
|
53
|
+
# Returns the URI identifying the database.
|
54
|
+
def uri
|
55
|
+
uri = URI::Generic.new(
|
56
|
+
self.class.adapter_scheme.to_s,
|
57
|
+
nil,
|
58
|
+
@opts[:host],
|
59
|
+
@opts[:port],
|
60
|
+
nil,
|
61
|
+
"/#{@opts[:database]}",
|
62
|
+
nil,
|
63
|
+
nil,
|
64
|
+
nil
|
65
|
+
)
|
66
|
+
uri.user = @opts[:user]
|
67
|
+
uri.password = @opts[:password] if uri.user
|
68
|
+
uri.to_s
|
69
|
+
end
|
70
|
+
alias url uri # Because I don't care much for the semantic difference.
|
71
|
+
|
72
|
+
# Returns a blank dataset
|
73
|
+
def dataset
|
74
|
+
ds = Sequel::Dataset.new(self)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Fetches records for an arbitrary SQL statement. If a block is given,
|
78
|
+
# it is used to iterate over the records:
|
79
|
+
#
|
80
|
+
# DB.fetch('SELECT * FROM items') {|r| p r}
|
81
|
+
#
|
82
|
+
# If a block is not given, the method returns a dataset instance:
|
83
|
+
#
|
84
|
+
# DB.fetch('SELECT * FROM items').print
|
85
|
+
#
|
86
|
+
# Fetch can also perform parameterized queries for protection against SQL
|
87
|
+
# injection:
|
88
|
+
#
|
89
|
+
# DB.fetch('SELECT * FROM items WHERE name = ?', my_name).print
|
90
|
+
#
|
91
|
+
# A short-hand form for Database#fetch is Database#[]:
|
92
|
+
#
|
93
|
+
# DB['SELECT * FROM items'].each {|r| p r}
|
94
|
+
#
|
95
|
+
def fetch(sql, *args, &block)
|
96
|
+
ds = dataset
|
97
|
+
sql = sql.gsub('?') {|m| ds.literal(args.shift)}
|
98
|
+
if block
|
99
|
+
ds.fetch_rows(sql, &block)
|
100
|
+
else
|
101
|
+
ds.opts[:sql] = sql
|
102
|
+
ds
|
103
|
+
end
|
104
|
+
end
|
105
|
+
alias_method :>>, :fetch
|
106
|
+
|
107
|
+
# Converts a query block into a dataset. For more information see
|
108
|
+
# Dataset#query.
|
109
|
+
def query(&block)
|
110
|
+
dataset.query(&block)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns a new dataset with the from method invoked. If a block is given,
|
114
|
+
# it is used as a filter on the dataset.
|
115
|
+
def from(*args, &block)
|
116
|
+
ds = dataset.from(*args)
|
117
|
+
block ? ds.filter(&block) : ds
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns a new dataset with the select method invoked.
|
121
|
+
def select(*args); dataset.select(*args); end
|
122
|
+
|
123
|
+
# Returns a dataset from the database. If the first argument is a string,
|
124
|
+
# the method acts as an alias for Database#fetch, returning a dataset for
|
125
|
+
# arbitrary SQL:
|
126
|
+
#
|
127
|
+
# DB['SELECT * FROM items WHERE name = ?', my_name].print
|
128
|
+
#
|
129
|
+
# Otherwise, the dataset returned has its from option set to the given
|
130
|
+
# arguments:
|
131
|
+
#
|
132
|
+
# DB[:items].sql #=> "SELECT * FROM items"
|
133
|
+
#
|
134
|
+
def [](*args)
|
135
|
+
(String === args.first) ? fetch(*args) : from(*args)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Raises a Sequel::Error::NotImplemented. This method is overriden in descendants.
|
139
|
+
def execute(sql)
|
140
|
+
raise NotImplementedError, "#execute should be overriden by adapters"
|
141
|
+
end
|
142
|
+
|
143
|
+
# Executes the supplied SQL statement. The SQL can be supplied as a string
|
144
|
+
# or as an array of strings. If an array is give, comments and excessive
|
145
|
+
# white space are removed. See also Array#to_sql.
|
146
|
+
def <<(sql); execute((Array === sql) ? sql.to_sql : sql); end
|
147
|
+
|
148
|
+
# Acquires a database connection, yielding it to the passed block.
|
149
|
+
def synchronize(&block)
|
150
|
+
@pool.hold(&block)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Returns true if there is a database connection
|
154
|
+
def test_connection
|
155
|
+
@pool.hold {|conn|}
|
156
|
+
true
|
157
|
+
end
|
158
|
+
|
159
|
+
# include Dataset::SQL
|
160
|
+
include Schema::SQL
|
161
|
+
|
162
|
+
# default serial primary key definition. this should be overriden for each adapter.
|
163
|
+
def serial_primary_key_options
|
164
|
+
{:primary_key => true, :type => :integer, :auto_increment => true}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Creates a table. The easiest way to use this method is to provide a
|
168
|
+
# block:
|
169
|
+
# DB.create_table :posts do
|
170
|
+
# primary_key :id, :serial
|
171
|
+
# column :title, :text
|
172
|
+
# column :content, :text
|
173
|
+
# index :title
|
174
|
+
# end
|
175
|
+
def create_table(name, &block)
|
176
|
+
g = Schema::Generator.new(self, &block)
|
177
|
+
create_table_sql_list(name, *g.create_info).each {|sql| execute(sql)}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Forcibly creates a table. If the table already exists it is dropped.
|
181
|
+
def create_table!(name, &block)
|
182
|
+
drop_table(name) rescue nil
|
183
|
+
create_table(name, &block)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Drops one or more tables corresponding to the given table names.
|
187
|
+
def drop_table(*names)
|
188
|
+
names.each {|n| execute(drop_table_sql(n))}
|
189
|
+
end
|
190
|
+
|
191
|
+
# Renames a table:
|
192
|
+
#
|
193
|
+
# DB.tables #=> [:items]
|
194
|
+
# DB.rename_table :items, :old_items
|
195
|
+
# DB.tables #=> [:old_items]
|
196
|
+
def rename_table(*args)
|
197
|
+
execute(rename_table_sql(*args))
|
198
|
+
end
|
199
|
+
|
200
|
+
# Alters the given table with the specified block. Here are the currently
|
201
|
+
# available operations:
|
202
|
+
#
|
203
|
+
# DB.alter_table :items do
|
204
|
+
# add_column :category, :text, :default => 'ruby'
|
205
|
+
# drop_column :category
|
206
|
+
# rename_column :cntr, :counter
|
207
|
+
# set_column_type :value, :float
|
208
|
+
# set_column_default :value, :float
|
209
|
+
# add_index [:group, :category]
|
210
|
+
# drop_index [:group, :category]
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# Note that #add_column accepts all the options available for column
|
214
|
+
# definitions using create_table, and #add_index accepts all the options
|
215
|
+
# available for index definition.
|
216
|
+
def alter_table(name, &block)
|
217
|
+
g = Schema::AlterTableGenerator.new(self, &block)
|
218
|
+
alter_table_sql_list(name, g.operations).each {|sql| execute(sql)}
|
219
|
+
end
|
220
|
+
|
221
|
+
# Adds a column to the specified table. This method expects a column name,
|
222
|
+
# a datatype and optionally a hash with additional constraints and options:
|
223
|
+
#
|
224
|
+
# DB.add_column :items, :name, :text, :unique => true, :null => false
|
225
|
+
# DB.add_column :items, :category, :text, :default => 'ruby'
|
226
|
+
def add_column(table, *args)
|
227
|
+
alter_table(table) {add_column(*args)}
|
228
|
+
end
|
229
|
+
|
230
|
+
# Removes a column from the specified table:
|
231
|
+
#
|
232
|
+
# DB.drop_column :items, :category
|
233
|
+
def drop_column(table, *args)
|
234
|
+
alter_table(table) {drop_column(*args)}
|
235
|
+
end
|
236
|
+
|
237
|
+
# Renames a column in the specified table. This method expects the current
|
238
|
+
# column name and the new column name:
|
239
|
+
#
|
240
|
+
# DB.rename_column :items, :cntr, :counter
|
241
|
+
def rename_column(table, *args)
|
242
|
+
alter_table(table) {rename_column(*args)}
|
243
|
+
end
|
244
|
+
|
245
|
+
# Set the data type for the given column in the given table:
|
246
|
+
#
|
247
|
+
# DB.set_column_type :items, :price, :float
|
248
|
+
def set_column_type(table, *args)
|
249
|
+
alter_table(table) {set_column_type(*args)}
|
250
|
+
end
|
251
|
+
|
252
|
+
# Sets the default value for the given column in the given table:
|
253
|
+
#
|
254
|
+
# DB.set_column_default :items, :category, 'perl!'
|
255
|
+
def set_column_default(table, *args)
|
256
|
+
alter_table(table) {set_column_default(*args)}
|
257
|
+
end
|
258
|
+
|
259
|
+
# Adds an index to a table for the given columns:
|
260
|
+
#
|
261
|
+
# DB.add_index :posts, :title
|
262
|
+
# DB.add_index :posts, [:author, :title], :unique => true
|
263
|
+
def add_index(table, *args)
|
264
|
+
alter_table(table) {add_index(*args)}
|
265
|
+
end
|
266
|
+
|
267
|
+
# Removes an index for the given table and column/s:
|
268
|
+
#
|
269
|
+
# DB.drop_index :posts, :title
|
270
|
+
# DB.drop_index :posts, [:author, :title]
|
271
|
+
def drop_index(table, *args)
|
272
|
+
alter_table(table) {drop_index(*args)}
|
273
|
+
end
|
274
|
+
|
275
|
+
# Returns true if the given table exists.
|
276
|
+
def table_exists?(name)
|
277
|
+
if respond_to?(:tables)
|
278
|
+
tables.include?(name.to_sym)
|
279
|
+
else
|
280
|
+
from(name).first && true
|
281
|
+
end
|
282
|
+
rescue
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
# Creates a view based on a dataset or an SQL string:
|
287
|
+
#
|
288
|
+
# DB.create_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
|
289
|
+
# DB.create_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
|
290
|
+
def create_view(name, source)
|
291
|
+
source = source.sql if source.is_a?(Dataset)
|
292
|
+
execute("CREATE VIEW #{name} AS #{source}")
|
293
|
+
end
|
294
|
+
|
295
|
+
# Creates a view, replacing it if it already exists:
|
296
|
+
#
|
297
|
+
# DB.create_or_replace_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
|
298
|
+
# DB.create_or_replace_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
|
299
|
+
def create_or_replace_view(name, source)
|
300
|
+
source = source.sql if source.is_a?(Dataset)
|
301
|
+
execute("CREATE OR REPLACE VIEW #{name} AS #{source}")
|
302
|
+
end
|
303
|
+
|
304
|
+
# Drops a view:
|
305
|
+
#
|
306
|
+
# DB.drop_view(:cheap_items)
|
307
|
+
def drop_view(name)
|
308
|
+
execute("DROP VIEW #{name}")
|
309
|
+
end
|
310
|
+
|
311
|
+
SQL_BEGIN = 'BEGIN'.freeze
|
312
|
+
SQL_COMMIT = 'COMMIT'.freeze
|
313
|
+
SQL_ROLLBACK = 'ROLLBACK'.freeze
|
314
|
+
|
315
|
+
# A simple implementation of SQL transactions. Nested transactions are not
|
316
|
+
# supported - calling #transaction within a transaction will reuse the
|
317
|
+
# current transaction. May be overridden for databases that support nested
|
318
|
+
# transactions.
|
319
|
+
def transaction
|
320
|
+
@pool.hold do |conn|
|
321
|
+
@transactions ||= []
|
322
|
+
if @transactions.include? Thread.current
|
323
|
+
return yield(conn)
|
324
|
+
end
|
325
|
+
conn.execute(SQL_BEGIN)
|
326
|
+
begin
|
327
|
+
@transactions << Thread.current
|
328
|
+
result = yield(conn)
|
329
|
+
conn.execute(SQL_COMMIT)
|
330
|
+
result
|
331
|
+
rescue => e
|
332
|
+
conn.execute(SQL_ROLLBACK)
|
333
|
+
raise e unless Error::Rollback === e
|
334
|
+
ensure
|
335
|
+
@transactions.delete(Thread.current)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
@@adapters = Hash.new
|
341
|
+
|
342
|
+
# Sets the adapter scheme for the Database class. Call this method in
|
343
|
+
# descendnants of Database to allow connection using a URL. For example the
|
344
|
+
# following:
|
345
|
+
# class DB2::Database < Sequel::Database
|
346
|
+
# set_adapter_scheme :db2
|
347
|
+
# ...
|
348
|
+
# end
|
349
|
+
# would allow connection using:
|
350
|
+
# Sequel.open('db2://user:password@dbserver/mydb')
|
351
|
+
def self.set_adapter_scheme(scheme)
|
352
|
+
@scheme = scheme
|
353
|
+
@@adapters[scheme.to_sym] = self
|
354
|
+
end
|
355
|
+
|
356
|
+
# Returns the scheme for the Database class.
|
357
|
+
def self.adapter_scheme
|
358
|
+
@scheme
|
359
|
+
end
|
360
|
+
|
361
|
+
# Converts a uri to an options hash. These options are then passed
|
362
|
+
# to a newly created database object.
|
363
|
+
def self.uri_to_options(uri)
|
364
|
+
{
|
365
|
+
:user => uri.user,
|
366
|
+
:password => uri.password,
|
367
|
+
:host => uri.host,
|
368
|
+
:port => uri.port,
|
369
|
+
:database => (uri.path =~ /\/(.*)/) && ($1)
|
370
|
+
}
|
371
|
+
end
|
372
|
+
|
373
|
+
def self.adapter_class(scheme)
|
374
|
+
adapter_name = scheme.to_s =~ /\-/ ? scheme.to_s.gsub('-', '_').to_sym : scheme.to_sym
|
375
|
+
scheme = scheme.to_sym
|
376
|
+
|
377
|
+
if (klass = @@adapters[scheme]).nil?
|
378
|
+
# attempt to load the adapter file
|
379
|
+
begin
|
380
|
+
require File.join(File.dirname(__FILE__), "adapters/#{scheme}")
|
381
|
+
rescue LoadError => e
|
382
|
+
raise Error::AdapterNotFound, "Could not load #{scheme} adapter:\n #{e.message}"
|
383
|
+
end
|
384
|
+
|
385
|
+
# make sure we actually loaded the adapter
|
386
|
+
if (klass = @@adapters[scheme]).nil?
|
387
|
+
raise Error::AdapterNotFound, "Could not load #{scheme} adapter"
|
388
|
+
end
|
389
|
+
end
|
390
|
+
return klass
|
391
|
+
end
|
392
|
+
|
393
|
+
# call-seq:
|
394
|
+
# Sequel::Database.connect(conn_string)
|
395
|
+
# Sequel::Database.connect(opts)
|
396
|
+
# Sequel.connect(conn_string)
|
397
|
+
# Sequel.connect(opts)
|
398
|
+
# Sequel.open(conn_string)
|
399
|
+
# Sequel.open(opts)
|
400
|
+
#
|
401
|
+
# Creates a new database object based on the supplied connection string
|
402
|
+
# and or options. If a URI is used, the URI scheme determines the database
|
403
|
+
# class used, and the rest of the string specifies the connection options.
|
404
|
+
# For example:
|
405
|
+
#
|
406
|
+
# DB = Sequel.open 'sqlite:///blog.db'
|
407
|
+
#
|
408
|
+
# The second form of this method takes an options:
|
409
|
+
#
|
410
|
+
# DB = Sequel.open :adapter => :sqlite, :database => 'blog.db'
|
411
|
+
def self.connect(conn_string, opts = nil)
|
412
|
+
if conn_string.is_a?(String)
|
413
|
+
uri = URI.parse(conn_string)
|
414
|
+
scheme = uri.scheme
|
415
|
+
scheme = :dbi if scheme =~ /^dbi-(.+)/
|
416
|
+
c = adapter_class(scheme)
|
417
|
+
c.new(c.uri_to_options(uri).merge(opts || {}))
|
418
|
+
else
|
419
|
+
opts = conn_string.merge(opts || {})
|
420
|
+
c = adapter_class(opts[:adapter])
|
421
|
+
c.new(opts)
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
@@single_threaded = false
|
426
|
+
|
427
|
+
# Sets the default single_threaded mode for new databases.
|
428
|
+
def self.single_threaded=(value)
|
429
|
+
@@single_threaded = value
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|