sequel 0.0.16 → 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/CHANGELOG +16 -0
  2. data/README +41 -12
  3. data/Rakefile +1 -1
  4. data/lib/sequel/dataset.rb +113 -32
  5. metadata +2 -2
data/CHANGELOG CHANGED
@@ -1,5 +1,21 @@
1
+ *0.0.17*
2
+
3
+ * Fixed Dataset#from to have variable arity, like Dataset#select and Dataset#where (patch by Alex Bradbury.)
4
+
5
+ * Added support for GROUP BY and HAVING clauses (patches by Alex Bradbury.) Refactored Dataset#filter.
6
+
7
+ * More specs.
8
+
9
+ * Refactored Dataset#where for better composability.
10
+
11
+ * Added Dataset#[]= method.
12
+
13
+ * Added support for DISTINCT and OFFSET clauses (patches by Alex Bradbury.) Dataset#limit now accepts ranges. Added Dataset#uniq and distinct methods.
14
+
1
15
  *0.0.16*
2
16
 
17
+ * More documentation.
18
+
3
19
  * Added support for subqueries in Dataset#literal.
4
20
 
5
21
  * Added support for Model.all_by_XXX methods through Model.method_missing.
data/README CHANGED
@@ -39,7 +39,36 @@ Sequel includes an IRB console for quick'n'dirty access to databases. You can us
39
39
 
40
40
  You get an IRB session with the database object stored in DB.
41
41
 
42
- == A Short Tutorial
42
+ == An Introduction
43
+
44
+ Sequel was designed to take the hassle away from connecting to databases and manipulating them. Sequel deals with all the boring stuff like maintaining connections, formatting SQL correctly and fetching records so you can concentrate on your application.
45
+
46
+ Sequel uses the concept of datasets to retrieve data. A Dataset object encapsulates an SQL query and supports chainability, letting you fetch data using a convenient Ruby DSL that is both concise and infinitely flexible.
47
+
48
+ For example, the following one-liner returns the average GDP for the five biggest countries in the middle east region:
49
+
50
+ DB[:countries].filter(:region => 'Middle East').reverse_order(:area).limit(5).avg(:GDP)
51
+
52
+ Which is equivalent to:
53
+
54
+ SELECT avg(GDP) FROM countries WHERE region = 'Middle East' ORDER BY area DESC LIMIT 5
55
+
56
+ Since datasets retrieve records only when needed, they can be stored and later reused. Records are fetched as hashes (they can also be fetched as custom model objects), and are accessed using an Enumerable interface:
57
+
58
+ middle_east = DB[:countries].filter(:region => 'Middle East')
59
+ middle_east.order(:name).each {|r| puts r[:name]}
60
+
61
+ Sequel also offers convenience methods for extracting data from Datasets, such as an extended map method:
62
+
63
+ middle_east.map(:name) #=> ['Egypt', 'Greece', 'Israel', ...]
64
+
65
+ Or getting results as a transposed hash, with one column as key and another as value:
66
+
67
+ middle_east.hash_column(:name, :area) #=> {'Israel' => 20000, 'Greece' => 120000, ...}
68
+
69
+ 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...
70
+
71
+ == Getting Started
43
72
 
44
73
  === Connecting to a database
45
74
 
@@ -49,18 +78,18 @@ Before connecting to a database, you should require the corresponding adaptor, f
49
78
 
50
79
  Note: you don't need to require 'sequel' separately before that, as each adapter requires 'sequel' if it hasn't yet been required.
51
80
 
52
- There are two ways to create a connection to a database. The easier way is to provide a connection URL:
81
+ To connect to a database you simply provide Sequel with a URL:
53
82
 
54
- DB = Sequel.open("sqlite:///blog.db")
55
-
56
- You can also specify optional parameters, such as the connection pool size:
83
+ DB = Sequel.open 'sqlite:///blog.db'
84
+
85
+ The connection URL can also include such stuff as the user name and password:
57
86
 
58
- DB = Sequel.open("postgres://postgres:postgres@localhost/my_db",
59
- :max_connections => 10)
87
+ DB = Sequel.open 'postgres://cico:12345@localhost:5432/mydb'
60
88
 
61
- The second, more verbose, way is to create an instance of a database class:
89
+ You can also specify optional parameters, such as the connection pool size, or a logger for logging SQL queries:
62
90
 
