Class: Sequel::Dataset

Inherits:
Object show all
Includes:
Enumerable, Convenience, SQL, Sequelizer
Defined in:
lib/sequel/dataset.rb,
lib/sequel/dataset/sql.rb,
lib/sequel/dataset/sequelizer.rb,
lib/sequel/dataset/convenience.rb

Overview

A Dataset represents a view of a the data in a database, constrained by specific parameters such as filtering conditions, order, etc. Datasets can be used to create, retrieve, update and delete records.

Query results are always retrieved on demand, so a dataset can be kept around and reused indefinitely:

my_posts = DB[:posts].filter(:author => 'david') # no records are retrieved
p my_posts.all # records are now retrieved
...
p my_posts.all # records are retrieved again

In order to provide this functionality, dataset methods such as where, select, order, etc. return modified copies of the dataset, so you can use different datasets to access data:

posts = DB[:posts]
davids_posts = posts.filter(:author => 'david')
old_posts = posts.filter('stamp < ?', 1.week.ago)

Datasets are Enumerable objects, so they can be manipulated using any of the Enumerable methods, such as map, inject, etc.

The Dataset Adapter Interface

Each adapter should define its own dataset class as a descendant of Sequel::Dataset. The following methods should be overriden by the adapter Dataset class (each method with the stock implementation):

# Iterate over the results of the SQL query and call the supplied
# block with each record (as a hash).
def fetch_rows(sql, &block)
  @db.synchronize do
    r = @db.execute(sql)
    r.each(&block)
  end
end

# Insert records.
def insert(*values)
  @db.synchronize do
    @db.execute(insert_sql(*values)).last_insert_id
  end
end

# Update records.
def update(*args, &block)
  @db.synchronize do
    @db.execute(update_sql(*args, &block)).affected_rows
  end
end

# Delete records.
def delete(opts = nil)
  @db.synchronize do
    @db.execute(delete_sql(opts)).affected_rows
  end
end

Defined Under Namespace

Modules: Convenience, SQL, Sequelizer

Constant Summary collapse

NOTIMPL_MSG =
"This method must be overriden in Sequel adapters".freeze
STOCK_TRANSFORMS =
{
  :marshal => [proc {|v| Marshal.load(v)}, proc {|v| Marshal.dump(v)}],
  :yaml => [proc {|v| YAML.load v if v}, proc {|v| v.to_yaml}]
}
@@dataset_classes =
[]

Constants included from Convenience

Convenience::COMMA_SEPARATOR, Convenience::MAGIC_METHODS, Convenience::MUTATION_RE, Convenience::NAKED_HASH

Constants included from SQL

SQL::ALIASED_REGEXP, SQL::AND_SEPARATOR, SQL::COMMA_SEPARATOR, SQL::DATE_FORMAT, SQL::FALSE, SQL::JOIN_TYPES, SQL::NULL, SQL::QUALIFIED_REGEXP, SQL::QUESTION_MARK, SQL::STOCK_COUNT_OPTS, SQL::TIMESTAMP_FORMAT, SQL::TRUE, SQL::WILDCARD

Constants included from Sequelizer

Sequelizer::JOIN_AND, Sequelizer::JOIN_COMMA

Instance Attribute Summary collapse

Attributes included from Convenience

#current_page, #page_count, #page_size, #pagination_record_count

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Convenience

#[], #[]=, #avg, #create_or_replace_view, #create_view, #current_page_record_count, #current_page_record_range, #each_hash, #empty?, #first, #group_and_count, #interval, #last, #magic_method_missing, #map, #max, #method_missing, #min, #multi_insert, #next_page, #page_range, #paginate, #prev_page, #print, #query, #range, #set_pagination_info, #single_record, #single_value, #sum, #to_csv, #to_hash

Methods included from SQL

#and, #column_list, #count, #delete_sql, #except, #exclude, #exists, #expression_list, #filter, #from, #full_outer_join, #group, #having, #inner_join, #insert_multiple, #insert_sql, #intersect, #invert_order, #join_expr, #join_table, #left_outer_join, #limit, #literal, #or, #order, #qualified_column_name, #quote_column_ref, #reverse_order, #right_outer_join, #select, #select_sql, #source_list, #to_table_reference, #union, #uniq, #update_sql, #where

