sequel 2.2.0 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1551 -4
- data/README +306 -19
- data/Rakefile +84 -56
- data/bin/sequel +106 -0
- data/doc/cheat_sheet.rdoc +225 -0
- data/doc/dataset_filtering.rdoc +182 -0
- data/lib/sequel_core.rb +136 -0
- data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
- data/lib/sequel_core/adapters/ado.rb +80 -0
- data/lib/sequel_core/adapters/db2.rb +148 -0
- data/lib/sequel_core/adapters/dbi.rb +117 -0
- data/lib/sequel_core/adapters/informix.rb +78 -0
- data/lib/sequel_core/adapters/jdbc.rb +186 -0
- data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
- data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
- data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
- data/lib/sequel_core/adapters/mysql.rb +231 -0
- data/lib/sequel_core/adapters/odbc.rb +155 -0
- data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
- data/lib/sequel_core/adapters/openbase.rb +64 -0
- data/lib/sequel_core/adapters/oracle.rb +170 -0
- data/lib/sequel_core/adapters/postgres.rb +199 -0
- data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
- data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
- data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
- data/lib/sequel_core/adapters/sqlite.rb +138 -0
- data/lib/sequel_core/connection_pool.rb +194 -0
- data/lib/sequel_core/core_ext.rb +203 -0
- data/lib/sequel_core/core_sql.rb +184 -0
- data/lib/sequel_core/database.rb +471 -0
- data/lib/sequel_core/database/schema.rb +156 -0
- data/lib/sequel_core/dataset.rb +457 -0
- data/lib/sequel_core/dataset/callback.rb +13 -0
- data/lib/sequel_core/dataset/convenience.rb +245 -0
- data/lib/sequel_core/dataset/pagination.rb +96 -0
- data/lib/sequel_core/dataset/query.rb +41 -0
- data/lib/sequel_core/dataset/schema.rb +15 -0
- data/lib/sequel_core/dataset/sql.rb +889 -0
- data/lib/sequel_core/deprecated.rb +26 -0
- data/lib/sequel_core/exceptions.rb +42 -0
- data/lib/sequel_core/migration.rb +187 -0
- data/lib/sequel_core/object_graph.rb +216 -0
- data/lib/sequel_core/pretty_table.rb +71 -0
- data/lib/sequel_core/schema.rb +2 -0
- data/lib/sequel_core/schema/generator.rb +239 -0
- data/lib/sequel_core/schema/sql.rb +325 -0
- data/lib/sequel_core/sql.rb +812 -0
- data/lib/sequel_model.rb +5 -1
- data/lib/sequel_model/association_reflection.rb +3 -8
- data/lib/sequel_model/base.rb +15 -10
- data/lib/sequel_model/inflector.rb +3 -5
- data/lib/sequel_model/plugins.rb +1 -1
- data/lib/sequel_model/record.rb +11 -3
- data/lib/sequel_model/schema.rb +4 -4
- data/lib/sequel_model/validations.rb +6 -1
- data/spec/adapters/ado_spec.rb +17 -0
- data/spec/adapters/informix_spec.rb +96 -0
- data/spec/adapters/mysql_spec.rb +764 -0
- data/spec/adapters/oracle_spec.rb +222 -0
- data/spec/adapters/postgres_spec.rb +441 -0
- data/spec/adapters/spec_helper.rb +7 -0
- data/spec/adapters/sqlite_spec.rb +400 -0
- data/spec/integration/dataset_test.rb +51 -0
- data/spec/integration/eager_loader_test.rb +702 -0
- data/spec/integration/schema_test.rb +102 -0
- data/spec/integration/spec_helper.rb +44 -0
- data/spec/integration/type_test.rb +43 -0
- data/spec/rcov.opts +2 -0
- data/spec/sequel_core/connection_pool_spec.rb +363 -0
- data/spec/sequel_core/core_ext_spec.rb +156 -0
- data/spec/sequel_core/core_sql_spec.rb +427 -0
- data/spec/sequel_core/database_spec.rb +964 -0
- data/spec/sequel_core/dataset_spec.rb +2977 -0
- data/spec/sequel_core/expression_filters_spec.rb +346 -0
- data/spec/sequel_core/migration_spec.rb +261 -0
- data/spec/sequel_core/object_graph_spec.rb +234 -0
- data/spec/sequel_core/pretty_table_spec.rb +58 -0
- data/spec/sequel_core/schema_generator_spec.rb +122 -0
- data/spec/sequel_core/schema_spec.rb +497 -0
- data/spec/sequel_core/spec_helper.rb +51 -0
- data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
- data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
- data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
- data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
- data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
- data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
- data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
- data/spec/sequel_model/inflector_spec.rb +119 -0
- data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
- data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
- data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
- data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
- data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
- data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
- data/spec/spec_config.rb +9 -0
- data/spec/spec_config.rb.example +10 -0
- metadata +110 -37
- data/spec/inflector_spec.rb +0 -34
@@ -0,0 +1,156 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Database
|
3
|
+
# Adds a column to the specified table. This method expects a column name,
|
4
|
+
# a datatype and optionally a hash with additional constraints and options:
|
5
|
+
#
|
6
|
+
# DB.add_column :items, :name, :text, :unique => true, :null => false
|
7
|
+
# DB.add_column :items, :category, :text, :default => 'ruby'
|
8
|
+
#
|
9
|
+
# See alter_table.
|
10
|
+
def add_column(table, *args)
|
11
|
+
alter_table(table) {add_column(*args)}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds an index to a table for the given columns:
|
15
|
+
#
|
16
|
+
# DB.add_index :posts, :title
|
17
|
+
# DB.add_index :posts, [:author, :title], :unique => true
|
18
|
+
#
|
19
|
+
# See alter_table.
|
20
|
+
def add_index(table, *args)
|
21
|
+
alter_table(table) {add_index(*args)}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Alters the given table with the specified block. Here are the currently
|
25
|
+
# available operations:
|
26
|
+
#
|
27
|
+
# DB.alter_table :items do
|
28
|
+
# add_column :category, :text, :default => 'ruby'
|
29
|
+
# drop_column :category
|
30
|
+
# rename_column :cntr, :counter
|
31
|
+
# set_column_type :value, :float
|
32
|
+
# set_column_default :value, :float
|
33
|
+
# add_index [:group, :category]
|
34
|
+
# drop_index [:group, :category]
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# Note that #add_column accepts all the options available for column
|
38
|
+
# definitions using create_table, and #add_index accepts all the options
|
39
|
+
# available for index definition.
|
40
|
+
#
|
41
|
+
# See Schema::AlterTableGenerator.
|
42
|
+
def alter_table(name, generator=nil, &block)
|
43
|
+
generator ||= Schema::AlterTableGenerator.new(self, &block)
|
44
|
+
alter_table_sql_list(name, generator.operations).flatten.each {|sql| execute_ddl(sql)}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Creates a table with the columns given in the provided block:
|
48
|
+
#
|
49
|
+
# DB.create_table :posts do
|
50
|
+
# primary_key :id, :serial
|
51
|
+
# column :title, :text
|
52
|
+
# column :content, :text
|
53
|
+
# index :title
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
# See Schema::Generator.
|
57
|
+
def create_table(name, generator=nil, &block)
|
58
|
+
generator ||= Schema::Generator.new(self, &block)
|
59
|
+
create_table_sql_list(name, *generator.create_info).flatten.each {|sql| execute_ddl(sql)}
|
60
|
+
end
|
61
|
+
|
62
|
+
# Forcibly creates a table. If the table already exists it is dropped.
|
63
|
+
def create_table!(name, generator=nil, &block)
|
64
|
+
drop_table(name) rescue nil
|
65
|
+
create_table(name, generator, &block)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates a view, replacing it if it already exists:
|
69
|
+
#
|
70
|
+
# DB.create_or_replace_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
|
71
|
+
# DB.create_or_replace_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
|
72
|
+
def create_or_replace_view(name, source)
|
73
|
+
source = source.sql if source.is_a?(Dataset)
|
74
|
+
execute_ddl("CREATE OR REPLACE VIEW #{name} AS #{source}")
|
75
|
+
end
|
76
|
+
|
77
|
+
# Creates a view based on a dataset or an SQL string:
|
78
|
+
#
|
79
|
+
# DB.create_view(:cheap_items, "SELECT * FROM items WHERE price < 100")
|
80
|
+
# DB.create_view(:ruby_items, DB[:items].filter(:category => 'ruby'))
|
81
|
+
def create_view(name, source)
|
82
|
+
source = source.sql if source.is_a?(Dataset)
|
83
|
+
execute_ddl("CREATE VIEW #{name} AS #{source}")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Removes a column from the specified table:
|
87
|
+
#
|
88
|
+
# DB.drop_column :items, :category
|
89
|
+
#
|
90
|
+
# See alter_table.
|
91
|
+
def drop_column(table, *args)
|
92
|
+
alter_table(table) {drop_column(*args)}
|
93
|
+
end
|
94
|
+
|
95
|
+
# Removes an index for the given table and column/s:
|
96
|
+
#
|
97
|
+
# DB.drop_index :posts, :title
|
98
|
+
# DB.drop_index :posts, [:author, :title]
|
99
|
+
#
|
100
|
+
# See alter_table.
|
101
|
+
def drop_index(table, columns)
|
102
|
+
alter_table(table) {drop_index(columns)}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Drops one or more tables corresponding to the given table names:
|
106
|
+
#
|
107
|
+
# DB.drop_table(:posts, :comments)
|
108
|
+
def drop_table(*names)
|
109
|
+
names.each {|n| execute_ddl(drop_table_sql(n))}
|
110
|
+
end
|
111
|
+
|
112
|
+
# Drops a view:
|
113
|
+
#
|
114
|
+
# DB.drop_view(:cheap_items)
|
115
|
+
def drop_view(name)
|
116
|
+
execute_ddl("DROP VIEW #{name}")
|
117
|
+
end
|
118
|
+
|
119
|
+
# Renames a table:
|
120
|
+
#
|
121
|
+
# DB.tables #=> [:items]
|
122
|
+
# DB.rename_table :items, :old_items
|
123
|
+
# DB.tables #=> [:old_items]
|
124
|
+
def rename_table(*args)
|
125
|
+
execute_ddl(rename_table_sql(*args))
|
126
|
+
end
|
127
|
+
|
128
|
+
# Renames a column in the specified table. This method expects the current
|
129
|
+
# column name and the new column name:
|
130
|
+
#
|
131
|
+
# DB.rename_column :items, :cntr, :counter
|
132
|
+
#
|
133
|
+
# See alter_table.
|
134
|
+
def rename_column(table, *args)
|
135
|
+
alter_table(table) {rename_column(*args)}
|
136
|
+
end
|
137
|
+
|
138
|
+
# Sets the default value for the given column in the given table:
|
139
|
+
#
|
140
|
+
# DB.set_column_default :items, :category, 'perl!'
|
141
|
+
#
|
142
|
+
# See alter_table.
|
143
|
+
def set_column_default(table, *args)
|
144
|
+
alter_table(table) {set_column_default(*args)}
|
145
|
+
end
|
146
|
+
|
147
|
+
# Set the data type for the given column in the given table:
|
148
|
+
#
|
149
|
+
# DB.set_column_type :items, :price, :float
|
150
|
+
#
|
151
|
+
# See alter_table.
|
152
|
+
def set_column_type(table, *args)
|
153
|
+
alter_table(table) {set_column_type(*args)}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,457 @@
|
|
1
|
+
%w'callback convenience pagination query schema sql'.each do |f|
|
2
|
+
require "sequel_core/dataset/#{f}"
|
3
|
+
end
|
4
|
+
|
5
|
+
module Sequel
|
6
|
+
# A Dataset represents a view of a the data in a database, constrained by
|
7
|
+
# specific parameters such as filtering conditions, order, etc. Datasets
|
8
|
+
# can be used to create, retrieve, update and delete records.
|
9
|
+
#
|
10
|
+
# Query results are always retrieved on demand, so a dataset can be kept
|
11
|
+
# around and reused indefinitely:
|
12
|
+
#
|
13
|
+
# my_posts = DB[:posts].filter(:author => 'david') # no records are retrieved
|
14
|
+
# p my_posts.all # records are now retrieved
|
15
|
+
# ...
|
16
|
+
# p my_posts.all # records are retrieved again
|
17
|
+
#
|
18
|
+
# In order to provide this functionality, dataset methods such as where,
|
19
|
+
# select, order, etc. return modified copies of the dataset, so you can
|
20
|
+
# use different datasets to access data:
|
21
|
+
#
|
22
|
+
# posts = DB[:posts]
|
23
|
+
# davids_posts = posts.filter(:author => 'david')
|
24
|
+
# old_posts = posts.filter('stamp < ?', Date.today - 7)
|
25
|
+
#
|
26
|
+
# Datasets are Enumerable objects, so they can be manipulated using any
|
27
|
+
# of the Enumerable methods, such as map, inject, etc.
|
28
|
+
#
|
29
|
+
# === The Dataset Adapter Interface
|
30
|
+
#
|
31
|
+
# Each adapter should define its own dataset class as a descendant of
|
32
|
+
# Sequel::Dataset. The following methods should be overridden by the adapter
|
33
|
+
# Dataset class (each method with the stock implementation):
|
34
|
+
#
|
35
|
+
# # Iterate over the results of the SQL query and call the supplied
|
36
|
+
# # block with each record (as a hash).
|
37
|
+
# def fetch_rows(sql, &block)
|
38
|
+
# @db.synchronize do
|
39
|
+
# r = @db.execute(sql)
|
40
|
+
# r.each(&block)
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # Insert records.
|
45
|
+
# def insert(*values)
|
46
|
+
# @db.synchronize do
|
47
|
+
# @db.execute(insert_sql(*values)).last_insert_id
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # Update records.
|
52
|
+
# def update(*args)
|
53
|
+
# @db.synchronize do
|
54
|
+
# @db.execute(update_sql(*args)).affected_rows
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# # Delete records.
|
59
|
+
# def delete(opts = nil)
|
60
|
+
# @db.synchronize do
|
61
|
+
# @db.execute(delete_sql(opts)).affected_rows
|
62
|
+
# end
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# === Methods added via metaprogramming
|
66
|
+
#
|
67
|
+
# Some methods are added via metaprogramming:
|
68
|
+
#
|
69
|
+
# * ! methods - These methods are the same as their non-! counterparts,
|
70
|
+
# but they modify the receiver instead of returning a modified copy
|
71
|
+
# of the dataset.
|
72
|
+
# * inner_join, full_outer_join, right_outer_join, left_outer_join -
|
73
|
+
# This methods are shortcuts to join_table with the join type
|
74
|
+
# already specified.
|
75
|
+
class Dataset
|
76
|
+
include Enumerable
|
77
|
+
|
78
|
+
# The dataset options that require the removal of cached columns
|
79
|
+
# if changed.
|
80
|
+
COLUMN_CHANGE_OPTS = [:select, :sql, :from, :join].freeze
|
81
|
+
|
82
|
+
# Array of all subclasses of Dataset
|
83
|
+
DATASET_CLASSES = []
|
84
|
+
|
85
|
+
# All methods that should have a ! method added that modifies
|
86
|
+
# the receiver.
|
87
|
+
MUTATION_METHODS = %w'and distinct exclude exists filter from from_self full_outer_join graph
|
88
|
+
group group_and_count group_by having inner_join intersect invert join
|
89
|
+
left_outer_join limit naked or order order_by order_more paginate query reject
|
90
|
+
reverse reverse_order right_outer_join select select_all select_more
|
91
|
+
set_graph_aliases set_model sort sort_by unfiltered union unordered where'.collect{|x| x.to_sym}
|
92
|
+
|
93
|
+
NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
|
94
|
+
STOCK_TRANSFORMS = {
|
95
|
+
:marshal => [
|
96
|
+
# for backwards-compatibility we support also non-base64-encoded values.
|
97
|
+
proc {|v| Marshal.load(v.unpack('m')[0]) rescue Marshal.load(v)},
|
98
|
+
proc {|v| [Marshal.dump(v)].pack('m')}
|
99
|
+
],
|
100
|
+
:yaml => [
|
101
|
+
proc {|v| YAML.load v if v},
|
102
|
+
proc {|v| v.to_yaml}
|
103
|
+
]
|
104
|
+
}
|
105
|
+
|
106
|
+
# The database that corresponds to this dataset
|
107
|
+
attr_accessor :db
|
108
|
+
|
109
|
+
# The hash of options for this dataset, keys are symbols.
|
110
|
+
attr_accessor :opts
|
111
|
+
|
112
|
+
# The row_proc for this database, should be a Proc that takes
|
113
|
+
# a single hash argument and returns the object you want to
|
114
|
+
# fetch_rows to return.
|
115
|
+
attr_accessor :row_proc
|
116
|
+
|
117
|
+
# Whether to quote identifiers for this dataset
|
118
|
+
attr_writer :quote_identifiers
|
119
|
+
|
120
|
+
# Constructs a new instance of a dataset with an associated database and
|
121
|
+
# options. Datasets are usually constructed by invoking Database methods:
|
122
|
+
#
|
123
|
+
# DB[:posts]
|
124
|
+
#
|
125
|
+
# Or:
|
126
|
+
#
|
127
|
+
# DB.dataset # the returned dataset is blank
|
128
|
+
#
|
129
|
+
# Sequel::Dataset is an abstract class that is not useful by itself. Each
|
130
|
+
# database adaptor should provide a descendant class of Sequel::Dataset.
|
131
|
+
def initialize(db, opts = nil)
|
132
|
+
@db = db
|
133
|
+
@quote_identifiers = db.quote_identifiers? if db.respond_to?(:quote_identifiers?)
|
134
|
+
@opts = opts || {}
|
135
|
+
@row_proc = nil
|
136
|
+
@transform = nil
|
137
|
+
end
|
138
|
+
|
139
|
+
### Class Methods ###
|
140
|
+
|
141
|
+
# The array of dataset subclasses.
|
142
|
+
def self.dataset_classes
|
143
|
+
DATASET_CLASSES
|
144
|
+
end
|
145
|
+
|
146
|
+
# Setup mutation (e.g. filter!) methods. These operate the same as the
|
147
|
+
# non-! methods, but replace the options of the current dataset with the
|
148
|
+
# options of the resulting dataset.
|
149
|
+
def self.def_mutation_method(*meths)
|
150
|
+
meths.each do |meth|
|
151
|
+
class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end")
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Add the subclass to the array of subclasses.
|
156
|
+
def self.inherited(c)
|
157
|
+
DATASET_CLASSES << c
|
158
|
+
end
|
159
|
+
|
160
|
+
### Instance Methods ###
|
161
|
+
|
162
|
+
# Alias for insert, but not aliased directly so subclasses
|
163
|
+
# don't have to override both methods.
|
164
|
+
def <<(*args)
|
165
|
+
insert(*args)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Return the dataset as a column with the given alias, so it can be used in the
|
169
|
+
# SELECT clause. This dataset should result in a single row and a single column.
|
170
|
+
def as(aliaz)
|
171
|
+
::Sequel::SQL::AliasedExpression.new(self, aliaz)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns an array with all records in the dataset. If a block is given,
|
175
|
+
# the array is iterated over after all items have been loaded.
|
176
|
+
def all(opts = nil, &block)
|
177
|
+
a = []
|
178
|
+
each(opts) {|r| a << r}
|
179
|
+
post_load(a)
|
180
|
+
a.each(&block) if block
|
181
|
+
a
|
182
|
+
end
|
183
|
+
|
184
|
+
# Returns a new clone of the dataset with with the given options merged.
|
185
|
+
# If the options changed include options in COLUMN_CHANGE_OPTS, the cached
|
186
|
+
# columns are deleted.
|
187
|
+
def clone(opts = {})
|
188
|
+
c = super()
|
189
|
+
c.opts = @opts.merge(opts)
|
190
|
+
c.instance_variable_set(:@columns, nil) if opts.keys.any?{|o| COLUMN_CHANGE_OPTS.include?(o)}
|
191
|
+
c
|
192
|
+
end
|
193
|
+
|
194
|
+
# Returns the columns in the result set in their true order.
|
195
|
+
# If the columns are currently cached, returns the cached value. Otherwise,
|
196
|
+
# a SELECT query is performed to get a single row. Adapters are expected
|
197
|
+
# to fill the columns cache with the column information when a query is performed.
|
198
|
+
# If the dataset does not have any rows, this will be an empty array.
|
199
|
+
# If you are looking for all columns for a single table, see Schema::SQL#schema.
|
200
|
+
def columns
|
201
|
+
return @columns if @columns
|
202
|
+
ds = unfiltered.unordered.clone(:distinct => nil)
|
203
|
+
ds.single_record
|
204
|
+
@columns = ds.instance_variable_get(:@columns)
|
205
|
+
@columns || []
|
206
|
+
end
|
207
|
+
|
208
|
+
# Remove the cached list of columns and do a SELECT query to find
|
209
|
+
# the columns.
|
210
|
+
def columns!
|
211
|
+
@columns = nil
|
212
|
+
columns
|
213
|
+
end
|
214
|
+
|
215
|
+
# Add a mutation method to this dataset instance.
|
216
|
+
def def_mutation_method(*meths)
|
217
|
+
meths.each do |meth|
|
218
|
+
instance_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end")
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Deletes the records in the dataset. Adapters should override this method.
|
223
|
+
def delete(*args)
|
224
|
+
@db.execute_dui(delete_sql(*args))
|
225
|
+
end
|
226
|
+
|
227
|
+
# Iterates over the records in the dataset.
|
228
|
+
def each(opts = nil, &block)
|
229
|
+
if graph = @opts[:graph]
|
230
|
+
graph_each(opts, &block)
|
231
|
+
else
|
232
|
+
row_proc = @row_proc unless opts && opts[:naked]
|
233
|
+
transform = @transform
|
234
|
+
fetch_rows(select_sql(opts)) do |r|
|
235
|
+
r = transform_load(r) if transform
|
236
|
+
r = row_proc[r] if row_proc
|
237
|
+
yield r
|
238
|
+
end
|
239
|
+
end
|
240
|
+
self
|
241
|
+
end
|
242
|
+
|
243
|
+
# Executes a select query and fetches records, passing each record to the
|
244
|
+
# supplied block. Adapters should override this method.
|
245
|
+
def fetch_rows(sql, &block)
|
246
|
+
raise NotImplementedError, NOTIMPL_MSG
|
247
|
+
end
|
248
|
+
|
249
|
+
# Inserts values into the associated table. Adapters should override this
|
250
|
+
# method.
|
251
|
+
def insert(*values)
|
252
|
+
@db.execute_dui(insert_sql(*values))
|
253
|
+
end
|
254
|
+
|
255
|
+
# Returns a string representation of the dataset including the class name
|
256
|
+
# and the corresponding SQL select statement.
|
257
|
+
def inspect
|
258
|
+
"#<#{self.class}: #{sql.inspect}>"
|
259
|
+
end
|
260
|
+
|
261
|
+
# Returns the the model classes associated with the dataset as a hash.
|
262
|
+
# If the dataset is associated with a single model class, a key of nil
|
263
|
+
# is used. For datasets with polymorphic models, the keys are
|
264
|
+
# values of the polymorphic column and the values are the corresponding
|
265
|
+
# model classes to which they map.
|
266
|
+
def model_classes
|
267
|
+
@opts[:models]
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns a naked dataset clone - i.e. a dataset that returns records as
|
271
|
+
# hashes rather than model objects.
|
272
|
+
def naked
|
273
|
+
clone.set_model(nil)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Returns the column name for the polymorphic key.
|
277
|
+
def polymorphic_key
|
278
|
+
@opts[:polymorphic_key]
|
279
|
+
end
|
280
|
+
|
281
|
+
# Whether this dataset quotes identifiers.
|
282
|
+
def quote_identifiers?
|
283
|
+
@quote_identifiers
|
284
|
+
end
|
285
|
+
|
286
|
+
# Alias for set, but not aliased directly so subclasses
|
287
|
+
# don't have to override both methods.
|
288
|
+
def set(*args)
|
289
|
+
update(*args)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Associates or disassociates the dataset with a model(s). If
|
293
|
+
# nil is specified, the dataset is turned into a naked dataset and returns
|
294
|
+
# records as hashes. If a model class specified, the dataset is modified
|
295
|
+
# to return records as instances of the model class, e.g:
|
296
|
+
#
|
297
|
+
# class MyModel
|
298
|
+
# def initialize(values)
|
299
|
+
# @values = values
|
300
|
+
# ...
|
301
|
+
# end
|
302
|
+
# end
|
303
|
+
#
|
304
|
+
# dataset.set_model(MyModel)
|
305
|
+
#
|
306
|
+
# You can also provide additional arguments to be passed to the model's
|
307
|
+
# initialize method:
|
308
|
+
#
|
309
|
+
# class MyModel
|
310
|
+
# def initialize(values, options)
|
311
|
+
# @values = values
|
312
|
+
# ...
|
313
|
+
# end
|
314
|
+
# end
|
315
|
+
#
|
316
|
+
# dataset.set_model(MyModel, :allow_delete => false)
|
317
|
+
#
|
318
|
+
# The dataset can be made polymorphic by specifying a column name as the
|
319
|
+
# polymorphic key and a hash mapping column values to model classes.
|
320
|
+
#
|
321
|
+
# dataset.set_model(:kind, {1 => Person, 2 => Business})
|
322
|
+
#
|
323
|
+
# You can also set a default model class to fall back on by specifying a
|
324
|
+
# class corresponding to nil:
|
325
|
+
#
|
326
|
+
# dataset.set_model(:kind, {nil => DefaultClass, 1 => Person, 2 => Business})
|
327
|
+
#
|
328
|
+
# To make sure that there is always a default model class, the hash provided
|
329
|
+
# should have a default value. To make the dataset map string values to
|
330
|
+
# model classes, and keep a good default, try:
|
331
|
+
#
|
332
|
+
# dataset.set_model(:kind, Hash.new{|h,k| h[k] = (k.constantize rescue DefaultClass)})
|
333
|
+
def set_model(key, *args)
|
334
|
+
# This code is more verbose then necessary for performance reasons
|
335
|
+
case key
|
336
|
+
when nil # set_model(nil) => no argument provided, so the dataset is denuded
|
337
|
+
@opts.merge!(:naked => true, :models => nil, :polymorphic_key => nil)
|
338
|
+
self.row_proc = nil
|
339
|
+
when Class
|
340
|
+
# isomorphic model
|
341
|
+
@opts.merge!(:naked => nil, :models => {nil => key}, :polymorphic_key => nil)
|
342
|
+
if key.respond_to?(:load)
|
343
|
+
# the class has a values setter method, so we use it
|
344
|
+
self.row_proc = proc{|h| key.load(h, *args)}
|
345
|
+
else
|
346
|
+
# otherwise we just pass the hash to the constructor
|
347
|
+
self.row_proc = proc{|h| key.new(h, *args)}
|
348
|
+
end
|
349
|
+
when Symbol
|
350
|
+
# polymorphic model
|
351
|
+
hash = args.shift || raise(ArgumentError, "No class hash supplied for polymorphic model")
|
352
|
+
@opts.merge!(:naked => true, :models => hash, :polymorphic_key => key)
|
353
|
+
if (hash.empty? ? (hash[nil] rescue nil) : hash.values.first).respond_to?(:load)
|
354
|
+
# the class has a values setter method, so we use it
|
355
|
+
self.row_proc = proc do |h|
|
356
|
+
c = hash[h[key]] || hash[nil] || \
|
357
|
+
raise(Error, "No matching model class for record (#{polymorphic_key} => #{h[polymorphic_key].inspect})")
|
358
|
+
c.load(h, *args)
|
359
|
+
end
|
360
|
+
else
|
361
|
+
# otherwise we just pass the hash to the constructor
|
362
|
+
self.row_proc = proc do |h|
|
363
|
+
c = hash[h[key]] || hash[nil] || \
|
364
|
+
raise(Error, "No matching model class for record (#{polymorphic_key} => #{h[polymorphic_key].inspect})")
|
365
|
+
c.new(h, *args)
|
366
|
+
end
|
367
|
+
end
|
368
|
+
else
|
369
|
+
raise ArgumentError, "Invalid model specified"
|
370
|
+
end
|
371
|
+
self
|
372
|
+
end
|
373
|
+
|
374
|
+
# Sets a value transform which is used to convert values loaded and saved
|
375
|
+
# to/from the database. The transform should be supplied as a hash. Each
|
376
|
+
# value in the hash should be an array containing two proc objects - one
|
377
|
+
# for transforming loaded values, and one for transforming saved values.
|
378
|
+
# The following example demonstrates how to store Ruby objects in a dataset
|
379
|
+
# using Marshal serialization:
|
380
|
+
#
|
381
|
+
# dataset.transform(:obj => [
|
382
|
+
# proc {|v| Marshal.load(v)},
|
383
|
+
# proc {|v| Marshal.dump(v)}
|
384
|
+
# ])
|
385
|
+
#
|
386
|
+
# dataset.insert_sql(:obj => 1234) #=>
|
387
|
+
# "INSERT INTO items (obj) VALUES ('\004\bi\002\322\004')"
|
388
|
+
#
|
389
|
+
# Another form of using transform is by specifying stock transforms:
|
390
|
+
#
|
391
|
+
# dataset.transform(:obj => :marshal)
|
392
|
+
#
|
393
|
+
# The currently supported stock transforms are :marshal and :yaml.
|
394
|
+
def transform(t)
|
395
|
+
@transform = t
|
396
|
+
t.each do |k, v|
|
397
|
+
case v
|
398
|
+
when Array
|
399
|
+
if (v.size != 2) || !v.first.is_a?(Proc) && !v.last.is_a?(Proc)
|
400
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
401
|
+
end
|
402
|
+
else
|
403
|
+
unless v = STOCK_TRANSFORMS[v]
|
404
|
+
raise Error::InvalidTransform, "Invalid transform specified"
|
405
|
+
else
|
406
|
+
t[k] = v
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
self
|
411
|
+
end
|
412
|
+
|
413
|
+
# Applies the value transform for data loaded from the database.
|
414
|
+
def transform_load(r)
|
415
|
+
r.inject({}) do |m, kv|
|
416
|
+
k, v = *kv
|
417
|
+
m[k] = (tt = @transform[k]) ? tt[0][v] : v
|
418
|
+
m
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Applies the value transform for data saved to the database.
|
423
|
+
def transform_save(r)
|
424
|
+
r.inject({}) do |m, kv|
|
425
|
+
k, v = *kv
|
426
|
+
m[k] = (tt = @transform[k]) ? tt[1][v] : v
|
427
|
+
m
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
# Updates values for the dataset. Adapters should override this method.
|
432
|
+
def update(*args)
|
433
|
+
@db.execute_dui(update_sql(*args))
|
434
|
+
end
|
435
|
+
|
436
|
+
# Add the mutation methods via metaprogramming
|
437
|
+
def_mutation_method(*MUTATION_METHODS)
|
438
|
+
|
439
|
+
protected
|
440
|
+
|
441
|
+
# Return true if the dataset has a non-nil value for any key in opts.
|
442
|
+
def options_overlap(opts)
|
443
|
+
!(@opts.collect{|k,v| k unless v.nil?}.compact & opts).empty?
|
444
|
+
end
|
445
|
+
|
446
|
+
private
|
447
|
+
|
448
|
+
# Modify the receiver with the results of sending the meth, args, and block
|
449
|
+
# to the receiver and merging the options of the resulting dataset into
|
450
|
+
# the receiver's options.
|
451
|
+
def mutation_method(meth, *args, &block)
|
452
|
+
copy = send(meth, *args, &block)
|
453
|
+
@opts.merge!(copy.opts)
|
454
|
+
self
|
455
|
+
end
|
456
|
+
end
|
457
|
+
end
|