63
- DB = Sequel::Postgres::Database.new(:database => 'my_db', :host => 'localhost')
91
+ DB = Sequel.open("postgres://postgres:postgres@localhost/my_db",
92
+ :max_connections => 10, :logger => Logger.new('log/db.log'))
64
93
 
65
94
  === Arbitrary SQL queries
66
95
 
@@ -72,7 +101,7 @@ Or more succinctly:
72
101
  DB << "create table t (a text, b text)"
73
102
  DB << "insert into t values ('a', 'b')"
74
103
 
75
- === Creating Datasets
104
+ === Getting Dataset Instances
76
105
 
77
106
  Dataset is the primary means through which records are retrieved and manipulated. You can create an blank dataset by using the dataset method:
78
107
 
@@ -147,9 +176,9 @@ You can also specify a custom WHERE clause:
147
176
 
148
177
  posts.filter('(stamp < ?) AND (author <> ?)', 3.days.ago, author_name)
149
178
 
150
- Datasets can also be used as subqueries for filtering:
179
+ Datasets can also be used as subqueries:
151
180
 
152
- expensive_stuff = DB[:items].filter(:price => DB[:items].select('AVG(price) + 100'))
181
+ DB[:items].filter('price > ?', DB[:items].select('AVG(price) + 100'))
153
182
 
154
183
  === Summarizing Records
155
184
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'fileutils'
6
6
  include FileUtils
7
7
 
8
8
  NAME = "sequel"
9
- VERS = "0.0.16"
9
+ VERS = "0.0.17"
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",
@@ -36,7 +36,7 @@ module Sequel
36
36
  #
37
37
  # Sequel::Dataset is an abstract class that is not useful by itself. Each
38
38
  # database adaptor should provide a descendant class of Sequel::Dataset.
39
- def initialize(db, opts = {}, record_class = nil)
39
+ def initialize(db, opts = nil, record_class = nil)
40
40
  @db = db
41
41
  @opts = opts || {}
42
42
  @record_class = record_class
@@ -100,7 +100,7 @@ module Sequel
100
100
  case source
101
101
  when Array: source.join(COMMA_SEPARATOR)
102
102
  else source
103
- end
103
+ end
104
104
  end
105
105
 
106
106
  NULL = "NULL".freeze
@@ -149,7 +149,7 @@ module Sequel
149
149
  end
150
150
 
151
151
  # Formats a where clause.
152
- def where_list(where)
152
+ def where_list(where, parenthesize = false)
153
153
  case where
154
154
  when Hash:
155
155
  where.map {|kv| where_condition(*kv)}.join(AND_SEPARATOR)
@@ -157,7 +157,7 @@ module Sequel
157
157
  fmt = where.shift
158
158
  fmt.gsub('?') {|i| literal(where.shift)}
159
159
  else
160
- where
160
+ parenthesize ? "(#{where})" : where
161
161
  end
162
162
  end
163
163
 
@@ -171,7 +171,8 @@ module Sequel
171
171
  end
172
172
 
173
173
  # Returns a copy of the dataset with the source changed.
174
- def from(source)
174
+ def from(*source)
175
+ source = source.first if source.size == 1
175
176
  dup_merge(:from => source)
176
177
  end
177
178
 
@@ -181,6 +182,12 @@ module Sequel
181
182
  dup_merge(:select => fields)
182
183
  end
183
184
 
185
+ # Returns a copy of the dataset with the distinct option.
186
+ def uniq
187
+ dup_merge(:distinct => true)
188
+ end
189
+ alias distinct uniq
190
+
184
191
  # Returns a copy of the dataset with the order changed.
185
192
  def order(*order)
186
193
  dup_merge(:order => order)
@@ -200,26 +207,51 @@ module Sequel
200
207
  end
201
208
  end
202
209
 
210
+ # Returns a copy of the dataset with the results grouped by the value of
211
+ # the given fields
212
+ def group(*fields)
213
+ dup_merge(:group => fields)
214
+ end
215
+
203
216
  AND_WHERE = "%s AND %s".freeze
204
217
 
205
- # Returns a copy of the dataset with the where conditions changed.
206
- def where(*cond)
218
+ # Returns a copy of the dataset with the given conditions imposed upon it.
219
+ # If the query has been grouped, then the conditions are imposed in the
220
+ # HAVING clause. If not, then they are imposed in the WHERE clause.
221
+ def filter(*cond)
222
+ clause = (@opts[:group] ? :having : :where)
207
223
  cond = cond.first if cond.size == 1
