sequel 0.3.0.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,29 @@
1
+ === 0.3.1 (2007-10-30)
2
+
3
+ * Typo fixes (#79).
4
+
5
+ * Added require 'yaml' to dataset.rb (#78).
6
+
7
+ * Changed postgres adapter to use the ruby-postgres library's type conversion if available (#76).
8
+
9
+ * Fixed string literalization in mysql adapter for strings with comment backslashes in them (#75).
10
+
11
+ * Fixed ParseTree dependency to work with version 2.0.0 and later (#74).
12
+
13
+ * foreign_key definitions now accept :key option for specifying the remote key (#73).
14
+
15
+ * Fixed Model#method_missing to not raise error for columns not in the table but for which a value exists (#77).
16
+
17
+ * New documentation for Model.
18
+
19
+ * Implemented Oracle adapter based on ruby-oci8 library.
20
+
21
+ * Implemented Model#pk_hash. Is it really necessary?
22
+
23
+ * Deprecated Model#pkey. Implemented better Model#pk method.
24
+
25
+ * Specs and docs for Model.one_to_one, Model.one_to_many macros.
26
+
1
27
  === 0.3.0.1 (2007-10-20)
2
28
 
3
29
  * Changed Database#fetch to return a modified dataset.
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "sequel"
9
- VERS = "0.3.0.1"
9
+ VERS = "0.3.1"
10
10
  CLEAN.include ['**/.*.sw?', 'pkg/*', '.config', 'doc/*', 'coverage/*']
11
11
  RDOC_OPTS = ['--quiet', '--title', "Sequel: Concise ORM for Ruby",
12
12
  "--opname", "index.html",
@@ -44,7 +44,7 @@ spec = Gem::Specification.new do |s|
44
44
  s.executables = ['sequel']
45
45
 
46
46
  s.add_dependency('metaid')
47
- s.add_dependency('ParseTree')
47
+ s.add_dependency('ParseTree', '>= 2.0.0')
48
48
  s.add_dependency('ruby2ruby')
49
49
 
50
50
  s.required_ruby_version = '>= 1.8.4'
@@ -1,5 +1,6 @@
1
1
  require 'time'
2
2
  require 'date'
3
+ require 'yaml'
3
4
 
4
5
  require File.join(File.dirname(__FILE__), 'dataset/sql')
5
6
  require File.join(File.dirname(__FILE__), 'dataset/sequelizer')
@@ -443,15 +443,15 @@ module Sequel
443
443
  else
444
444
  values = values[0] if values.size == 1
445
445
  case values
446
+ when Sequel::Model
447
+ insert_sql(values.values)
446
448
  when Array
447
- if values.fields
448
- if values.empty?
449
- "INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
450
- else
451
- fl = values.fields
452
- vl = transform_save(values.values).map {|v| literal(v)}
453
- "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)});"
454
- end
449
+ if values.empty?
450
+ "INSERT INTO #{@opts[:from]} DEFAULT VALUES;"
451
+ elsif values.fields
452
+ fl = values.fields
453
+ vl = transform_save(values.values).map {|v| literal(v)}
454
+ "INSERT INTO #{@opts[:from]} (#{fl.join(COMMA_SEPARATOR)}) VALUES (#{vl.join(COMMA_SEPARATOR)});"
455
455
  else
456
456
  "INSERT INTO #{@opts[:from]} VALUES (#{literal(values)});"
457
457
  end
data/lib/sequel/model.rb CHANGED
@@ -1,154 +1,217 @@
1
1
  module Sequel
2
- # == Cheatsheet:
3
- # class Item < Sequel::Model(:items)
4
- # set_schema do
5
- # primary_key :id
6
- # text :name, :unique => true, :null => false
7
- # boolean :active, :default => true
8
- # integer :grade
9
- #
10
- # index :grade
11
- # end
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]
12
11
  # end
13
- #
14
- # Item.create_table unless Item.table_exists?
15
- # Item.create_table!
16
- #
17
- # i = Item.create(:name => 'Shoes', :grade => 0)
18
- #
19
- # Item[1].grade #=> 0
20
- #
21
- # i.set(:grade => 2)
22
- # i.grade # => 2
23
- #
24
- # Item[:name => 'Shoes'].grade # => 2
25
- #
26
- # i.grade = 4
27
- # Item[1].grade # => 2
28
- # i.save
29
- # Item[1].grade # => 4
30
- #
31
- # == Subsets
32
- # Subsets are filter mapped to class methods:
33
- #
34
- # class Ticket < Sequel::Model(:tickets)
35
- #
36
- # subset(:pending) { finished_at == nil }
37
- # subset(:closed) { finished_at != nil }
38
- #
39
- # # ...
40
- #
12
+ #
13
+ # You can also use the shorthand form:
14
+ #
15
+ # DB = Sequel('sqlite:/blog.db')
16
+ # class Post < Sequel::Model(:posts)
41
17
  # end
42
- #
43
- # Now you can do:
44
- #
45
- # Ticket.pending.each { |ticket| puts ticket.caption }
46
- #
47
- # == Advanced filtering methods (or dataset magic)
48
- # One of the cool features of Sequel::Model is that it acts as a proxy to
49
- # the underlying dataset, so you can invoke methods on the class instead of
50
- # on the dataset:
51
- #
52
- # Customer.filter(:name =~ 'Roberts')
53
- #
54
- # In the prevailing style of implementing models (which is actually very
55
- # similar to ActiveRecord models) table-wide operations are defined as
56
- # class methods:
57
- #
58
- # class Node < Sequel::Model(:nodes)
59
- # def self.subtree(path)
60
- # filter(:path => Regexp.new("^#{path}(/.+)?$"))
61
- # end
62
- # def self.alarms
63
- # filter {:kind => ALARM}
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
+ # You can also create a new instance and save it:
83
+ #
84
+ # post = Post.new
85
+ # post.title = 'hello world'
86
+ # post.save
87
+ #
88
+ # === Hooks
89
+ #
90
+ # 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.
91
+ #
92
+ # Hooks are defined by supplying a block:
93
+ #
94
+ # class Post < Sequel::Model(:posts)
95
+ # after_create do
96
+ # set(:created_at => Time.now)
64
97
  # end
65
- # def self.recalculate
66
- # exclude(:expression => nil).each {|n| n.calculate}
98
+ #
99
+ # after_destroy
100
+ # author.update_post_count
67
101
  # end
68
102
  # end
69
- #
70
- # The recalculate class method calls the exclude method. The exclude
71
- # call is proxied to the underlying dataset, which lets you call each
72
- # method separately:
73
- #
74
- # Node.subtree('/test')
75
- # Node.alarms
76
- # Node.recalculate
77
- #
78
- # ... but this will raise a NoMethodError:
79
- #
80
- # Node.subtree('/test').alarms.recalculate
81
- #
82
- # It turns out the solution is very simple - instead of defining class
83
- # methods, define dataset methods:
84
- #
85
- # class Node < Sequel::Model(:nodes)
86
- # def dataset.subtree(path)
87
- # filter(:path => Regexp.new("^#{path}(/.+)?$"))
103
+ #
104
+ # === Deleting records
105
+ #
106
+ # 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:
107
+ #
108
+ # post.delete #=> bypasses hooks
109
+ # post.destroy #=> runs hooks
110
+ #
111
+ # 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:
112
+ #
113
+ # Post.filter(:category => 32).delete #=> bypasses hooks
114
+ # Post.filter(:category => 32).destroy #=> runs hooks
115
+ #
116
+ # 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.
117
+ #
118
+ # === Associations
119
+ #
120
+ # The most straightforward way to define an association in a Sequel model is as a regular instance method:
121
+ #
122
+ # class Post < Sequel::Model(:posts)
123
+ # def author; Author[author_id]; end
124
+ # end
125
+ #
126
+ # class Author < Sequel::Model(:authors)
127
+ # def posts; Post.filter(:author_id => pk); end
128
+ # end
129
+ #
130
+ # 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:
131
+ #
132
+ # class Post < Sequel::Model(:posts)
133
+ # one_to_one :author, :from => Author
134
+ # end
135
+ #
136
+ # The one_to_many macro is roughly equivalent to ActiveRecord's has_many macro:
137
+ #
138
+ # class Author < Sequel::Model(:authors)
139
+ # one_to_many :posts, :from => Post, :key => :author_id
140
+ # end
141
+ #
142
+ # 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.
143
+ #
144
+ # === Caching model instances with memcached
145
+ #
146
+ # 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:
147
+ #
148
+ # require 'memcache'
149
+ # CACHE = MemCache.new 'localhost:11211', :namespace => 'blog'
150
+ #
151
+ # class Author < Sequel::Model(:authors)
152
+ # set_cache CACHE, :ttl => 3600
153
+ # end
154
+ #
155
+ # Author[333] # database hit
156
+ # Author[333] # cache hit
157
+ #
158
+ # === Extending the underlying dataset
159
+ #
160
+ # 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:
161
+ #
162
+ # class Post < Sequel::Model(:posts)
163
+ # def self.old_posts
164
+ # filter {:stamp < 30.days.ago}
88
165
  # end
89
- # def dataset.alarms
90
- # filter {:kind => ALARM}
166
+ #
167
+ # def self.clean_old_posts
168
+ # old_posts.delete
169
+ # end
170
+ # end
171
+ #
172
+ # You can also implement table-wide logic by defining methods on the dataset:
173
+ #
174
+ # class Post < Sequel::Model(:posts)
175
+ # def dataset.old_posts
176
+ # filter {:stamp < 30.days.ago}
91
177
  # end
92
- # def dataset.recalculate
93
- # exclude(:expression => nil).each {|n| n.calculate}
178
+ #
179
+ # def dataset.clean_old_posts
180
+ # old_posts.delete
94
181
  # end
95
182
  # end
96
- #
97
- # Now you can mix all of these methods any way you like:
98
- #
99
- # Node.filter {:stamp < Time.now < 3600}.alarms
100
- # Node.filter(:project_id => 123).subtree('/abc')
101
- # Node.subtree('/test').recalculate
102
- # # ...
103
- #
104
- # == Schemas
105
- # You can define your schema in the Model class itself:
106
- #
107
- # class Comment < Sequel::Model(:comments)
183
+ #
184
+ # 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:
185
+ #
186
+ # Post.filter(:category => 'ruby').clean_old_posts
187
+ #
188
+ # Sequel models also provide a short hand notation for filters:
189
+ #
190
+ # class Post < Sequel::Model(:posts)
191
+ # subset(:old_posts) {:stamp < 30.days.ago}
192
+ # subset :invisible, :visible => false
193
+ # end
194
+ #
195
+ # === Defining the underlying schema
196
+ #
197
+ # 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:
198
+ #
199
+ # class Post < Sequel::Model(:posts)
108
200
  # set_schema do
109
201
  # primary_key :id
110
- # foreign_key :post_id, :table => :posts, :on_delete => :cascade
111
- #
112
- # varchar :name
113
- # varchar :email
114
- # text :comment
202
+ # text :title
203
+ # text :category
204
+ # foreign_key :author_id, :table => authors
115
205
  # end
116
- #
117
- # # ...
118
- #
119
- # end
120
- #
121
- # == Hooks
122
- # You can setup hooks here:
123
- # * before_save calls either
124
- # * before_create with
125
- # * after_create or if record already exists
126
- # * before_update with
127
- # * after_update and finally
128
- # * after_save
129
- # ... and here:
130
- # * before_destroy with
131
- # * after_destroy
132
- #
133
- # ...with:
134
- #
135
- # class Example < Sequel::Model(:hooks)
136
- # before_create { self.created_at = Time.now }
137
- #
138
- # # ...
139
- # end
140
- #
141
- # == Serialization of complexe attributes
142
- # Sometimes there are datatypes you can't natively map to your db. In this
143
- # case you can just do serialize:
144
- #
145
- # class Serialized < Sequel::Model(:serialized)
146
- # serialize :column1, :format => :yaml # YAML is the default serialization method
147
- # serialize :column2, :format => :marshal # serializes through marshalling
148
- #
149
- # # ...
150
- #
151
206
  # end
207
+ #
208
+ # You can then create the underlying table, drop it, or recreate it:
209
+ #
210
+ # Post.table_exists?
211
+ # Post.create_table
212
+ # Post.drop_table
213
+ # Post.create_table! # drops the table if it exists and then recreates it
214
+ #
152
215
  class Model
153
216
  alias_method :model, :class
154
217
  end
@@ -178,7 +241,7 @@ module Sequel
178
241
  # Finds a single record according to the supplied filter, e.g.:
179
242
  #
180
243
  # Ticket.find :author => 'Sharon' # => record
181
- # Ticket.find {:price}17 # => Dataset
244
+ # Ticket.find {:price == 17} # => Dataset
182
245
  #
183
246
  def self.find(*args, &block)
184
247
  dataset.filter(*args, &block).limit(1).first
@@ -263,13 +326,14 @@ module Sequel
263
326
  @values[:id]
264
327
  end
265
328
 
266
- # Compares models by values.
329
+ # Compares model instances by values.
267
330
  def ==(obj)
268
331
  (obj.class == model) && (obj.values == @values)
269
332
  end
270
- # Compares object by pkey.
333
+
334
+ # Compares model instances by pkey.
271
335
  def ===(obj)
272
- (obj.class == model) && (obj.pkey == pkey)
336
+ (obj.class == model) && (obj.pk == pk)
273
337
  end
274
338
 
275
339
  end