sequel 0.4.5 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,4 +1,8 @@
1
- === 0.4.5 (2007-12-25)
1
+ === 0.5 (2007-12-30)
2
+
3
+ * Removed model code into separate sub-project. Rearranged trunk into core, model and model_plugins.
4
+
5
+ === 0.4.5 (2007-12-25)
2
6
 
3
7
  * Added rdoc for new alter_table functionality (#109).
4
8
 
data/README CHANGED
@@ -24,6 +24,10 @@ If you have any comments or suggestions please send an email to ciconia at gmail
24
24
 
25
25
  sudo gem install sequel
26
26
 
27
+ Note: as of version 0.5, Sequel models are distributed in a separate gem. In order to use sequel models, you should install the sequel_model gem:
28
+
29
+ sudo gem install sequel_model
30
+
27
31
  == Supported Databases
28
32
 
29
33
  Sequel currently supports:
@@ -74,178 +78,4 @@ Or getting results as a transposed hash, with one column as key and another as v
74
78
 
75
79
  middle_east.to_hash(:name, :area) #=> {'Israel' => 20000, 'Greece' => 120000, ...}
76
80
 
77
- Much of Sequel is still undocumented (especially the part relating to model classes). The following section provides examples of common usage. Feel free to explore...
78
-
79
- == Getting Started
80
-
81
- === Connecting to a database
82
-
83
- To connect to a database you simply provide Sequel with a URL:
84
-
85
- require 'sequel'
86
- DB = Sequel.open 'sqlite:///blog.db'
87
-
88
- The connection URL can also include such stuff as the user name and password:
89
-
90
- DB = Sequel.open 'postgres://cico:12345@localhost:5432/mydb'
91
-
92
- You can also specify optional parameters, such as the connection pool size, or a logger for logging SQL queries:
93
-
94
- DB = Sequel.open("postgres://postgres:postgres@localhost/my_db",
95
- :max_connections => 10, :logger => Logger.new('log/db.log'))
96
-
97
- === Arbitrary SQL queries
98
-
99
- DB.execute("create table t (a text, b text)")
100
- DB.execute("insert into t values ('a', 'b')")
101
-
102
- Or more succinctly:
103
-
104
- DB << "create table t (a text, b text)"
105
- DB << "insert into t values ('a', 'b')"
106
-
107
- === Getting Dataset Instances
108
-
109
- Dataset is the primary means through which records are retrieved and manipulated. You can create an blank dataset by using the dataset method:
110
-
111
- dataset = DB.dataset
112
-
113
- Or by using the from methods:
114
-
115
- posts = DB.from(:posts)
116
-
117
- You can also use the equivalent shorthand:
118
-
119
- posts = DB[:posts]
120
-
121
- 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.
122
-
123
- === Retrieving Records
124
-
125
- You can retrieve records by using the all method:
126
-
127
- posts.all
128
-
129
- The all method returns an array of hashes, where each hash corresponds to a record.
130
-
131
- You can also iterate through records one at a time:
132
-
133
- posts.each {|row| p row}
134
-
135
- Or perform more advanced stuff:
136
-
137
- posts.map(:id)
138
- posts.inject({}) {|h, r| h[r[:id]] = r[:name]}
139
-
140
- You can also retrieve the first record in a dataset:
141
-
142
- posts.first
143
-
144
- Or retrieve a single record with a specific value:
145
-
146
- posts[:id => 1]
147
-
148
- If the dataset is ordered, you can also ask for the last record:
149
-
150
- posts.order(:stamp).last
151
-
152
- === Filtering Records
153
-
154
- The simplest way to filter records is to provide a hash of values to match:
155
-
156
- my_posts = posts.filter(:category => 'ruby', :author => 'david')
157
-
158
- You can also specify ranges:
159
-
160
- my_posts = posts.filter(:stamp => (2.weeks.ago)..(1.week.ago))
161
-
162
- Or lists of values:
163
-
164
- my_posts = posts.filter(:category => ['ruby', 'postgres', 'linux'])
165
-
166
- Sequel now also accepts expressions as closures, AKA block filters:
167
-
168
- my_posts = posts.filter {:category == ['ruby', 'postgres', 'linux']}
169
-
170
- Which also lets you do stuff like:
171
-
172
- my_posts = posts.filter {:stamp > 1.month.ago}
173
-
174
- Some adapters (like postgresql) will also let you specify Regexps:
175
-
176
- my_posts = posts.filter(:category => /ruby/i)
177
-
178
- You can also use an inverse filter:
179
-
180
- my_posts = posts.exclude(:category => /ruby/i)
181
-
182
- You can then retrieve the records by using any of the retrieval methods:
183
-
184
- my_posts.each {|row| p row}
185
-
186
- You can also specify a custom WHERE clause:
187
-
188
- posts.filter('(stamp < ?) AND (author <> ?)', 3.days.ago, author_name)
189
-
190
- Datasets can also be used as subqueries:
191
-
192
- DB[:items].filter('price > ?', DB[:items].select('AVG(price) + 100'))
193
-
194
- === Summarizing Records
195
-
196
- Counting records is easy:
197
- posts.filter(:category => /ruby/i).count
198
-
199
- And you can also query maximum/minimum values:
200
- max_value = DB[:history].max(:value)
201
-
202
- Or calculate a sum:
203
- total = DB[:items].sum(:price)
204
-
205
- === Ordering Records
206
-
207
- posts.order(:stamp)
208
-
209
- You can also specify descending order
210
-
211
- posts.order(:stamp.DESC)
212
-
213
- === Deleting Records
214
-
215
- posts.filter('stamp < ?', 3.days.ago).delete
216
-
217
- === Inserting Records
218
-
219
- posts.insert(:category => 'ruby', :author => 'david')
220
-
221
- Or alternatively:
222
-
223
- posts << {:category => 'ruby', :author => 'david'}
224
-
225
- === Updating Records
226
-
227
- posts.filter('stamp < ?', 3.days.ago).update(:state => 'archived')
228
-
229
- === Joining Tables
230
-
231
- Joining is very useful in a variety of scenarios, for example many-to-many relationships. With Sequel it's really easy:
232
-
233
- order_items = DB[:items].join(:order_items, :item_id => :id).
234
- filter(:order_items__order_id => 1234)
235
-
236
- This is equivalent to the SQL:
237
-
238
- SELECT * FROM items LEFT OUTER JOIN order_items
239
- ON order_items.item_id = items.id
240
- WHERE order_items.order_id = 1234
241
-
242
- You can then do anything you like with the dataset:
243
-
244
- order_total = order_items.sum(:price)
245
-
246
- Which is equivalent to the SQL:
247
-
248
- SELECT sum(price) FROM items LEFT OUTER JOIN order_items
249
- ON order_items.item_id = items.id
250
- WHERE order_items.order_id = 1234
251
-
81
+ You can find more information on getting started with Sequel {here}[http://code.google.com/p/ruby-sequel/wiki/GettingStarted]
data/Rakefile CHANGED
@@ -9,11 +9,11 @@ include FileUtils
9
9
  # Configuration
10
10
  ##############################################################################
11
11
  NAME = "sequel"
12
- VERS = "0.4.5"
12
+ VERS = "0.5"
13
13
  CLEAN.include ["**/.*.sw?", "pkg/*", ".config", "doc/*", "coverage/*"]
14
14
  RDOC_OPTS = [
15
15
  "--quiet",
16
- "--title", "Sequel: Concise ORM for Ruby",
16
+ "--title", "Sequel: Database access for Ruby",
17
17
  "--opname", "index.html",
18
18
  "--line-numbers",
19
19
  "--main", "README",
@@ -29,7 +29,7 @@ Rake::RDocTask.new do |rdoc|
29
29
  rdoc.rdoc_dir = "doc/rdoc"
30
30
  rdoc.options += RDOC_OPTS
31
31
  rdoc.main = "README"
32
- rdoc.title = "Sequel: Lightweight ORM library for Ruby"
32
+ rdoc.title = "Sequel: Database access for Ruby"
33
33
  rdoc.rdoc_files.add ["README", "COPYING", "lib/sequel.rb", "lib/**/*.rb"]
34
34
  end
35
35
 
@@ -49,7 +49,7 @@ spec = Gem::Specification.new do |s|
49
49
  s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
50
50
  s.rdoc_options += RDOC_OPTS +
51
51
  ["--exclude", "^(examples|extras)\/", "--exclude", "lib/sequel.rb"]
52
- s.summary = "Lightweight ORM library for Ruby"
52
+ s.summary = "Database access for Ruby"
53
53
  s.description = s.summary
54
54
  s.author = "Sharon Rosner"
55
55
  s.email = "ciconia@gmail.com"
@@ -57,9 +57,6 @@ spec = Gem::Specification.new do |s|
57
57
  s.executables = ["sequel"]
58
58
  s.required_ruby_version = ">= 1.8.4"
59
59
 
60
- # Instead of requiring this, how about we simply use it if it's available
61
- # by rescuing LoadError where we require it in model/validations.rb?
62
- # s.add_dependency("validatable")
63
60
  case RUBY_PLATFORM
64
61
  when /mswin/
65
62
  s.platform = Gem::Platform::CURRENT
@@ -100,20 +97,18 @@ task :uninstall => [:clean] do
100
97
  sh %{sudo gem uninstall #{NAME}}
101
98
  end
102
99
 
100
+ task :tag do
101
+ cwd = FileUtils.pwd
102
+ sh %{cd ../.. && svn copy #{cwd} tags/#{NAME}-#{VERS} && svn commit -m "#{NAME}-#{VERS} tag." tags}
103
+ end
104
+
103
105
  ##############################################################################
104
106
  # gem and rdoc release
105
107
  ##############################################################################
106
108
  task :release => [:package] do
107
109
  sh %{rubyforge login}
108
- sh %{rubyforge add_release sequel sequel #{VERS} pkg/sequel-#{VERS}.tgz}
109
- sh %{rubyforge add_file sequel sequel #{VERS} pkg/sequel-#{VERS}.gem}
110
- sh %{rubyforge add_file sequel sequel #{VERS} pkg/sequel-#{VERS}-x86-mswin32-60.gem}
111
- end
112
-
113
- desc "Update docs and upload to rubyforge.org"
114
- task :doc_rforge do
115
- sh %{rake doc}
116
- sh %{scp -r doc/rdoc/* ciconia@rubyforge.org:/var/www/gforge-projects/sequel}
110
+ sh %{rubyforge add_release sequel #{NAME} #{VERS} pkg/#{NAME}-#{VERS}.tgz}
111
+ sh %{rubyforge add_file sequel #{NAME} #{VERS} pkg/#{NAME}-#{VERS}.gem}
117
112
  end
118
113
 
119
114
  ##############################################################################
@@ -1,325 +1,8 @@
1
1
  module Sequel
2
- # == Sequel Models
3
- #
4
- # Models in Sequel are based on the Active Record pattern described by Martin Fowler (http://www.martinfowler.com/eaaCatalog/activeRecord.html). A model class corresponds to a table or a dataset, and an instance of that class wraps a single record in the model's underlying dataset.
5
- #
6
- # Model classes are defined as regular Ruby classes:
7
- #
8
- # DB = Sequel('sqlite:/blog.db')
9
- # class Post < Sequel::Model
10
- # set_dataset DB[:posts]
11
- # end
12
- #
13
- # You can also use the shorthand form:
14
- #
15
- # DB = Sequel('sqlite:/blog.db')
16
- # class Post < Sequel::Model(:posts)
17
- # end
18
- #
19
- # === Model instances
20
- #
21
- # Model instance are identified by a primary key. By default, Sequel assumes the primary key column to be :id. The Model#[] method can be used to fetch records by their primary key:
22
- #
23
- # post = Post[123]
24
- #
25
- # The Model#pk method is used to retrieve the record's primary key value:
26
- #
27
- # post.pk #=> 123
28
- #
29
- # Sequel models allow you to use any column as a primary key, and even composite keys made from multiple columns:
30
- #
31
- # class Post < Sequel::Model(:posts)
32
- # set_primary_key [:category, :title]
33
- # end
34
- #
35
- # post = Post['ruby', 'hello world']
36
- # post.pk #=> ['ruby', 'hello world']
37
- #
38
- # You can also define a model class that does not have a primary key, but then you lose the ability to update records.
39
- #
40
- # A model instance can also be fetched by specifying a condition:
41
- #
42
- # post = Post[:title => 'hello world']
43
- # post = Post.find {:stamp < 10.days.ago}
44
- #
45
- # === Iterating over records
46
- #
47
- # A model class lets you iterate over specific records by acting as a proxy to the underlying dataset. This means that you can use the entire Dataset API to create customized queries that return model instances, e.g.:
48
- #
49
- # Post.filter(:category => 'ruby').each {|post| p post}
50
- #
51
- # You can also manipulate the records in the dataset:
52
- #
53
- # Post.filter {:stamp < 7.days.ago}.delete
54
- # Post.filter {:title =~ /ruby/}.update(:category => 'ruby')
55
- #
56
- # === Accessing record values
57
- #
58
- # A model instances stores its values as a hash:
59
- #
60
- # post.values #=> {:id => 123, :category => 'ruby', :title => 'hello world'}
61
- #
62
- # You can read the record values as object attributes:
63
- #
64
- # post.id #=> 123
65
- # post.title #=> 'hello world'
66
- #
67
- # You can also change record values:
68
- #
69
- # post.title = 'hey there'
70
- # post.save
71
- #
72
- # Another way to change values by using the #set method:
73
- #
74
- # post.set(:title => 'hey there')
75
- #
76
- # === Creating new records
77
- #
78
- # New records can be created by calling Model.create:
79
- #
80
- # post = Post.create(:title => 'hello world')
81
- #
82
- # Another way is to construct a new instance and save it:
83
- #
84
- # post = Post.new
85
- # post.title = 'hello world'
86
- # post.save
87
- #
88
- # You can also supply a block to Model.new and Model.create:
89
- #
90
- # post = Post.create {|p| p.title = 'hello world'}
91
- #
92
- # post = Post.new do |p|
93
- # p.title = 'hello world'
94
- # p.save
95
- # end
96
- #
97
- # === Hooks
98
- #
99
- # You can execute custom code when creating, updating, or deleting records by using hooks. The before_create and after_create hooks wrap record creation. The before_update and after_update wrap record updating. The before_save and after_save wrap record creation and updating. The before_destroy and after_destroy wrap destruction.
100
- #
101
- # Hooks are defined by supplying a block:
102
- #
103
- # class Post < Sequel::Model(:posts)
104
- # after_create do
105
- # set(:created_at => Time.now)
106
- # end
107
- #
108
- # after_destroy
109
- # author.update_post_count
110
- # end
111
- # end
112
- #
113
- # === Deleting records
114
- #
115
- # You can delete individual records by calling #delete or #destroy. The only difference between the two methods is that #destroy invokes before_destroy and after_destroy hooks, while #delete does not:
116
- #
117
- # post.delete #=> bypasses hooks
118
- # post.destroy #=> runs hooks
119
- #
120
- # Records can also be deleted en-masse by invoking Model.delete and Model.destroy. As stated above, you can specify filters for the deleted records:
121
- #
122
- # Post.filter(:category => 32).delete #=> bypasses hooks
123
- # Post.filter(:category => 32).destroy #=> runs hooks
124
- #
125
- # Please note that if Model.destroy is called, each record is deleted separately, but Model.delete deletes all relevant records with a single SQL statement.
126
- #
127
- # === Associations
128
- #
129
- # The most straightforward way to define an association in a Sequel model is as a regular instance method:
130
- #
131
- # class Post < Sequel::Model(:posts)
132
- # def author; Author[author_id]; end
133
- # end
134
- #
135
- # class Author < Sequel::Model(:authors)
136
- # def posts; Post.filter(:author_id => pk); end
137
- # end
138
- #
139
- # Sequel also provides two macros to assist with common types of associations. The one_to_one macro is roughly equivalent to ActiveRecord?'s belongs_to macro. It defines both getter and setter methods for the association:
140
- #
141
- # class Post < Sequel::Model(:posts)
142
- # one_to_one :author, :from => Author
143
- # end
144
- #
145
- # post = Post.create(:name => 'hi!')
146
- # post.author = Author[:name => 'Sharon']
147
- #
148
- # The one_to_many macro is roughly equivalent to ActiveRecord's has_many macro:
149
- #
150
- # class Author < Sequel::Model(:authors)
151
- # one_to_many :posts, :from => Post, :key => :author_id
152
- # end
153
- #
154
- # You will have noticed that in some cases the association macros are actually more verbose than hand-coding instance methods. The one_to_one and one_to_many macros also make assumptions (just like ActiveRecord macros) about the database schema which may not be relevant in many cases.
155
- #
156
- # === Caching model instances with memcached
157
- #
158
- # Sequel models can be cached using memcached based on their primary keys. The use of memcached can significantly reduce database load by keeping model instances in memory. The set_cache method is used to specify caching:
159
- #
160
- # require 'memcache'
161
- # CACHE = MemCache.new 'localhost:11211', :namespace => 'blog'
162
- #
163
- # class Author < Sequel::Model(:authors)
164
- # set_cache CACHE, :ttl => 3600
165
- # end
166
- #
167
- # Author[333] # database hit
168
- # Author[333] # cache hit
169
- #
170
- # === Extending the underlying dataset
171
- #
172
- # The obvious way to add table-wide logic is to define class methods to the model class definition. That way you can define subsets of the underlying dataset, change the ordering, or perform actions on multiple records:
173
- #
174
- # class Post < Sequel::Model(:posts)
175
- # def self.old_posts
176
- # filter {:stamp < 30.days.ago}
177
- # end
178
- #
179
- # def self.clean_old_posts
180
- # old_posts.delete
181
- # end
182
- # end
183
- #
184
- # You can also implement table-wide logic by defining methods on the dataset:
185
- #
186
- # class Post < Sequel::Model(:posts)
187
- # def dataset.old_posts
188
- # filter {:stamp < 30.days.ago}
189
- # end
190
- #
191
- # def dataset.clean_old_posts
192
- # old_posts.delete
193
- # end
194
- # end
195
- #
196
- # This is the recommended way of implementing table-wide operations, and allows you to have access to your model API from filtered datasets as well:
197
- #
198
- # Post.filter(:category => 'ruby').clean_old_posts
199
- #
200
- # Sequel models also provide a short hand notation for filters:
201
- #
202
- # class Post < Sequel::Model(:posts)
203
- # subset(:old_posts) {:stamp < 30.days.ago}
204
- # subset :invisible, :visible => false
205
- # end
206
- #
207
- # === Defining the underlying schema
208
- #
209
- # Model classes can also be used as a place to define your table schema and control it. The schema DSL is exactly the same provided by Sequel::Schema::Generator:
210
- #
211
- # class Post < Sequel::Model(:posts)
212
- # set_schema do
213
- # primary_key :id
214
- # text :title
215
- # text :category
216
- # foreign_key :author_id, :table => authors
217
- # end
218
- # end
219
- #
220
- # You can then create the underlying table, drop it, or recreate it:
221
- #
222
- # Post.table_exists?
223
- # Post.create_table
224
- # Post.drop_table
225
- # Post.create_table! # drops the table if it exists and then recreates it
226
- #
227
2
  class Model
228
- alias_method :model, :class
229
- end
230
-
231
- end
232
-
233
- require File.join(File.dirname(__FILE__), "model/base")
234
- require File.join(File.dirname(__FILE__), "model/hooks")
235
- require File.join(File.dirname(__FILE__), "model/record")
236
- require File.join(File.dirname(__FILE__), "model/schema")
237
- require File.join(File.dirname(__FILE__), "model/relations")
238
- require File.join(File.dirname(__FILE__), "model/caching")
239
- require File.join(File.dirname(__FILE__), "model/plugins")
240
- require File.join(File.dirname(__FILE__), "model/validations")
241
-
242
- module Sequel
243
-
244
- class Model
245
-
246
- # Defines a method that returns a filtered dataset.
247
- def self.subset(name, *args, &block)
248
- dataset.meta_def(name) {filter(*args, &block)}
249
- end
250
-
251
- # Finds a single record according to the supplied filter, e.g.:
252
- #
253
- # Ticket.find :author => 'Sharon' # => record
254
- # Ticket.find {:price == 17} # => Dataset
255
- #
256
- def self.find(*args, &block)
257
- dataset.filter(*args, &block).first
258
- end
259
-
260
- # TODO: doc
261
- def self.[](*args)
262
- args = args.first if (args.size == 1)
263
- if args === true || args === false
264
- raise Error::InvalidFilter, "Did you mean to supply a hash?"
265
- end
266
- dataset[(Hash === args) ? args : primary_key_hash(args)]
267
- end
268
-
269
- # TODO: doc
270
- def self.fetch(*args)
271
- db.fetch(*args).set_model(self)
272
- end
273
-
274
- # Like find but invokes create with given conditions when record does not
275
- # exists.
276
- def self.find_or_create(cond)
277
- find(cond) || create(cond)
278
- end
279
-
280
- ############################################################################
281
-
282
- # Deletes all records in the model's table.
283
- def self.delete_all
284
- dataset.delete
285
- end
286
-
287
- # Like delete_all, but invokes before_destroy and after_destroy hooks if used.
288
- def self.destroy_all
289
- if has_hooks?(:before_destroy) || has_hooks?(:after_destroy)
290
- dataset.destroy
291
- else
292
- dataset.delete
293
- end
294
- end
295
-
296
- def self.is_dataset_magic_method?(m)
297
- method_name = m.to_s
298
- Sequel::Dataset::MAGIC_METHODS.each_key do |r|
299
- return true if method_name =~ r
300
- end
301
- false
302
- end
303
-
304
- def self.method_missing(m, *args, &block) #:nodoc:
305
- Thread.exclusive do
306
- if dataset.respond_to?(m) || is_dataset_magic_method?(m)
307
- instance_eval("def #{m}(*args, &block); dataset.#{m}(*args, &block); end")
308
- end
309
- end
310
- respond_to?(m) ? send(m, *args, &block) : super(m, *args)
311
- end
312
-
313
- # TODO: Comprehensive description goes here!
314
- def self.join(*args)
315
- table_name = dataset.opts[:from].first
316
- dataset.join(*args).select(table_name.to_sym.ALL)
317
- end
318
-
319
- # Returns an array containing all of the models records.
320
- def self.all
321
- dataset.all
3
+ def self.database_opened(db)
4
+ @db = db if (self == Model) && !@db
322
5
  end
323
6
  end
324
-
325
7
  end
8
+