208
- if @opts[:where]
209
- if @opts[:where].is_a?(Hash) && cond.is_a?(Hash)
210
- cond = @opts[:where].merge(cond)
211
- else
212
- cond = AND_WHERE % [where_list(@opts[:where]), where_list(cond)]
213
- end
224
+ if @opts[clause]
225
+ cond = AND_WHERE % [where_list(@opts[clause]), where_list(cond, true)]
214
226
  end
215
- dup_merge(:where => cond)
227
+ dup_merge(clause => where_list(cond))
216
228
  end
229
+
230
+ # Returns a copy of the dataset with the where conditions changed. Raises
231
+ # if the dataset has been grouped. See also #filter
232
+ def where(*cond)
233
+ if @opts[:group]
234
+ raise "Can't specify a WHERE clause once the dataset has been grouped"
235
+ else
236
+ filter(*cond)
237
+ end
238
+ end
239
+
240
+ # Returns a copy of the dataset with the having conditions changed. Raises
241
+ # if the dataset has not been grouped. See also #filter
242
+ def having(*cond)
243
+ unless @opts[:group]
244
+ raise "Can only specify a HAVING clause on a grouped dataset"
245
+ else
246
+ filter(*cond)
247
+ end
248
+ end
217
249
 
218
250
  NOT_WHERE = "NOT %s".freeze
219
251
 
220
252
  def exclude(*cond)
221
253
  cond = cond.first if cond.size == 1
222
- where(NOT_WHERE % where_list(cond))
254
+ filter(NOT_WHERE % where_list(cond))
223
255
  end
224
256
 
225
257
  LEFT_OUTER_JOIN = 'LEFT OUTER JOIN'.freeze
@@ -232,7 +264,6 @@ module Sequel
232
264
  :join_cond => cond)
233
265
  end
234
266
 
235
- alias_method :filter, :where
236
267
  alias_method :all, :to_a
237
268
 
238
269
  alias_method :enum_map, :map
@@ -266,9 +297,13 @@ module Sequel
266
297
  end
267
298
 
268
299
  SELECT = "SELECT %s FROM %s".freeze
300
+ SELECT_DISTINCT = "SELECT DISTINCT %s FROM %s".freeze
269
301
  LIMIT = " LIMIT %s".freeze
302
+ OFFSET = " OFFSET %s".freeze
270
303
  ORDER = " ORDER BY %s".freeze
271
304
  WHERE = " WHERE %s".freeze
305
+ GROUP = " GROUP BY %s".freeze
306
+ HAVING = " HAVING %s".freeze
272
307
  JOIN_CLAUSE = " %s %s ON %s".freeze
273
308
 
274
309
  EMPTY = ''.freeze
@@ -281,7 +316,7 @@ module Sequel
281
316
  fields = opts[:select]
282
317
  select_fields = fields ? field_list(fields) : WILDCARD
283
318
  select_source = source_list(opts[:from])
284
- sql = SELECT % [select_fields, select_source]
319
+ sql = (opts[:distinct] ? SELECT_DISTINCT : SELECT) % [select_fields, select_source]
285
320
 
286
321
  if join_type = opts[:join_type]
287
322
  join_table = opts[:join_table]
@@ -290,17 +325,28 @@ module Sequel
290
325
  end
291
326
 
292
327
  if where = opts[:where]
293
- sql << (WHERE % where_list(where))
328
+ sql << (WHERE % where)
294
329
  end
295
330
 
331
+ if group = opts[:group]
332
+ sql << (GROUP % group.join(COMMA_SEPARATOR))
333
+ end
334
+
296
335
  if order = opts[:order]
297
336
  sql << (ORDER % order.join(COMMA_SEPARATOR))
298
337
  end
299
338
 
339
+ if having = opts[:having]
340
+ sql << (HAVING % having)
341
+ end
342
+
300
343
  if limit = opts[:limit]
301
344
  sql << (LIMIT % limit)
345
+ if offset = opts[:offset]
346
+ sql << (OFFSET % offset)
347
+ end
302
348
  end
303
-
349
+
304
350
  sql
305
351
  end
306
352
 
@@ -335,14 +381,21 @@ module Sequel
335
381
  def update_sql(values, opts = nil)
336
382
  opts = opts ? @opts.merge(opts) : @opts
