sequel 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. data/CHANGELOG +1551 -4
  2. data/README +306 -19
  3. data/Rakefile +84 -56
  4. data/bin/sequel +106 -0
  5. data/doc/cheat_sheet.rdoc +225 -0
  6. data/doc/dataset_filtering.rdoc +182 -0
  7. data/lib/sequel_core.rb +136 -0
  8. data/lib/sequel_core/adapters/adapter_skeleton.rb +54 -0
  9. data/lib/sequel_core/adapters/ado.rb +80 -0
  10. data/lib/sequel_core/adapters/db2.rb +148 -0
  11. data/lib/sequel_core/adapters/dbi.rb +117 -0
  12. data/lib/sequel_core/adapters/informix.rb +78 -0
  13. data/lib/sequel_core/adapters/jdbc.rb +186 -0
  14. data/lib/sequel_core/adapters/jdbc/mysql.rb +55 -0
  15. data/lib/sequel_core/adapters/jdbc/postgresql.rb +66 -0
  16. data/lib/sequel_core/adapters/jdbc/sqlite.rb +47 -0
  17. data/lib/sequel_core/adapters/mysql.rb +231 -0
  18. data/lib/sequel_core/adapters/odbc.rb +155 -0
  19. data/lib/sequel_core/adapters/odbc_mssql.rb +106 -0
  20. data/lib/sequel_core/adapters/openbase.rb +64 -0
  21. data/lib/sequel_core/adapters/oracle.rb +170 -0
  22. data/lib/sequel_core/adapters/postgres.rb +199 -0
  23. data/lib/sequel_core/adapters/shared/mysql.rb +275 -0
  24. data/lib/sequel_core/adapters/shared/postgres.rb +351 -0
  25. data/lib/sequel_core/adapters/shared/sqlite.rb +146 -0
  26. data/lib/sequel_core/adapters/sqlite.rb +138 -0
  27. data/lib/sequel_core/connection_pool.rb +194 -0
  28. data/lib/sequel_core/core_ext.rb +203 -0
  29. data/lib/sequel_core/core_sql.rb +184 -0
  30. data/lib/sequel_core/database.rb +471 -0
  31. data/lib/sequel_core/database/schema.rb +156 -0
  32. data/lib/sequel_core/dataset.rb +457 -0
  33. data/lib/sequel_core/dataset/callback.rb +13 -0
  34. data/lib/sequel_core/dataset/convenience.rb +245 -0
  35. data/lib/sequel_core/dataset/pagination.rb +96 -0
  36. data/lib/sequel_core/dataset/query.rb +41 -0
  37. data/lib/sequel_core/dataset/schema.rb +15 -0
  38. data/lib/sequel_core/dataset/sql.rb +889 -0
  39. data/lib/sequel_core/deprecated.rb +26 -0
  40. data/lib/sequel_core/exceptions.rb +42 -0
  41. data/lib/sequel_core/migration.rb +187 -0
  42. data/lib/sequel_core/object_graph.rb +216 -0
  43. data/lib/sequel_core/pretty_table.rb +71 -0
  44. data/lib/sequel_core/schema.rb +2 -0
  45. data/lib/sequel_core/schema/generator.rb +239 -0
  46. data/lib/sequel_core/schema/sql.rb +325 -0
  47. data/lib/sequel_core/sql.rb +812 -0
  48. data/lib/sequel_model.rb +5 -1
  49. data/lib/sequel_model/association_reflection.rb +3 -8
  50. data/lib/sequel_model/base.rb +15 -10
  51. data/lib/sequel_model/inflector.rb +3 -5
  52. data/lib/sequel_model/plugins.rb +1 -1
  53. data/lib/sequel_model/record.rb +11 -3
  54. data/lib/sequel_model/schema.rb +4 -4
  55. data/lib/sequel_model/validations.rb +6 -1
  56. data/spec/adapters/ado_spec.rb +17 -0
  57. data/spec/adapters/informix_spec.rb +96 -0
  58. data/spec/adapters/mysql_spec.rb +764 -0
  59. data/spec/adapters/oracle_spec.rb +222 -0
  60. data/spec/adapters/postgres_spec.rb +441 -0
  61. data/spec/adapters/spec_helper.rb +7 -0
  62. data/spec/adapters/sqlite_spec.rb +400 -0
  63. data/spec/integration/dataset_test.rb +51 -0
  64. data/spec/integration/eager_loader_test.rb +702 -0
  65. data/spec/integration/schema_test.rb +102 -0
  66. data/spec/integration/spec_helper.rb +44 -0
  67. data/spec/integration/type_test.rb +43 -0
  68. data/spec/rcov.opts +2 -0
  69. data/spec/sequel_core/connection_pool_spec.rb +363 -0
  70. data/spec/sequel_core/core_ext_spec.rb +156 -0
  71. data/spec/sequel_core/core_sql_spec.rb +427 -0
  72. data/spec/sequel_core/database_spec.rb +964 -0
  73. data/spec/sequel_core/dataset_spec.rb +2977 -0
  74. data/spec/sequel_core/expression_filters_spec.rb +346 -0
  75. data/spec/sequel_core/migration_spec.rb +261 -0
  76. data/spec/sequel_core/object_graph_spec.rb +234 -0
  77. data/spec/sequel_core/pretty_table_spec.rb +58 -0
  78. data/spec/sequel_core/schema_generator_spec.rb +122 -0
  79. data/spec/sequel_core/schema_spec.rb +497 -0
  80. data/spec/sequel_core/spec_helper.rb +51 -0
  81. data/spec/{association_reflection_spec.rb → sequel_model/association_reflection_spec.rb} +6 -6
  82. data/spec/{associations_spec.rb → sequel_model/associations_spec.rb} +47 -18
  83. data/spec/{base_spec.rb → sequel_model/base_spec.rb} +2 -1
  84. data/spec/{caching_spec.rb → sequel_model/caching_spec.rb} +0 -0
  85. data/spec/{dataset_methods_spec.rb → sequel_model/dataset_methods_spec.rb} +13 -1
  86. data/spec/{eager_loading_spec.rb → sequel_model/eager_loading_spec.rb} +75 -14
  87. data/spec/{hooks_spec.rb → sequel_model/hooks_spec.rb} +4 -4
  88. data/spec/sequel_model/inflector_spec.rb +119 -0
  89. data/spec/{model_spec.rb → sequel_model/model_spec.rb} +30 -11
  90. data/spec/{plugins_spec.rb → sequel_model/plugins_spec.rb} +0 -0
  91. data/spec/{record_spec.rb → sequel_model/record_spec.rb} +47 -6
  92. data/spec/{schema_spec.rb → sequel_model/schema_spec.rb} +18 -4
  93. data/spec/{spec_helper.rb → sequel_model/spec_helper.rb} +3 -2
  94. data/spec/{validations_spec.rb → sequel_model/validations_spec.rb} +37 -17
  95. data/spec/spec_config.rb +9 -0
  96. data/spec/spec_config.rb.example +10 -0
  97. metadata +110 -37
  98. 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