sequel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +13 -0
- data/COPYING +18 -0
- data/README +151 -0
- data/Rakefile +96 -0
- data/lib/sequel.rb +13 -0
- data/lib/sequel/connection_pool.rb +65 -0
- data/lib/sequel/core_ext.rb +9 -0
- data/lib/sequel/database.rb +119 -0
- data/lib/sequel/dataset.rb +357 -0
- data/lib/sequel/model.rb +237 -0
- data/lib/sequel/postgres.rb +396 -0
- data/lib/sequel/schema.rb +163 -0
- data/lib/sequel/sqlite.rb +112 -0
- data/spec/database_spec.rb +18 -0
- data/spec/dataset_spec.rb +124 -0
- data/spec/postgres_spec.rb +6 -0
- metadata +85 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
*0.0.1*
|
2
|
+
|
3
|
+
* More documentation for Dataset.
|
4
|
+
|
5
|
+
* Renamed Database#query to Database#dataset.
|
6
|
+
|
7
|
+
* Added Dataset#insert_multiple for inserting multiple records.
|
8
|
+
|
9
|
+
* Added Dataset#<< as shorthand for inserting records.
|
10
|
+
|
11
|
+
* Added Database#<< method for executing arbitrary SQL.
|
12
|
+
|
13
|
+
* Imported Sequel code.
|
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2006 Sharon Rosner
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
== about Sequel
|
2
|
+
|
3
|
+
Sequel is an ORM framework for Ruby. Sequel provides thread safety, connection pooling, and a DSL for constructing queries and table schemas.
|
4
|
+
|
5
|
+
== Sequel vs. ActiveRecord
|
6
|
+
|
7
|
+
Sequel offers the following advantages over ActiveRecord:
|
8
|
+
|
9
|
+
* Better performance with large tables: unlike ActiveRecord, Sequel does not load the entire resultset into memory, but fetches each record separately and implements an Enumerable interface.
|
10
|
+
* Easy construction of queries using a DSL.
|
11
|
+
* Using model classes is possible, but not mandatory.
|
12
|
+
|
13
|
+
== Resources
|
14
|
+
|
15
|
+
* {Project page}[http://code.google.com/p/ruby-sequel/]
|
16
|
+
* {Source code}[http://ruby-sequel.googlecode.com/svn/]
|
17
|
+
* {Bug tracking}[http://code.google.com/p/ruby-sequel/issues/list]
|
18
|
+
* {RubyForge page}[http://rubyforge.org/projects/sequel/]
|
19
|
+
|
20
|
+
To check out the source code:
|
21
|
+
|
22
|
+
svn co http://ruby-sequel.googlecode.com/svn/trunk
|
23
|
+
|
24
|
+
== Installation
|
25
|
+
|
26
|
+
sudo gem install sequel
|
27
|
+
|
28
|
+
== A Short Tutorial
|
29
|
+
|
30
|
+
=== Connecting to a database
|
31
|
+
|
32
|
+
There are two ways to create a connection to a database. The easier way is to provide a connection URL:
|
33
|
+
|
34
|
+
DB = Sequel.connect("postgres://postgres:postgres@localhost:5432/my_db")
|
35
|
+
|
36
|
+
You can also specify optional parameters, such as the connection pool size:
|
37
|
+
|
38
|
+
DB = Sequel.connect("postgres://postgres:postgres@localhost:5432/my_db",
|
39
|
+
:max_connections => 10)
|
40
|
+
|
41
|
+
The second, more verbose, way is to create an instance of a database class:
|
42
|
+
|
43
|
+
DB = Sequel::Postgres::Database.new(:database => 'my_db', :host => 'localhost',
|
44
|
+
:port => 5432)
|
45
|
+
|
46
|
+
== Arbitrary SQL queries
|
47
|
+
|
48
|
+
DB.execute("create table t (a text, b text)")
|
49
|
+
DB.execute("insert into t values ('a', 'b')")
|
50
|
+
|
51
|
+
Or more succintly:
|
52
|
+
|
53
|
+
DB << "create table t (a text, b text)"
|
54
|
+
DB << "insert into t values ('a', 'b')"
|
55
|
+
|
56
|
+
=== Creating Datasets
|
57
|
+
|
58
|
+
Dataset is the primary means through which records are retrieved and manipulated. You can create an blank dataset by using the query method:
|
59
|
+
|
60
|
+
dataset = DB.query
|
61
|
+
|
62
|
+
Or by using the from methods:
|
63
|
+
|
64
|
+
posts = DB.from(:posts)
|
65
|
+
|
66
|
+
You can also use the equivalent shorthand:
|
67
|
+
|
68
|
+
posts = DB[:posts]
|
69
|
+
|
70
|
+
Note: the dataset will only fetch records when you explicitly ask for them, as will be shown below. Datasets can be manipulated to filter through records, change record order and even join tables, as will also be shown below.
|
71
|
+
|
72
|
+
=== Retrieving Records
|
73
|
+
|
74
|
+
You can retrieve records by using the all method:
|
75
|
+
|
76
|
+
posts.all
|
77
|
+
|
78
|
+
The all method returns an array of hashes, where each hash corresponds to a record.
|
79
|
+
|
80
|
+
You can also iterate through records one at a time:
|
81
|
+
|
82
|
+
posts.each {|row| p row}
|
83
|
+
|
84
|
+
Or perform more advanced stuff:
|
85
|
+
|
86
|
+
posts.map(:id)
|
87
|
+
posts.inject({}) {|h, r| h[r[:id]] = r[:name]}
|
88
|
+
|
89
|
+
You can also retrieve the first record in a dataset:
|
90
|
+
|
91
|
+
posts.first
|
92
|
+
|
93
|
+
If the dataset is ordered, you can also ask for the last record:
|
94
|
+
|
95
|
+
posts.order(:stamp).last
|
96
|
+
|
97
|
+
=== Filtering Records
|
98
|
+
|
99
|
+
The simplest way to filter records is to provide a hash of values to match:
|
100
|
+
|
101
|
+
my_posts = posts.filter(:category => 'ruby', :author => 'david')
|
102
|
+
|
103
|
+
You can also specify ranges:
|
104
|
+
|
105
|
+
my_posts = posts.filter(:stamp => 2.weeks.ago..1.week.ago)
|
106
|
+
|
107
|
+
Some adapters will also let you specify Regexps:
|
108
|
+
|
109
|
+
my_posts = posts.filter(:category => /ruby/i)
|
110
|
+
|
111
|
+
You can also use an inverse filter:
|
112
|
+
|
113
|
+
my_posts = posts.exclude(:category => /ruby/i)
|
114
|
+
|
115
|
+
You can then retrieve the records by using any of the retrieval methods:
|
116
|
+
|
117
|
+
my_posts.each {|row| p row}
|
118
|
+
|
119
|
+
You can also specify a custom WHERE clause:
|
120
|
+
|
121
|
+
posts.filter('(stamp < ?) AND (author <> ?)', 3.days.ago, author_name)
|
122
|
+
|
123
|
+
=== Counting Records
|
124
|
+
|
125
|
+
posts.count
|
126
|
+
|
127
|
+
=== Ordering Records
|
128
|
+
|
129
|
+
posts.order(:stamp)
|
130
|
+
|
131
|
+
You can also specify descending order
|
132
|
+
|
133
|
+
posts.order(:stamp.DESC)
|
134
|
+
|
135
|
+
=== Deleting Records
|
136
|
+
|
137
|
+
posts.filter('stamp < ?', 3.days.ago).delete
|
138
|
+
|
139
|
+
=== Inserting Records
|
140
|
+
|
141
|
+
posts.insert(:category => 'ruby', :author => 'david')
|
142
|
+
|
143
|
+
Or alternatively:
|
144
|
+
|
145
|
+
posts << {:category => 'ruby', :author => 'david'}
|
146
|
+
|
147
|
+
=== Updating Records
|
148
|
+
|
149
|
+
posts.filter('stamp < ?', 3.days.ago).update(:state => 'archived')
|
150
|
+
|
151
|
+
===
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/clean'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'fileutils'
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
NAME = "sequel"
|
9
|
+
VERS = "0.0.1"
|
10
|
+
CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
|
11
|
+
RDOC_OPTS = ['--quiet', '--title', "Sequel Documentation",
|
12
|
+
"--opname", "index.html",
|
13
|
+
"--line-numbers",
|
14
|
+
"--main", "README",
|
15
|
+
"--inline-source"]
|
16
|
+
|
17
|
+
desc "Packages up Sequel."
|
18
|
+
task :default => [:package]
|
19
|
+
task :package => [:clean]
|
20
|
+
|
21
|
+
task :doc => [:rdoc]
|
22
|
+
|
23
|
+
Rake::RDocTask.new do |rdoc|
|
24
|
+
rdoc.rdoc_dir = 'doc/rdoc'
|
25
|
+
rdoc.options += RDOC_OPTS
|
26
|
+
rdoc.main = "README"
|
27
|
+
rdoc.title = "Sequel Documentation"
|
28
|
+
rdoc.rdoc_files.add ['README', 'COPYING', 'lib/sequel.rb', 'lib/sequel/**/*.rb']
|
29
|
+
end
|
30
|
+
|
31
|
+
spec = Gem::Specification.new do |s|
|
32
|
+
s.name = NAME
|
33
|
+
s.version = VERS
|
34
|
+
s.platform = Gem::Platform::RUBY
|
35
|
+
s.has_rdoc = true
|
36
|
+
s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
|
37
|
+
s.rdoc_options += RDOC_OPTS +
|
38
|
+
['--exclude', '^(examples|extras)\/', '--exclude', 'lib/sequel.rb']
|
39
|
+
s.summary = "ORM framework for Ruby."
|
40
|
+
s.description = s.summary
|
41
|
+
s.author = "Sharon Rosner"
|
42
|
+
s.email = 'ciconia@gmail.com'
|
43
|
+
s.homepage = 'http://code.google.com/p/ruby-sequel/'
|
44
|
+
|
45
|
+
s.add_dependency('metaid')
|
46
|
+
s.required_ruby_version = '>= 1.8.2'
|
47
|
+
|
48
|
+
s.files = %w(COPYING README Rakefile) + Dir.glob("{doc,spec,lib}/**/*")
|
49
|
+
|
50
|
+
s.require_path = "lib"
|
51
|
+
end
|
52
|
+
|
53
|
+
Rake::GemPackageTask.new(spec) do |p|
|
54
|
+
p.need_tar = true
|
55
|
+
p.gem_spec = spec
|
56
|
+
end
|
57
|
+
|
58
|
+
task :install do
|
59
|
+
sh %{rake package}
|
60
|
+
sh %{sudo gem install pkg/#{NAME}-#{VERS}}
|
61
|
+
end
|
62
|
+
|
63
|
+
task :uninstall => [:clean] do
|
64
|
+
sh %{sudo gem uninstall #{NAME}}
|
65
|
+
end
|
66
|
+
|
67
|
+
desc 'Update docs and upload to rubyforge.org'
|
68
|
+
task :doc_rforge do
|
69
|
+
sh %{rake doc}
|
70
|
+
sh %{scp -r doc/rdoc/* ciconia@rubyforge.org:/var/www/gforge-projects/sequel}
|
71
|
+
end
|
72
|
+
|
73
|
+
require 'spec/rake/spectask'
|
74
|
+
|
75
|
+
desc "Run specs with coverage"
|
76
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
77
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
78
|
+
t.rcov = true
|
79
|
+
end
|
80
|
+
|
81
|
+
##############################################################################
|
82
|
+
# Statistics
|
83
|
+
##############################################################################
|
84
|
+
|
85
|
+
STATS_DIRECTORIES = [
|
86
|
+
%w(Code lib/),
|
87
|
+
%w(Spec spec/)
|
88
|
+
].collect { |name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir) }
|
89
|
+
|
90
|
+
desc "Report code statistics (KLOCs, etc) from the application"
|
91
|
+
task :stats do
|
92
|
+
require 'extra/stats'
|
93
|
+
verbose = true
|
94
|
+
CodeStatistics.new(*STATS_DIRECTORIES).to_s
|
95
|
+
end
|
96
|
+
|
data/lib/sequel.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
dir = File.join(File.dirname(__FILE__), 'sequel')
|
2
|
+
require File.join(dir, 'core_ext')
|
3
|
+
require File.join(dir, 'database')
|
4
|
+
require File.join(dir, 'connection_pool')
|
5
|
+
require File.join(dir, 'schema')
|
6
|
+
require File.join(dir, 'dataset')
|
7
|
+
require File.join(dir, 'model')
|
8
|
+
|
9
|
+
module Sequel #:nodoc:
|
10
|
+
def self.connect(url)
|
11
|
+
Database.connect(url)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
class ConnectionPool
|
5
|
+
attr_reader :max_size, :mutex, :conn_maker
|
6
|
+
attr_reader :available_connections, :allocated, :created_count
|
7
|
+
|
8
|
+
def initialize(max_size = 4, &block)
|
9
|
+
@max_size = max_size
|
10
|
+
@mutex = Mutex.new
|
11
|
+
@conn_maker = block
|
12
|
+
|
13
|
+
@available_connections = []
|
14
|
+
@allocated = {}
|
15
|
+
@created_count = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
def size
|
19
|
+
@created_count
|
20
|
+
end
|
21
|
+
|
22
|
+
def hold
|
23
|
+
t = Thread.current
|
24
|
+
if (conn = owned_connection(t))
|
25
|
+
return yield(conn)
|
26
|
+
end
|
27
|
+
while !(conn = acquire(t))
|
28
|
+
sleep 0.001
|
29
|
+
end
|
30
|
+
begin
|
31
|
+
yield conn
|
32
|
+
ensure
|
33
|
+
release(t)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def owned_connection(thread)
|
38
|
+
@mutex.synchronize {@allocated[thread]}
|
39
|
+
end
|
40
|
+
|
41
|
+
def acquire(thread)
|
42
|
+
@mutex.synchronize do
|
43
|
+
@allocated[thread] ||= available
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def available
|
48
|
+
@available_connections.pop || make_new
|
49
|
+
end
|
50
|
+
|
51
|
+
def make_new
|
52
|
+
if @created_count < @max_size
|
53
|
+
@created_count += 1
|
54
|
+
@conn_maker.call
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def release(thread)
|
59
|
+
@mutex.synchronize do
|
60
|
+
@available_connections << @allocated[thread]
|
61
|
+
@allocated.delete(thread)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'schema')
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
# A Database object represents a virtual connection to a database.
|
7
|
+
# The Database class is meant to be subclassed by database adapters in order
|
8
|
+
# to provide the functionality needed for executing queries.
|
9
|
+
class Database
|
10
|
+
# Constructs a new instance of a database connection with the specified
|
11
|
+
# options hash.
|
12
|
+
#
|
13
|
+
# Sequel::Database is an abstract class that is not useful by itself.
|
14
|
+
def initialize(opts = {})
|
15
|
+
@opts = opts
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns a new dataset with the from method invoked.
|
19
|
+
def from(*args); dataset.from(*args); end
|
20
|
+
|
21
|
+
# Returns a new dataset with the select method invoked.
|
22
|
+
def select(*args); dataset.select(*args); end
|
23
|
+
|
24
|
+
# Returns a new dataset with the from parameter set. For example,
|
25
|
+
# db[:posts].each {|p| puts p[:title]}
|
26
|
+
def [](table)
|
27
|
+
dataset.from(table)
|
28
|
+
end
|
29
|
+
|
30
|
+
# call-seq:
|
31
|
+
# db.execute(sql)
|
32
|
+
# db << sql
|
33
|
+
#
|
34
|
+
# Executes an sql query.
|
35
|
+
def <<(sql)
|
36
|
+
execute(sql)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a literal SQL representation of a value. This method is usually
|
40
|
+
# overriden in database adapters.
|
41
|
+
def literal(v)
|
42
|
+
case v
|
43
|
+
when String: "'%s'" % v
|
44
|
+
else v.to_s
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Creates a table. The easiest way to use this method is to provide a
|
49
|
+
# block:
|
50
|
+
# DB.create_table :posts do
|
51
|
+
# primary_key :id, :serial
|
52
|
+
# column :title, :text
|
53
|
+
# column :content, :text
|
54
|
+
# index :title
|
55
|
+
# end
|
56
|
+
def create_table(name, columns = nil, indexes = nil, &block)
|
57
|
+
if block
|
58
|
+
schema = Schema.new
|
59
|
+
schema.create_table(name, &block)
|
60
|
+
schema.create(self)
|
61
|
+
else
|
62
|
+
execute Schema.create_table_sql(name, columns, indexes)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Drops a table.
|
67
|
+
def drop_table(name)
|
68
|
+
execute Schema.drop_table_sql(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Performs a brute-force check for the existance of a table. This method is
|
72
|
+
# usually overriden in descendants.
|
73
|
+
def table_exists?(name)
|
74
|
+
from(name).first && true
|
75
|
+
rescue
|
76
|
+
false
|
77
|
+
end
|
78
|
+
|
79
|
+
@@adapters = Hash.new
|
80
|
+
|
81
|
+
# Sets the adapter scheme for the database class. Call this method in
|
82
|
+
# descendnants of Database to allow connection using a URL. For example:
|
83
|
+
# class DB2::Database < Sequel::Database
|
84
|
+
# set_adapter_scheme :db2
|
85
|
+
# ...
|
86
|
+
# end
|
87
|
+
def self.set_adapter_scheme(scheme)
|
88
|
+
@@adapters[scheme.to_sym] = self
|
89
|
+
end
|
90
|
+
|
91
|
+
# Converts a uri to an options hash. These options are then passed
|
92
|
+
# to a newly created database object.
|
93
|
+
def self.uri_to_options(uri)
|
94
|
+
{
|
95
|
+
:user => uri.user,
|
96
|
+
:password => uri.password,
|
97
|
+
:host => uri.host,
|
98
|
+
:port => uri.port,
|
99
|
+
:database => (uri.path =~ /\/(.*)/) && ($1)
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
# call-seq:
|
104
|
+
# Sequel::Database.connect(conn_string)
|
105
|
+
# Sequel.connect(conn_string)
|
106
|
+
#
|
107
|
+
# Creates a new database object based on the supplied connection string.
|
108
|
+
# The specified scheme determines the database class used, and the rest
|
109
|
+
# of the string specifies the connection options. For example:
|
110
|
+
# DB = Sequel.connect('sqlite:///blog.db')
|
111
|
+
def self.connect(conn_string)
|
112
|
+
uri = URI.parse(conn_string)
|
113
|
+
c = @@adapters[uri.scheme.to_sym]
|
114
|
+
raise "Invalid database scheme" unless c
|
115
|
+
c.new(c.uri_to_options(uri))
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|