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,409 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'date'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), 'dataset/sql')
|
6
|
+
require File.join(File.dirname(__FILE__), 'dataset/sequelizer')
|
7
|
+
require File.join(File.dirname(__FILE__), 'dataset/convenience')
|
8
|
+
|
9
|
+
module Sequel
|
10
|
+
# A Dataset represents a view of a the data in a database, constrained by
|
11
|
+
# specific parameters such as filtering conditions, order, etc. Datasets
|
12
|
+
# can be used to create, retrieve, update and delete records.
|
13
|
+
#
|
14
|
+
# Query results are always retrieved on demand, so a dataset can be kept
|
15
|
+
# around and reused indefinitely:
|
16
|
+
# my_posts = DB[:posts].filter(:author => 'david') # no records are retrieved
|
17
|
+
# p my_posts.all # records are now retrieved
|
18
|
+
# ...
|
19
|
+
# p my_posts.all # records are retrieved again
|
20
|
+
#
|
21
|
+
# In order to provide this functionality, dataset methods such as where,
|
22
|
+
# select, order, etc. return modified copies of the dataset, so you can
|
23
|
+
# use different datasets to access data:
|
24
|
+
# posts = DB[:posts]
|
25
|
+
# davids_posts = posts.filter(:author => 'david')
|
26
|
+
# old_posts = posts.filter('stamp < ?', 1.week.ago)
|
27
|
+
#
|
28
|
+
# Datasets are Enumerable objects, so they can be manipulated using any
|
29
|
+
# of the Enumerable methods, such as map, inject, etc.
|
30
|
+
#
|
31
|
+
# === The Dataset Adapter Interface
|
32
|
+
#
|
33
|
+
# Each adapter should define its own dataset class as a descendant of
|
34
|
+
# Sequel::Dataset. The following methods should be overriden by the adapter
|
35
|
+
# Dataset class (each method with the stock implementation):
|
36
|
+
#
|
37
|
+
# # Iterate over the results of the SQL query and call the supplied
|
38
|
+
# # block with each record (as a hash).
|
39
|
+
# def fetch_rows(sql, &block)
|
40
|
+
# @db.synchronize do
|
41
|
+
# r = @db.execute(sql)
|
42
|
+
# r.each(&block)
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# # Insert records.
|
47
|
+
# def insert(*values)
|
48
|
+
# @db.synchronize do
|
49
|
+
# @db.execute(insert_sql(*values)).last_insert_id
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# # Update records.
|
54
|
+
# def update(*args, &block)
|
55
|
+
# @db.synchronize do
|
56
|
+
# @db.execute(update_sql(*args, &block)).affected_rows
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# # Delete records.
|
61
|
+
# def delete(opts = nil)
|
62
|
+
# @db.synchronize do
|
63
|
+
# @db.execute(delete_sql(opts)).affected_rows
|
64
|
+
# end
|
65
|
+
# end
|
66
|
+
class Dataset
|
67
|
+
include Enumerable
|
68
|
+
include Sequelizer
|
69
|
+
include SQL
|
70
|
+
include Convenience
|
71
|
+
|
72
|
+
attr_reader :db
|
73
|
+
attr_accessor :opts
|
74
|
+
|
75
|
+
alias all to_a
|
76
|
+
alias size count
|
77
|
+
|
78
|
+
# Constructs a new instance of a dataset with a database instance, initial
|
79
|
+
# options and an optional record class. Datasets are usually constructed by
|
80
|
+
# invoking Database methods:
|
81
|
+
# DB[:posts]
|
82
|
+
# Or:
|
83
|
+
# DB.dataset # the returned dataset is blank
|
84
|
+
#
|
85
|
+
# Sequel::Dataset is an abstract class that is not useful by itself. Each
|
86
|
+
# database adaptor should provide a descendant class of Sequel::Dataset.
|
87
|
+
def initialize(db, opts = nil)
|
88
|
+
@db = db
|
89
|
+
@opts = opts || {}
|
90
|
+
@row_proc = nil
|
91
|
+
@transform = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a new instance of the dataset with with the give options merged.
|
95
|
+
def clone_merge(opts)
|
96
|
+
new_dataset = clone
|
97
|
+
new_dataset.set_options(@opts.merge(opts))
|
98
|
+
new_dataset
|
99
|
+
end
|
100
|
+
|
101
|
+
def set_options(opts) #:nodoc:
|
102
|
+
@opts = opts
|
103
|
+
@columns = nil
|
104
|
+
end
|
105
|
+
|
106
|
+
NOTIMPL_MSG = "This method must be overriden in Sequel adapters".freeze
|
107
|
+
|
108
|
+
# Executes a select query and fetches records, passing each record to the
|
109
|
+
# supplied block. Adapters should override this method.
|
110
|
+
def fetch_rows(sql, &block)
|
111
|
+
# @db.synchronize do
|
112
|
+
# r = @db.execute(sql)
|
113
|
+
# r.each(&block)
|
114
|
+
# end
|
115
|
+
raise NotImplementedError, NOTIMPL_MSG
|
116
|
+
end
|
117
|
+
|
118
|
+
# Inserts values into the associated table. Adapters should override this
|
119
|
+
# method.
|
120
|
+
def insert(*values)
|
121
|
+
# @db.synchronize do
|
122
|
+
# @db.execute(insert_sql(*values)).last_insert_id
|
123
|
+
# end
|
124
|
+
raise NotImplementedError, NOTIMPL_MSG
|
125
|
+
end
|
126
|
+
|
127
|
+
# Updates values for the dataset. Adapters should override this method.
|
128
|
+
def update(values, opts = nil)
|
129
|
+
# @db.synchronize do
|
130
|
+
# @db.execute(update_sql(values, opts)).affected_rows
|
131
|
+
# end
|
132
|
+
raise NotImplementedError, NOTIMPL_MSG
|
133
|
+
end
|
134
|
+
|
135
|
+
# Deletes the records in the dataset. Adapters should override this method.
|
136
|
+
def delete(opts = nil)
|
137
|
+
# @db.synchronize do
|
138
|
+
# @db.execute(delete_sql(opts)).affected_rows
|
139
|
+
# end
|
140
|
+
raise NotImplementedError, NOTIMPL_MSG
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns the columns in the result set in their true order. The stock
|
144
|
+
# implementation returns the content of @columns. If @columns is nil,
|
145
|
+
# a query is performed. Adapters are expected to fill @columns with the
|
146
|
+
# column information when a query is performed.
|
147
|
+
def columns
|
148
|
+
first unless @columns
|
149
|
+
@columns || []
|
150
|
+
end
|
151
|
+
|
152
|
+
# Inserts the supplied values into the associated table.
|
153
|
+
def <<(*args)
|
154
|
+
insert(*args)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Updates the dataset with the given values.
|
158
|
+
def set(*args, &block)
|
159
|
+
update(*args, &block)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Iterates over the records in the dataset
|
163
|
+
def each(opts = nil, &block)
|
164
|
+
fetch_rows(select_sql(opts), &block)
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
# Returns the the model classes associated with the dataset as a hash.
|
169
|
+
def model_classes
|
170
|
+
@opts[:models]
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns the column name for the polymorphic key.
|
174
|
+
def polymorphic_key
|
175
|
+
@opts[:polymorphic_key]
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a naked dataset clone - i.e. a dataset that returns records as
|
179
|
+
# hashes rather than model objects.
|
180
|
+
def naked
|
181
|
+
d = clone_merge(:naked => true, :models => nil, :polymorphic_key => nil)
|
182
|
+
d.set_model(nil)
|
183
|
+
d
|
184
|
+
end
|
185
|
+
|
186
|
+
# Associates or disassociates the dataset with a model. If no argument or
|
187
|
+
# nil is specified, the dataset is turned into a naked dataset and returns
|
188
|
+
# records as hashes. If a model class specified, the dataset is modified
|
189
|
+
# to return records as instances of the model class, e.g:
|
190
|
+
#
|
191
|
+
# class MyModel
|
192
|
+
# def initialize(values)
|
193
|
+
# @values = values
|
194
|
+
# ...
|
195
|
+
# end
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# dataset.set_model(MyModel)
|
199
|
+
#
|
200
|
+
# You can also provide additional arguments to be passed to the model's
|
201
|
+
# initialize method:
|
202
|
+
#
|
203
|
+
# class MyModel
|
204
|
+
# def initialize(values, options)
|
205
|
+
# @values = values
|
206
|
+
# ...
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# dataset.set_model(MyModel, :allow_delete => false)
|
211
|
+
#
|
212
|
+
# The dataset can be made polymorphic by specifying a column name as the
|
213
|
+
# polymorphic key and a hash mapping column values to model classes.
|
214
|
+
#
|
215
|
+
# dataset.set_model(:kind, {1 => Person, 2 => Business})
|
216
|
+
#
|
217
|
+
# You can also set a default model class to fall back on by specifying a
|
218
|
+
# class corresponding to nil:
|
219
|
+
#
|
220
|
+
# dataset.set_model(:kind, {nil => DefaultClass, 1 => Person, 2 => Business})
|
221
|
+
#
|
222
|
+
# To disassociate a model from the dataset, you can call the #set_model
|
223
|
+
# and specify nil as the class:
|
224
|
+
#
|
225
|
+
# dataset.set_model(nil)
|
226
|
+
#
|
227
|
+
def set_model(key, *args)
|
228
|
+
# pattern matching
|
229
|
+
case key
|
230
|
+
when nil # set_model(nil) => no
|
231
|
+
# no argument provided, so the dataset is denuded
|
232
|
+
@opts.merge!(:naked => true, :models => nil, :polymorphic_key => nil)
|
233
|
+
remove_row_proc
|
234
|
+
# extend_with_stock_each
|
235
|
+
when Class
|
236
|
+
# isomorphic model
|
237
|
+
@opts.merge!(:naked => nil, :models => {nil => key}, :polymorphic_key => nil)
|
238
|
+
set_row_proc {|h| key.new(h, *args)}
|
239
|
+
extend_with_destroy
|
240
|
+
when Symbol
|
241
|
+
# polymorphic model
|
242
|
+
hash = args.shift || raise(ArgumentError, "No class hash supplied for polymorphic model")
|
243
|
+
@opts.merge!(:naked => true, :models => hash, :polymorphic_key => key)
|
244
|
+
set_row_proc do |h|
|
245
|
+
c = hash[h[key]] || hash[nil] || \
|
246
|
+
raise(Error, "No matching model class for record (#{polymorphic_key} => #{h[polymorphic_key].inspect})")
|
247
|
+
c.new(h, *args)
|
248
|
+
end
|
249
|
+
extend_with_destroy
|
250
|
+
else
|
251
|
+
raise ArgumentError, "Invalid model specified"
|
252
|
+
end
|
253
|
+
self
|
254
|
+
end
|
255
|
+
|
256
|
+
# Overrides the each method to pass the values through a filter. The filter
|
257
|
+
# receives as argument a hash containing the column values for the current
|
258
|
+
# record. The filter should return a value which is then passed to the
|
259
|
+
# iterating block. In order to elucidate, here's a contrived example:
|
260
|
+
#
|
261
|
+
# dataset.set_row_proc {|h| h.merge(:xxx => 'yyy')}
|
262
|
+
# dataset.first[:xxx] #=> "yyy" # always!
|
263
|
+
#
|
264
|
+
def set_row_proc(&filter)
|
265
|
+
@row_proc = filter
|
266
|
+
update_each_method
|
267
|
+
end
|
268
|
+
|
269
|
+
# Removes the row making proc.
|
270
|
+
def remove_row_proc
|
271
|
+
@row_proc = nil
|
272
|
+
update_each_method
|
273
|
+
end
|
274
|
+
|
275
|
+
STOCK_TRANSFORMS = {
|
276
|
+
:marshal => [proc {|v| Marshal.load(v)}, proc {|v| Marshal.dump(v)}],
|
277
|
+
:yaml => [proc {|v| YAML.load v if v}, proc {|v| v.to_yaml}]
|
278
|
+
}
|
279
|
+
|
280
|
+
# Sets a value transform which is used to convert values loaded and saved
|
281
|
+
# to/from the database. The transform should be supplied as a hash. Each
|
282
|
+
# value in the hash should be an array containing two proc objects - one
|
283
|
+
# for transforming loaded values, and one for transforming saved values.
|
284
|
+
# The following example demonstrates how to store Ruby objects in a dataset
|
285
|
+
# using Marshal serialization:
|
286
|
+
#
|
287
|
+
# dataset.transform(:obj => [
|
288
|
+
# proc {|v| Marshal.load(v)},
|
289
|
+
# proc {|v| Marshal.dump(v)}
|
290
|
+
# ])
|
291
|
+
#
|
292
|
+
# dataset.insert_sql(:obj => 1234) #=>
|
293
|
+
# "INSERT INTO items (obj) VALUES ('\004\bi\002\322\004')"
|
294
|
+
#
|
295
|
+
# Another form of using transform is by specifying stock transforms:
|
296
|
+
#
|
297
|
+
# dataset.transform(:obj => :marshal)
|
298
|
+
#
|
299
|
+
# The currently supported stock transforms are :marshal and :yaml.
|
300
|
+
def transform(t)
|
301
|
+
@transform = t
|
302
|
+
t.each do |k, v|
|
303
|
+
case v
|
304
|
+
when Array
|
305
|
+
if (v.size != 2) || !v.first.is_a?(Proc) && !v.last.is_a?(Proc)
|
306
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
307
|
+
end
|
308
|
+
else
|
309
|
+
unless v = STOCK_TRANSFORMS[v]
|
310
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
311
|
+
else
|
312
|
+
t[k] = v
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
update_each_method
|
317
|
+
self
|
318
|
+
end
|
319
|
+
|
320
|
+
# Applies the value transform for data loaded from the database.
|
321
|
+
def transform_load(r)
|
322
|
+
@transform.each do |k, tt|
|
323
|
+
if r.has_key?(k)
|
324
|
+
r[k] = tt[0][r[k]]
|
325
|
+
end
|
326
|
+
end
|
327
|
+
r
|
328
|
+
end
|
329
|
+
|
330
|
+
# Applies the value transform for data saved to the database.
|
331
|
+
def transform_save(r)
|
332
|
+
@transform.each do |k, tt|
|
333
|
+
if r.has_key?(k)
|
334
|
+
r[k] = tt[1][r[k]]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
r
|
338
|
+
end
|
339
|
+
|
340
|
+
# Updates the each method according to whether @row_proc and @transform are
|
341
|
+
# set or not.
|
342
|
+
def update_each_method
|
343
|
+
# warning: ugly code generation ahead
|
344
|
+
if @row_proc && @transform
|
345
|
+
class << self
|
346
|
+
def each(opts = nil, &block)
|
347
|
+
if opts && opts[:naked]
|
348
|
+
fetch_rows(select_sql(opts)) {|r| block[transform_load(r)]}
|
349
|
+
else
|
350
|
+
fetch_rows(select_sql(opts)) {|r| block[@row_proc[transform_load(r)]]}
|
351
|
+
end
|
352
|
+
self
|
353
|
+
end
|
354
|
+
end
|
355
|
+
elsif @row_proc
|
356
|
+
class << self
|
357
|
+
def each(opts = nil, &block)
|
358
|
+
if opts && opts[:naked]
|
359
|
+
fetch_rows(select_sql(opts), &block)
|
360
|
+
else
|
361
|
+
fetch_rows(select_sql(opts)) {|r| block[@row_proc[r]]}
|
362
|
+
end
|
363
|
+
self
|
364
|
+
end
|
365
|
+
end
|
366
|
+
elsif @transform
|
367
|
+
class << self
|
368
|
+
def each(opts = nil, &block)
|
369
|
+
fetch_rows(select_sql(opts)) {|r| block[transform_load(r)]}
|
370
|
+
self
|
371
|
+
end
|
372
|
+
end
|
373
|
+
else
|
374
|
+
class << self
|
375
|
+
def each(opts = nil, &block)
|
376
|
+
fetch_rows(select_sql(opts), &block)
|
377
|
+
self
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Extends the dataset with a destroy method, that calls destroy for each
|
384
|
+
# record in the dataset.
|
385
|
+
def extend_with_destroy
|
386
|
+
unless respond_to?(:destroy)
|
387
|
+
meta_def(:destroy) do
|
388
|
+
unless @opts[:models]
|
389
|
+
raise Error, "No model associated with this dataset"
|
390
|
+
end
|
391
|
+
count = 0
|
392
|
+
@db.transaction {each {|r| count += 1; r.destroy}}
|
393
|
+
count
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
@@dataset_classes = []
|
399
|
+
|
400
|
+
def self.dataset_classes #:nodoc:
|
401
|
+
@@dataset_classes
|
402
|
+
end
|
403
|
+
|
404
|
+
def self.inherited(c) #:nodoc:
|
405
|
+
@@dataset_classes << c
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
@@ -0,0 +1,321 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
|
3
|
+
module Sequel
|
4
|
+
class Dataset
|
5
|
+
module Convenience
|
6
|
+
# Iterates through each record, converting it into a hash.
|
7
|
+
def each_hash(&block)
|
8
|
+
each {|a| block[a.to_hash]}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns true if the record count is 0
|
12
|
+
def empty?
|
13
|
+
count == 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the first record in the dataset.
|
17
|
+
def single_record(opts = nil)
|
18
|
+
each(opts) {|r| return r}
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
NAKED_HASH = {:naked => true}.freeze
|
23
|
+
|
24
|
+
# Returns the first value of the first reecord in the dataset.
|
25
|
+
# Returns nill if dataset is empty.
|
26
|
+
def single_value(opts = nil)
|
27
|
+
opts = opts ? NAKED_HASH.merge(opts) : NAKED_HASH
|
28
|
+
# reset the columns cache so it won't fuck subsequent calls to columns
|
29
|
+
each(opts) {|r| @columns = nil; return r.values.first}
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the first record in the dataset. If the num argument is specified,
|
34
|
+
# an array is returned with the first <i>num</i> records.
|
35
|
+
def first(*args, &block)
|
36
|
+
if block
|
37
|
+
return filter(&block).single_record(:limit => 1)
|
38
|
+
end
|
39
|
+
args = args.empty? ? 1 : (args.size == 1) ? args.first : args
|
40
|
+
case args
|
41
|
+
when 1
|
42
|
+
single_record(:limit => 1)
|
43
|
+
when Fixnum
|
44
|
+
limit(args).all
|
45
|
+
else
|
46
|
+
filter(args, &block).single_record(:limit => 1)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the first record matching the condition.
|
51
|
+
def [](*conditions)
|
52
|
+
first(*conditions)
|
53
|
+
end
|
54
|
+
|
55
|
+
def []=(conditions, values)
|
56
|
+
filter(conditions).update(values)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the last records in the dataset by inverting the order. If no
|
60
|
+
# order is given, an exception is raised. If num is not given, the last
|
61
|
+
# record is returned. Otherwise an array is returned with the last
|
62
|
+
# <i>num</i> records.
|
63
|
+
def last(*args)
|
64
|
+
raise Error, 'No order specified' unless
|
65
|
+
@opts[:order] || (opts && opts[:order])
|
66
|
+
|
67
|
+
args = args.empty? ? 1 : (args.size == 1) ? args.first : args
|
68
|
+
|
69
|
+
case args
|
70
|
+
when Fixnum
|
71
|
+
l = {:limit => args}
|
72
|
+
opts = {:order => invert_order(@opts[:order])}. \
|
73
|
+
merge(opts ? opts.merge(l) : l)
|
74
|
+
if args == 1
|
75
|
+
single_record(opts)
|
76
|
+
else
|
77
|
+
clone_merge(opts).all
|
78
|
+
end
|
79
|
+
else
|
80
|
+
filter(args).last(1)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Maps column values for each record in the dataset (if a column name is
|
85
|
+
# given), or performs the stock mapping functionality of Enumerable.
|
86
|
+
def map(column_name = nil, &block)
|
87
|
+
if column_name
|
88
|
+
super() {|r| r[column_name]}
|
89
|
+
else
|
90
|
+
super(&block)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a hash with one column used as key and another used as value.
|
95
|
+
def to_hash(key_column, value_column)
|
96
|
+
inject({}) do |m, r|
|
97
|
+
m[r[key_column]] = r[value_column]
|
98
|
+
m
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns a paginated dataset. The resulting dataset also provides the
|
103
|
+
# total number of pages (Dataset#page_count) and the current page number
|
104
|
+
# (Dataset#current_page), as well as Dataset#prev_page and Dataset#next_page
|
105
|
+
# for implementing pagination controls.
|
106
|
+
def paginate(page_no, page_size)
|
107
|
+
record_count = count
|
108
|
+
total_pages = (record_count / page_size.to_f).ceil
|
109
|
+
paginated = limit(page_size, (page_no - 1) * page_size)
|
110
|
+
paginated.set_pagination_info(page_no, page_size, record_count)
|
111
|
+
paginated
|
112
|
+
end
|
113
|
+
|
114
|
+
# Sets the pagination info
|
115
|
+
def set_pagination_info(page_no, page_size, record_count)
|
116
|
+
@current_page = page_no
|
117
|
+
@page_size = page_size
|
118
|
+
@pagination_record_count = record_count
|
119
|
+
@page_count = (record_count / page_size.to_f).ceil
|
120
|
+
end
|
121
|
+
|
122
|
+
attr_accessor :page_size, :page_count, :current_page, :pagination_record_count
|
123
|
+
|
124
|
+
# Returns the previous page number or nil if the current page is the first
|
125
|
+
def prev_page
|
126
|
+
current_page > 1 ? (current_page - 1) : nil
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the next page number or nil if the current page is the last page
|
130
|
+
def next_page
|
131
|
+
current_page < page_count ? (current_page + 1) : nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns the page range
|
135
|
+
def page_range
|
136
|
+
1..page_count
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns the record range for the current page
|
140
|
+
def current_page_record_range
|
141
|
+
return (0..0) if @current_page > @page_count
|
142
|
+
|
143
|
+
a = 1 + (@current_page - 1) * @page_size
|
144
|
+
b = a + @page_size - 1
|
145
|
+
b = @pagination_record_count if b > @pagination_record_count
|
146
|
+
a..b
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns the number of records in the current page
|
150
|
+
def current_page_record_count
|
151
|
+
return 0 if @current_page > @page_count
|
152
|
+
|
153
|
+
a = 1 + (@current_page - 1) * @page_size
|
154
|
+
b = a + @page_size - 1
|
155
|
+
b = @pagination_record_count if b > @pagination_record_count
|
156
|
+
b - a + 1
|
157
|
+
end
|
158
|
+
|
159
|
+
# Returns the minimum value for the given column.
|
160
|
+
def min(column)
|
161
|
+
single_value(:select => [column.MIN.AS(:v)])
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns the maximum value for the given column.
|
165
|
+
def max(column)
|
166
|
+
single_value(:select => [column.MAX.AS(:v)])
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns the sum for the given column.
|
170
|
+
def sum(column)
|
171
|
+
single_value(:select => [column.SUM.AS(:v)])
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns the average value for the given column.
|
175
|
+
def avg(column)
|
176
|
+
single_value(:select => [column.AVG.AS(:v)])
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns a dataset grouped by the given column with count by group.
|
180
|
+
def group_and_count(column)
|
181
|
+
group(column).select(column, :count[column].AS(:count)).order(:count)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns a Range object made from the minimum and maximum values for the
|
185
|
+
# given column.
|
186
|
+
def range(column)
|
187
|
+
r = select(column.MIN.AS(:v1), column.MAX.AS(:v2)).first
|
188
|
+
r && (r[:v1]..r[:v2])
|
189
|
+
end
|
190
|
+
|
191
|
+
# Returns the interval between minimum and maximum values for the given
|
192
|
+
# column.
|
193
|
+
def interval(column)
|
194
|
+
r = select("(max(#{literal(column)}) - min(#{literal(column)})) AS v".lit).first
|
195
|
+
r && r[:v]
|
196
|
+
end
|
197
|
+
|
198
|
+
# Pretty prints the records in the dataset as plain-text table.
|
199
|
+
def print(*cols)
|
200
|
+
Sequel::PrettyTable.print(naked.all, cols.empty? ? columns : cols)
|
201
|
+
end
|
202
|
+
|
203
|
+
COMMA_SEPARATOR = ', '.freeze
|
204
|
+
|
205
|
+
# Returns a string in CSV format containing the dataset records. By
|
206
|
+
# default the CSV representation includes the column titles in the
|
207
|
+
# first line. You can turn that off by passing false as the
|
208
|
+
# include_column_titles argument.
|
209
|
+
def to_csv(include_column_titles = true)
|
210
|
+
records = naked.to_a
|
211
|
+
csv = ''
|
212
|
+
if include_column_titles
|
213
|
+
csv << "#{@columns.join(COMMA_SEPARATOR)}\r\n"
|
214
|
+
end
|
215
|
+
records.each {|r| csv << "#{r.join(COMMA_SEPARATOR)}\r\n"}
|
216
|
+
csv
|
217
|
+
end
|
218
|
+
|
219
|
+
# Inserts multiple records into the associated table. This method can be
|
220
|
+
# to efficiently insert a large amounts of records into a table. Inserts
|
221
|
+
# are automatically wrapped in a transaction. If the :commit_every
|
222
|
+
# option is specified, the method will generate a separate transaction
|
223
|
+
# for each batch of records, e.g.:
|
224
|
+
#
|
225
|
+
# dataset.multi_insert(list, :commit_every => 1000)
|
226
|
+
def multi_insert(list, opts = {})
|
227
|
+
if every = opts[:commit_every]
|
228
|
+
list.each_slice(every) do |s|
|
229
|
+
@db.transaction do
|
230
|
+
s.each {|r| @db.execute(insert_sql(r))}
|
231
|
+
# @db.execute(s.map {|r| insert_sql(r)}.join)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
else
|
235
|
+
@db.transaction do
|
236
|
+
# @db.execute(list.map {|r| insert_sql(r)}.join)
|
237
|
+
list.each {|r| @db.execute(insert_sql(r))}
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
module QueryBlockCopy #:nodoc:
|
243
|
+
def each(*args); raise Error, "#each cannot be invoked inside a query block."; end
|
244
|
+
def insert(*args); raise Error, "#insert cannot be invoked inside a query block."; end
|
245
|
+
def update(*args); raise Error, "#update cannot be invoked inside a query block."; end
|
246
|
+
def delete(*args); raise Error, "#delete cannot be invoked inside a query block."; end
|
247
|
+
|
248
|
+
def clone_merge(opts)
|
249
|
+
@opts.merge!(opts)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Translates a query block into a dataset. Query blocks can be useful
|
254
|
+
# when expressing complex SELECT statements, e.g.:
|
255
|
+
#
|
256
|
+
# dataset = DB[:items].query do
|
257
|
+
# select :x, :y, :z
|
258
|
+
# where {:x > 1 && :y > 2}
|
259
|
+
# order_by :z.DESC
|
260
|
+
# end
|
261
|
+
#
|
262
|
+
def query(&block)
|
263
|
+
copy = clone_merge({})
|
264
|
+
copy.extend(QueryBlockCopy)
|
265
|
+
copy.instance_eval(&block)
|
266
|
+
clone_merge(copy.opts)
|
267
|
+
end
|
268
|
+
|
269
|
+
MUTATION_RE = /^(.+)!$/.freeze
|
270
|
+
|
271
|
+
# Provides support for mutation methods (filter!, order!, etc.) and magic
|
272
|
+
# methods.
|
273
|
+
def method_missing(m, *args, &block)
|
274
|
+
if m.to_s =~ MUTATION_RE
|
275
|
+
m = $1.to_sym
|
276
|
+
super unless respond_to?(m)
|
277
|
+
copy = send(m, *args, &block)
|
278
|
+
super if copy.class != self.class
|
279
|
+
@opts.merge!(copy.opts)
|
280
|
+
self
|
281
|
+
elsif magic_method_missing(m)
|
282
|
+
send(m, *args)
|
283
|
+
else
|
284
|
+
super
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
MAGIC_METHODS = {
|
289
|
+
/^order_by_(.+)$/ => proc {|c| proc {order(c)}},
|
290
|
+
/^first_by_(.+)$/ => proc {|c| proc {order(c).first}},
|
291
|
+
/^last_by_(.+)$/ => proc {|c| proc {order(c).last}},
|
292
|
+
/^filter_by_(.+)$/ => proc {|c| proc {|v| filter(c => v)}},
|
293
|
+
/^all_by_(.+)$/ => proc {|c| proc {|v| filter(c => v).all}},
|
294
|
+
/^find_by_(.+)$/ => proc {|c| proc {|v| filter(c => v).first}},
|
295
|
+
/^group_by_(.+)$/ => proc {|c| proc {group(c)}},
|
296
|
+
/^count_by_(.+)$/ => proc {|c| proc {group_and_count(c)}}
|
297
|
+
}
|
298
|
+
|
299
|
+
# Checks if the given method name represents a magic method and
|
300
|
+
# defines it. Otherwise, nil is returned.
|
301
|
+
def magic_method_missing(m)
|
302
|
+
method_name = m.to_s
|
303
|
+
MAGIC_METHODS.each_pair do |r, p|
|
304
|
+
if method_name =~ r
|
305
|
+
impl = p[$1.to_sym]
|
306
|
+
return Dataset.class_def(m, &impl)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
nil
|
310
|
+
end
|
311
|
+
|
312
|
+
def create_view(name)
|
313
|
+
@db.create_view(name, self)
|
314
|
+
end
|
315
|
+
|
316
|
+
def create_or_replace_view(name)
|
317
|
+
@db.create_or_replace_view(name, self)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|