337
383
 
384
+ if opts[:group]
385
+ raise "Can't update a grouped dataset"
386
+ elsif opts[:from].is_a?(Array) && opts[:from].size > 1
387
+ raise "Can't update in a joined dataset"
388
+ end
389
+
338
390
  set_list = values.map {|kv| SET_FORMAT % [kv[0], literal(kv[1])]}.
339
391
  join(COMMA_SEPARATOR)
340
- update_clause = UPDATE % [opts[:from], set_list]
392
+ sql = UPDATE % [opts[:from], set_list]
341
393
 
342
- where = opts[:where]
343
- where_clause = where ? WHERE % where_list(where) : EMPTY
394
+ if where = opts[:where]
395
+ sql << WHERE % where_list(where)
396
+ end
344
397
 
345
- [update_clause, where_clause].join(SPACE)
398
+ sql
346
399
  end
347
400
 
348
401
  DELETE = "DELETE FROM %s".freeze
@@ -350,12 +403,19 @@ module Sequel
350
403
  def delete_sql(opts = nil)
351
404
  opts = opts ? @opts.merge(opts) : @opts
352
405
 
353
- delete_source = opts[:from]
354
-
355
- where = opts[:where]
356
- where_clause = where ? WHERE % where_list(where) : EMPTY
357
-
358
- [DELETE % delete_source, where_clause].join(SPACE)
406
+ if opts[:group]
407
+ raise "Can't delete from a grouped dataset"
408
+ elsif opts[:from].is_a?(Array) && opts[:from].size > 1
409
+ raise "Can't delete from a joined dataset"
410
+ end
411
+
412
+ sql = DELETE % opts[:from]
413
+
414
+ if where = opts[:where]
415
+ sql << WHERE % where_list(where)
416
+ end
417
+
418
+ sql
359
419
  end
360
420
 
361
421
  COUNT = "COUNT(*)".freeze
@@ -390,10 +450,22 @@ module Sequel
390
450
 
391
451
  LIMIT_1 = {:limit => 1}.freeze
392
452
 
393
- def limit(l)
394
- dup_merge(:limit => l)
453
+ # If given an integer, the dataset will contain only the first l results.
454
+ # If given a range, it will contain only those at offsets within that
455
+ # range. If a second argument is given, it is used as an offset.
456
+ def limit(l, o = nil)
457
+ if l.is_a? Range
458
+ lim = (l.exclude_end? ? l.last - l.first : l.last + 1 - l.first)
459
+ dup_merge(:limit => lim, :offset=>l.first)
460
+ elsif o
461
+ dup_merge(:limit => l, :offset => o)
462
+ else
463
+ dup_merge(:limit => l)
464
+ end
395
465
  end
396
-
466
+
467
+ # Returns the first record in the dataset. If the num argument is specified,
468
+ # an array is returned with the first <i>num</i> records.
397
469
  def first(num = 1)
398
470
  if num == 1
399
471
  first_record
@@ -402,9 +474,15 @@ module Sequel
402
474
  end
403
475
  end
404
476
 
477
+ # Returns the first record matching the condition.
405
478
  def [](condition)
406
479
  where(condition).first
407
480
  end
481
+
482
+ # Updates all records matching the condition with the values specified.
483
+ def []=(condition, values)
484
+ where(condition).update(values)
485
+ end
408
486
 
409
487
  def last(num = 1)
410
488
  raise 'No order specified' unless @opts[:order] || (opts && opts[:order])
@@ -420,6 +498,8 @@ module Sequel
420
498
  end
421
499
  end
422
500
 
501
+ # Deletes all records in the dataset one at a time by invoking the destroy
502
+ # method of the associated model class.
423
503
  def destroy
424
504
  raise 'Dataset not associated with model' unless @record_class
425
505
 
@@ -428,6 +508,7 @@ module Sequel
428
508
  count
429
509
  end
430
510
 
511
+ # Pretty prints the records in the dataset as plain-text table.
431
512
  def print(*columns)
432
513
  Sequel::PrettyTable.print(naked.all, columns.empty? ? nil : columns)
433
514
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: sequel
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.0.16
7
- date: 2007-04-08 00:00:00 +03:00
6
+ version: 0.0.17
7
+ date: 2007-04-10 00:00:00 +03:00
8
8
  summary: Concise ORM for Ruby.
9
9
  require_paths:
10
10
  - lib