Methods included from Sequelizer

#call_expr, #compare_expr, #eval_expr, #ext_expr, #fcall_expr, #iter_expr, #match_expr, #proc_to_sql, #pt_expr, #replace_dvars, #unfold_each_expr, #value_to_parse_tree, #vcall_expr

Methods included from Enumerable

#send_each

Constructor Details

#initialize(db, opts = nil) ⇒ Dataset

Constructs a new instance of a dataset with a database instance, initial options and an optional record class. Datasets are usually constructed by invoking Database methods:

DB[:posts]

Or:

DB.dataset # the returned dataset is blank

Sequel::Dataset is an abstract class that is not useful by itself. Each database adaptor should provide a descendant class of Sequel::Dataset.



87
88
89
90
91
92
# File 'lib/sequel/dataset.rb', line 87

def initialize(db, opts = nil)
  @db = db
  @opts = opts || {}
  @row_proc = nil
  @transform = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method in the class Sequel::Dataset::Convenience

Instance Attribute Details

#dbObject (readonly)

Returns the value of attribute db.



72
73
74
# File 'lib/sequel/dataset.rb', line 72

def db
  @db
end

#optsObject

Returns the value of attribute opts.



73
74
75
# File 'lib/sequel/dataset.rb', line 73

def opts
  @opts
end

Class Method Details

.dataset_classesObject

:nodoc:



400
401
402
# File 'lib/sequel/dataset.rb', line 400

def self.dataset_classes #:nodoc:
  @@dataset_classes
end

.inherited(c) ⇒ Object

:nodoc:



404
405
406
# File 'lib/sequel/dataset.rb', line 404

def self.inherited(c) #:nodoc:
  @@dataset_classes << c
end

Instance Method Details

#<<(*args) ⇒ Object

Inserts the supplied values into the associated table.



153
154
155
# File 'lib/sequel/dataset.rb', line 153

def <<(*args)
  insert(*args)
end

#clone_merge(opts) ⇒ Object

Returns a new instance of the dataset with with the give options merged.



95
96
97
98
99
# File 'lib/sequel/dataset.rb', line 95

def clone_merge(opts)
  new_dataset = clone
  new_dataset.set_options(@opts.merge(opts))
  new_dataset
end

#columnsObject

Returns the columns in the result set in their true order. The stock implementation returns the content of @columns. If @columns is nil, a query is performed. Adapters are expected to fill @columns with the column information when a query is performed.



147
148
149
150
# File 'lib/sequel/dataset.rb', line 147

def columns
  first unless @columns
  @columns || []
end

#delete(opts = nil) ⇒ Object

Deletes the records in the dataset. Adapters should override this method.

Raises:



136
137
138
139
140
141
# File 'lib/sequel/dataset.rb', line 136

def delete(opts = nil)
  # @db.synchronize do
  #   @db.execute(delete_sql(opts)).affected_rows
  # end
  raise NotImplementedError, NOTIMPL_MSG
end

#each(opts = nil, &block) ⇒ Object

Iterates over the records in the dataset



163
164
165
166
# File 'lib/sequel/dataset.rb', line 163

def each(opts = nil, &block)
  fetch_rows(select_sql(opts), &block)
  self
end

#extend_with_destroyObject

Extends the dataset with a destroy method, that calls destroy for each record in the dataset.



385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/sequel/dataset.rb', line 385

def extend_with_destroy
  unless respond_to?(:destroy)
    meta_def(:destroy) do
      unless @opts[:models]
        raise Error, "No model associated with this dataset"
      end
      count = 0
      @db.transaction {each {|r| count += 1; r.destroy}}
      count
    end
  end
end

#fetch_rows(sql, &block) ⇒ Object

Executes a select query and fetches records, passing each record to the supplied block. Adapters should override this method.

Raises:



110
111
112
113
114
115
116
# File 'lib/sequel/dataset.rb', line 110

def fetch_rows(sql, &block)
  # @db.synchronize do
  #   r = @db.execute(sql)
  #   r.each(&block)
  # end
  raise NotImplementedError, NOTIMPL_MSG
end

#insert(*values) ⇒ Object

Inserts values into the associated table. Adapters should override this method.

Raises:



120
121
122
123
124
125
# File 'lib/sequel/dataset.rb', line 120

def insert(*values)
  # @db.synchronize do
  #   @db.execute(insert_sql(*values)).last_insert_id
  # end
  raise NotImplementedError, NOTIMPL_MSG
end

#model_classesObject

Returns the the model classes associated with the dataset as a hash.



169
170
171
# File 'lib/sequel/dataset.rb', line 169

def model_classes
  @opts[:models]
end

#nakedObject

Returns a naked dataset clone - i.e. a dataset that returns records as hashes rather than model objects.



180
181
182
183
184
# File 'lib/sequel/dataset.rb', line 180

def naked
  d = clone_merge(:naked => true, :models => nil, :polymorphic_key => nil)
  d.set_model(nil)
  d
end

#polymorphic_keyObject

Returns the column name for the polymorphic key.



174
175
176
# File 'lib/sequel/dataset.rb', line 174

def polymorphic_key
  @opts[:polymorphic_key]
end

#remove_row_procObject

Removes the row making proc.



270
271
272
273
# File 'lib/sequel/dataset.rb', line 270

def remove_row_proc
  @row_proc = nil
  update_each_method
end

#set(*args, &block) ⇒ Object

Updates the dataset with the given values.



158
159
160
# File 'lib/sequel/dataset.rb', line 158

def set(*args, &block)
  update(*args, &block)
end

#set_model(key, *args) ⇒ Object

Associates or disassociates the dataset with a model. If no argument or nil is specified, the dataset is turned into a naked dataset and returns records as hashes. If a model class specified, the dataset is modified to return records as instances of the model class, e.g:

class MyModel
  def initialize(values)
    @values = values
    ...
  end
end

dataset.set_model(MyModel)

You can also provide additional arguments to be passed to the model’s initialize method:

class MyModel
  def initialize(values, options)
    @values = values
    ...
  end
end

dataset.set_model(MyModel, :allow_delete => false)

The dataset can be made polymorphic by specifying a column name as the polymorphic key and a hash mapping column values to model classes.

dataset.set_model(:kind, {1 => Person, 2 => Business})

You can also set a default model class to fall back on by specifying a class corresponding to nil:

dataset.set_model(:kind, {nil => DefaultClass, 1 => Person, 2 => Business})

To disassociate a model from the dataset, you can call the #set_model and specify nil as the class:

dataset.set_model(nil)


227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/sequel/dataset.rb', line 227

def set_model(key, *args)
  # pattern matching
  case key
  when nil # set_model(nil) => no
    # no argument provided, so the dataset is denuded
    @opts.merge!(:naked => true, :models => nil, :polymorphic_key => nil)
    remove_row_proc
    # extend_with_stock_each
  when Class
    # isomorphic model
    @opts.merge!(:naked => nil, :models => {nil => key}, :polymorphic_key => nil)
    set_row_proc {|h| key.new(h, *args)}
    extend_with_destroy
  when Symbol
    # polymorphic model
    hash = args.shift || raise(ArgumentError, "No class hash supplied for polymorphic model")
    @opts.merge!(:naked => true, :models => hash, :polymorphic_key => key)
    set_row_proc do |h|
      c = hash[h[key]] || hash[nil] || \
        raise(Error, "No matching model class for record (#{polymorphic_key} => #{h[polymorphic_key].inspect})")
      c.new(h, *args)
    end
    extend_with_destroy
  else
    raise ArgumentError, "Invalid model specified"
  end
  self
end

#set_options(opts) ⇒ Object

:nodoc:



101
102
103
104
# File 'lib/sequel/dataset.rb', line 101

def set_options(opts) #:nodoc:
  @opts = opts
  @columns = nil
end

#set_row_proc(&filter) ⇒ Object

Overrides the each method to pass the values through a filter. The filter receives as argument a hash containing the column values for the current record. The filter should return a value which is then passed to the iterating block. In order to elucidate, here’s a contrived example:

dataset.set_row_proc {|h| h.merge(:xxx => 'yyy')}
dataset.first[:xxx] #=> "yyy" # always!


264
265
266
267
# File 'lib/sequel/dataset.rb', line 264

def set_row_proc(&filter)
  @row_proc = filter
  update_each_method
end

#transform(t) ⇒ Object

Sets a value transform which is used to convert values loaded and saved to/from the database. The transform should be supplied as a hash. Each value in the hash should be an array containing two proc objects - one for transforming loaded values, and one for transforming saved values. The following example demonstrates how to store Ruby objects in a dataset using Marshal serialization:

dataset.transform(:obj => [
  proc {|v| Marshal.load(v)},
  proc {|v| Marshal.dump(v)}
])

dataset.insert_sql(:obj => 1234) #=>
"INSERT INTO items (obj) VALUES ('\004\bi\002\322\004')"

Another form of using transform is by specifying stock transforms:

dataset.transform(:obj => :marshal)

The currently supported stock transforms are :marshal and :yaml.



300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/sequel/dataset.rb', line 300

def transform(t)
  @transform = t
  t.each do |k, v|
    case v
    when Array
      if (v.size != 2) || !v.first.is_a?(Proc) && !v.last.is_a?(Proc)
        raise Error::InvalidTransform, "Invalid transform specified"
      end
    else
      unless v = STOCK_TRANSFORMS[v]
        raise Error::InvalidTransform, "Invalid transform specified"
      else
        t[k] = v
      end
    end
  end
  update_each_method
  self
end

#transform_load(r) ⇒ Object

Applies the value transform for data loaded from the database.



321
322
323
324
325
326
327
328
# File 'lib/sequel/dataset.rb', line 321

def transform_load(r)
  @transform.each do |k, tt|
    if r.has_key?(k)
      r[k] = tt[0][r[k]]
    end
  end
  r
end

#transform_save(r) ⇒ Object

Applies the value transform for data saved to the database.



331
332
333
334
335
336
337
338
# File 'lib/sequel/dataset.rb', line 331

def transform_save(r)
  @transform.each do |k, tt|
    if r.has_key?(k)
      r[k] = tt[1][r[k]]
    end
  end
  r
end

#update(values, opts = nil) ⇒ Object

Updates values for the dataset. Adapters should override this method.

Raises:



128
129
130
131
132
133
# File 'lib/sequel/dataset.rb', line 128

def update(values, opts = nil)
  # @db.synchronize do
  #   @db.execute(update_sql(values, opts)).affected_rows
  # end
  raise NotImplementedError, NOTIMPL_MSG
end

#update_each_methodObject

Updates the each method according to whether @row_proc and @transform are set or not.



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/sequel/dataset.rb', line 342

def update_each_method
  # warning: ugly code generation ahead
  if @row_proc && @transform
    class << self
      def each(opts = nil, &block)
        if opts && opts[:naked]
          fetch_rows(select_sql(opts)) {|r| block[transform_load(r)]}
        else
          fetch_rows(select_sql(opts)) {|r| block[@row_proc[transform_load(r)]]}
        end
        self
      end
    end
  elsif @row_proc
    class << self
      def each(opts = nil, &block)
        if opts && opts[:naked]
          fetch_rows(select_sql(opts), &block)
        else
          fetch_rows(select_sql(opts)) {|r| block[@row_proc[r]]}
        end
        self
      end
    end
  elsif @transform
    class << self
      def each(opts = nil, &block)
        fetch_rows(select_sql(opts)) {|r| block[transform_load(r)]}
        self
      end
    end
  else
    class << self
      def each(opts = nil, &block)
        fetch_rows(select_sql(opts), &block)
        self
      end
    end
  end
end