sequel 3.0.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/CHANGELOG +100 -0
  2. data/README.rdoc +3 -3
  3. data/bin/sequel +102 -19
  4. data/doc/reflection.rdoc +83 -0
  5. data/doc/release_notes/3.1.0.txt +406 -0
  6. data/lib/sequel/adapters/ado.rb +11 -0
  7. data/lib/sequel/adapters/amalgalite.rb +5 -20
  8. data/lib/sequel/adapters/do.rb +44 -36
  9. data/lib/sequel/adapters/firebird.rb +29 -43
  10. data/lib/sequel/adapters/jdbc.rb +17 -27
  11. data/lib/sequel/adapters/mysql.rb +35 -40
  12. data/lib/sequel/adapters/odbc.rb +4 -23
  13. data/lib/sequel/adapters/oracle.rb +22 -19
  14. data/lib/sequel/adapters/postgres.rb +6 -15
  15. data/lib/sequel/adapters/shared/mssql.rb +1 -1
  16. data/lib/sequel/adapters/shared/mysql.rb +29 -10
  17. data/lib/sequel/adapters/shared/oracle.rb +6 -8
  18. data/lib/sequel/adapters/shared/postgres.rb +28 -72
  19. data/lib/sequel/adapters/shared/sqlite.rb +5 -3
  20. data/lib/sequel/adapters/sqlite.rb +5 -20
  21. data/lib/sequel/adapters/utils/savepoint_transactions.rb +80 -0
  22. data/lib/sequel/adapters/utils/unsupported.rb +0 -12
  23. data/lib/sequel/core.rb +12 -3
  24. data/lib/sequel/core_sql.rb +1 -8
  25. data/lib/sequel/database.rb +107 -43
  26. data/lib/sequel/database/schema_generator.rb +1 -0
  27. data/lib/sequel/database/schema_methods.rb +38 -4
  28. data/lib/sequel/dataset.rb +6 -0
  29. data/lib/sequel/dataset/convenience.rb +2 -2
  30. data/lib/sequel/dataset/graph.rb +2 -2
  31. data/lib/sequel/dataset/prepared_statements.rb +3 -8
  32. data/lib/sequel/dataset/sql.rb +93 -19
  33. data/lib/sequel/extensions/blank.rb +2 -1
  34. data/lib/sequel/extensions/inflector.rb +4 -3
  35. data/lib/sequel/extensions/migration.rb +13 -2
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pretty_table.rb +4 -0
  38. data/lib/sequel/extensions/query.rb +4 -0
  39. data/lib/sequel/extensions/schema_dumper.rb +100 -24
  40. data/lib/sequel/extensions/string_date_time.rb +3 -4
  41. data/lib/sequel/model.rb +2 -1
  42. data/lib/sequel/model/associations.rb +96 -38
  43. data/lib/sequel/model/base.rb +14 -14
  44. data/lib/sequel/model/plugins.rb +32 -21
  45. data/lib/sequel/plugins/caching.rb +13 -15
  46. data/lib/sequel/plugins/identity_map.rb +107 -0
  47. data/lib/sequel/plugins/lazy_attributes.rb +65 -0
  48. data/lib/sequel/plugins/many_through_many.rb +188 -0
  49. data/lib/sequel/plugins/schema.rb +13 -0
  50. data/lib/sequel/plugins/serialization.rb +53 -37
  51. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  52. data/lib/sequel/plugins/tactical_eager_loading.rb +61 -0
  53. data/lib/sequel/plugins/validation_class_methods.rb +28 -7
  54. data/lib/sequel/plugins/validation_helpers.rb +31 -24
  55. data/lib/sequel/sql.rb +16 -0
  56. data/lib/sequel/version.rb +1 -1
  57. data/spec/adapters/ado_spec.rb +47 -1
  58. data/spec/adapters/firebird_spec.rb +39 -36
  59. data/spec/adapters/mysql_spec.rb +25 -9
  60. data/spec/adapters/postgres_spec.rb +11 -24
  61. data/spec/core/database_spec.rb +54 -13
  62. data/spec/core/dataset_spec.rb +147 -29
  63. data/spec/core/object_graph_spec.rb +6 -1
  64. data/spec/core/schema_spec.rb +34 -0
  65. data/spec/core/spec_helper.rb +0 -2
  66. data/spec/extensions/caching_spec.rb +7 -0
  67. data/spec/extensions/identity_map_spec.rb +158 -0
  68. data/spec/extensions/lazy_attributes_spec.rb +113 -0
  69. data/spec/extensions/many_through_many_spec.rb +813 -0
  70. data/spec/extensions/migration_spec.rb +4 -4
  71. data/spec/extensions/schema_dumper_spec.rb +114 -13
  72. data/spec/extensions/schema_spec.rb +19 -3
  73. data/spec/extensions/serialization_spec.rb +28 -0
  74. data/spec/extensions/single_table_inheritance_spec.rb +25 -1
  75. data/spec/extensions/spec_helper.rb +2 -7
  76. data/spec/extensions/tactical_eager_loading_spec.rb +65 -0
  77. data/spec/extensions/validation_class_methods_spec.rb +10 -5
  78. data/spec/integration/dataset_test.rb +39 -6
  79. data/spec/integration/eager_loader_test.rb +7 -7
  80. data/spec/integration/spec_helper.rb +0 -1
  81. data/spec/integration/transaction_test.rb +28 -1
  82. data/spec/model/association_reflection_spec.rb +29 -3
  83. data/spec/model/associations_spec.rb +1 -0
  84. data/spec/model/eager_loading_spec.rb +70 -1
  85. data/spec/model/plugins_spec.rb +236 -50
  86. data/spec/model/spec_helper.rb +0 -2
  87. metadata +18 -5
@@ -1,5 +1,13 @@
1
1
  module Sequel
2
2
  module Plugins
3
+ # Sequel's built in schema plugin allows you to define your schema
4
+ # directly in the model using Model.set_schema (which takes a block
5
+ # similar to Database#create_table), and use Model.create_table to
6
+ # create a table using the schema information.
7
+ #
8
+ # This plugin is mostly suited to test code. If there is any
9
+ # chance that your application's schema could change, you should
10
+ # be using the migration extension instead.
3
11
  module Schema
4
12
  module ClassMethods
5
13
  # Creates table, using the column information from set_schema.
@@ -15,6 +23,11 @@ module Sequel
15
23
  drop_table rescue nil
16
24
  create_table
17
25
  end
26
+
27
+ # Creates the table unless the table already exists
28
+ def create_table?
29
+ create_table unless table_exists?
30
+ end
18
31
 
19
32
  # Drops table.
20
33
  def drop_table
@@ -21,47 +21,59 @@ module Sequel
21
21
  module Serialization
22
22
  # Set up the column readers to do deserialization and the column writers
23
23
  # to save the value in deserialized_values.
24
- def self.apply(model, format, *columns)
25
- raise(Error, "Unsupported serialization format (#{format}), should be :marshal or :yaml") unless [:marshal, :yaml].include?(format)
26
- raise(Error, "No columns given. The serialization plugin requires you specify which columns to serialize") if columns.empty?
27
- model.instance_eval do
28
- @serialization_format = format
29
- @serialized_columns = columns
30
- InstanceMethods.module_eval do
31
- columns.each do |column|
32
- define_method(column) do
33
- if deserialized_values.has_key?(column)
34
- deserialized_values[column]
35
- else
36
- deserialized_values[column] = deserialize_value(@values[column])
37
- end
38
- end
39
- define_method("#{column}=") do |v|
40
- changed_columns << column unless changed_columns.include?(column)
41
- deserialized_values[column] = v
42
- end
43
- end
44
- end
45
- end
24
+ def self.apply(model, *args)
25
+ model.instance_eval{@serialization_map = {}}
26
+ end
27
+
28
+ def self.configure(model, format=nil, *columns)
29
+ model.serialize_attributes(format, *columns) unless columns.empty?
46
30
  end
47
31
 
48
32
  module ClassMethods
49
- # The serialization format to use, should be :marshal or :yaml
50
- attr_reader :serialization_format
51
-
52
- # The columns to serialize
53
- attr_reader :serialized_columns
33
+ # A map of the serialized columns for this model. Keys are column
34
+ # symbols, values are serialization formats (:marshal or :yaml).
35
+ attr_reader :serialization_map
54
36
 
55
37
  # Copy the serialization format and columns to serialize into the subclass.
56
38
  def inherited(subclass)
57
39
  super
58
- sf = serialization_format
59
- sc = serialized_columns
60
- subclass.instance_eval do
61
- @serialization_format = sf
62
- @serialized_columns = sc
40
+ sm = serialization_map.dup
41
+ subclass.instance_eval{@serialization_map = sm}
42
+ end
43
+
44
+ # The first value in the serialization map. This is only for
45
+ # backwards compatibility, use serialization_map in new code.
46
+ def serialization_format
47
+ serialization_map.values.first
48
+ end
49
+
50
+ # Create instance level reader that deserializes column values on request,
51
+ # and instance level writer that stores new deserialized value in deserialized
52
+ # columns
53
+ def serialize_attributes(format, *columns)
54
+ raise(Error, "Unsupported serialization format (#{format}), should be :marshal or :yaml") unless [:marshal, :yaml].include?(format)
55
+ raise(Error, "No columns given. The serialization plugin requires you specify which columns to serialize") if columns.empty?
56
+ columns.each do |column|
57
+ serialization_map[column] = format
58
+ define_method(column) do
59
+ if deserialized_values.has_key?(column)
60
+ deserialized_values[column]
61
+ else
62
+ deserialized_values[column] = deserialize_value(column, @values[column])
63
+ end
64
+ end
65
+ define_method("#{column}=") do |v|
66
+ changed_columns << column unless changed_columns.include?(column)
67
+ deserialized_values[column] = v
68
+ end
63
69
  end
64
70
  end
71
+
72
+ # The columns that will be serialized. This is only for
73
+ # backwards compatibility, use serialization_map in new code.
74
+ def serialized_columns
75
+ serialization_map.keys
76
+ end
65
77
  end
66
78
 
67
79
  module InstanceMethods
@@ -78,7 +90,7 @@ module Sequel
78
90
  def before_save
79
91
  super
80
92
  deserialized_values.each do |k,v|
81
- @values[k] = serialize_value(v)
93
+ @values[k] = serialize_value(k, v)
82
94
  end
83
95
  end
84
96
 
@@ -91,24 +103,28 @@ module Sequel
91
103
  private
92
104
 
93
105
  # Deserialize the column from either marshal or yaml format
94
- def deserialize_value(v)
106
+ def deserialize_value(column, v)
95
107
  return v if v.nil?
96
- case model.serialization_format
108
+ case model.serialization_map[column]
97
109
  when :marshal
98
110
  Marshal.load(v.unpack('m')[0]) rescue Marshal.load(v)
99
111
  when :yaml
100
112
  YAML.load v if v
113
+ else
114
+ raise Error, "Bad serialization format (#{model.serialization_map[column].inspect}) for column #{column.inspect}"
101
115
  end
102
116
  end
103
117
 
104
118
  # Serialize the column to either marshal or yaml format
105
- def serialize_value(v)
119
+ def serialize_value(column, v)
106
120
  return v if v.nil?
107
- case model.serialization_format
121
+ case model.serialization_map[column]
108
122
  when :marshal
109
123
  [Marshal.dump(v)].pack('m')
110
124
  when :yaml
111
125
  v.to_yaml
126
+ else
127
+ raise Error, "Bad serialization format (#{model.serialization_map[column].inspect}) for column #{column.inspect}"
112
128
  end
113
129
  end
114
130
  end
@@ -18,7 +18,7 @@ module Sequel
18
18
  # Set the sti_key and sti_dataset for the model, and change the
19
19
  # dataset's row_proc so that the dataset yields objects of varying classes,
20
20
  # where the class used has the same name as the key field.
21
- def self.apply(model, key)
21
+ def self.configure(model, key)
22
22
  m = model.method(:constantize)
23
23
  model.instance_eval do
24
24
  @sti_key = key
@@ -0,0 +1,61 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The tactical_eager_loading plugin allows you to eagerly load
4
+ # an association for all objects retrieved from the same dataset
5
+ # without calling eager on the dataset. If you attempt to load
6
+ # associated objects for a record and the association for that
7
+ # object is currently not cached, it assumes you want to get
8
+ # the associated objects for all objects retrieved with the dataset that
9
+ # retrieved the current object.
10
+ #
11
+ # Tactical eager loading only takes affect if you retrieved the
12
+ # current object with Dataset#all, it doesn't work if you
13
+ # retrieved the current object with Dataset#each.
14
+ #
15
+ # Basically, this allows the following code to issue only two queries:
16
+ #
17
+ # Album.filter{id<100}.all do |a|
18
+ # a.artists
19
+ # end
20
+ module TacticalEagerLoading
21
+ module InstanceMethods
22
+ # The dataset that retrieved this object, set if the object was
23
+ # reteived via Dataset#all with an active identity map.
24
+ attr_accessor :retrieved_by
25
+
26
+ # All model objects retrieved with this object, set if the object was
27
+ # reteived via Dataset#all with an active identity map.
28
+ attr_accessor :retrieved_with
29
+
30
+ private
31
+
32
+ # If there is an active identity map and the association is not in the
33
+ # associations cache and the object was reteived via Dataset#all,
34
+ # eagerly load the association for all model objects retrieved with the
35
+ # current object.
36
+ def load_associated_objects(opts, reload=false)
37
+ name = opts[:name]
38
+ if !associations.include?(name) && retrieved_by
39
+ retrieved_by.send(:eager_load, retrieved_with, name=>{})
40
+ end
41
+ super
42
+ end
43
+ end
44
+
45
+ module DatasetMethods
46
+ private
47
+
48
+ # If there is an active identity map, set the reteived_with attribute for the object
49
+ # with the current dataset and array of all objects.
50
+ def post_load(objects)
51
+ super
52
+ objects.each do |o|
53
+ next unless o.is_a?(Sequel::Model)
54
+ o.retrieved_by = self
55
+ o.retrieved_with = objects
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,9 +1,26 @@
1
1
  module Sequel
2
- require 'extensions/blank'
2
+ extension :blank
3
3
 
4
4
  module Plugins
5
+ # Sequel's built-in validation_class_methods plugin adds backwards compatibility
6
+ # for the legacy class-level validation methods (e.g. validates_presence_of :column).
7
+ #
8
+ # It is recommended to use the validation_helpers plugin instead of this one,
9
+ # as it is less complex and more flexible.
5
10
  module ValidationClassMethods
11
+ # Setup the validations hash for the given model.
12
+ def self.apply(model)
13
+ model.class_eval do
14
+ @validation_mutex = Mutex.new
15
+ @validations = {}
16
+ end
17
+ end
18
+
6
19
  module ClassMethods
20
+ # A hash of associations for this model class. Keys are column symbols,
21
+ # values are arrays of validation procs.
22
+ attr_reader :validations
23
+
7
24
  # The Generator class is used to generate validation definitions using
8
25
  # the validates {} idiom.
9
26
  class Generator
@@ -23,6 +40,15 @@ module Sequel
23
40
  def has_validations?
24
41
  !validations.empty?
25
42
  end
43
+
44
+ # Setup the validations hash in the subclass
45
+ def inherited(subclass)
46
+ super
47
+ subclass.class_eval do
48
+ @validation_mutex = Mutex.new
49
+ @validations = {}
50
+ end
51
+ end
26
52
 
27
53
  # Instructs the model to skip validations defined in superclasses
28
54
  def skip_superclass_validations
@@ -146,7 +172,7 @@ module Sequel
146
172
  end
147
173
  tag = opts[:tag]
148
174
  atts.each do |a|
149
- a_vals = validations[a]
175
+ a_vals = @validation_mutex.synchronize{validations[a] ||= []}
150
176
  if tag && (old = a_vals.find{|x| x[0] == tag})
151
177
  old[1] = blk
152
178
  else
@@ -348,11 +374,6 @@ module Sequel
348
374
  end
349
375
  end
350
376
 
351
- # Returns the validations hash for the class.
352
- def validations
353
- @validations ||= Hash.new {|h, k| h[k] = []}
354
- end
355
-
356
377
  private
357
378
 
358
379
  # Removes and returns the last member of the array if it is a hash. Otherwise,
@@ -1,30 +1,37 @@
1
1
  module Sequel
2
2
  module Plugins
3
+ # The validation_helpers plugin contains instance method equivalents for most of the legacy
4
+ # class-level validations. The names and APIs are different, though. Example:
5
+ #
6
+ # class Album < Sequel::Model
7
+ # plugin :validation_helpers
8
+ # def validate
9
+ # validates_min_length 1, :num_tracks
10
+ # end
11
+ # end
12
+ #
13
+ # The validates_unique validation has a unique API, but the other validations have
14
+ # the API explained here:
15
+ #
16
+ # Arguments:
17
+ # * atts - Single attribute symbol or an array of attribute symbols specifying the
18
+ # attribute(s) to validate.
19
+ # Options:
20
+ # * :allow_blank - Whether to skip the validation if the value is blank. You should
21
+ # make sure all objects respond to blank if you use this option, which you can do by
22
+ # requiring 'sequel/extensions/blank'
23
+ # * :allow_missing - Whether to skip the validation if the attribute isn't a key in the
24
+ # values hash. This is different from allow_nil, because Sequel only sends the attributes
25
+ # in the values when doing an insert or update. If the attribute is not present, Sequel
26
+ # doesn't specify it, so the database will use the table's default value. This is different
27
+ # from having an attribute in values with a value of nil, which Sequel will send as NULL.
28
+ # If your database table has a non NULL default, this may be a good option to use. You
29
+ # don't want to use allow_nil, because if the attribute is in values but has a value nil,
30
+ # Sequel will attempt to insert a NULL value into the database, instead of using the
31
+ # database's default.
32
+ # * :allow_nil - Whether to skip the validation if the value is nil.
33
+ # * :message - The message to use
3
34
  module ValidationHelpers
4
- # ValidationHelpers contains instance method equivalents for most of the previous
5
- # default validations. The names and APIs have changed, though.
6
- #
7
- # The validates_unique validation has a unique API, but the other validations have
8
- # the API explained here:
9
- #
10
- # Arguments:
11
- # * atts - Single attribute symbol or an array of attribute symbols specifying the
12
- # attribute(s) to validate.
13
- # Options:
14
- # * :allow_blank - Whether to skip the validation if the value is blank. You should
15
- # make sure all objects respond to blank if you use this option, which you can do by
16
- # requiring 'sequel/extensions/blank'
17
- # * :allow_missing - Whether to skip the validation if the attribute isn't a key in the
18
- # values hash. This is different from allow_nil, because Sequel only sends the attributes
19
- # in the values when doing an insert or update. If the attribute is not present, Sequel
20
- # doesn't specify it, so the database will use the table's default value. This is different
21
- # from having an attribute in values with a value of nil, which Sequel will send as NULL.
22
- # If your database table has a non NULL default, this may be a good option to use. You
23
- # don't want to use allow_nil, because if the attribute is in values but has a value nil,
24
- # Sequel will attempt to insert a NULL value into the database, instead of using the
25
- # database's default.
26
- # * :allow_nil - Whether to skip the validation if the value is nil.
27
- # * :message - The message to use
28
35
  module InstanceMethods
29
36
  # Check that the attribute values are the given exact length.
30
37
  def validates_exact_length(exact, atts, opts={})
data/lib/sequel/sql.rb CHANGED
@@ -351,10 +351,22 @@ module Sequel
351
351
  end
352
352
  end
353
353
 
354
+ # Methods that create Subscripts (SQL array accesses).
355
+ module SubscriptMethods
356
+ # Return an SQL array subscript with the given arguments.
357
+ #
358
+ # :array.sql_subscript(1) # SQL: array[1]
359
+ # :array.sql_subscript(1, 2) # SQL: array[1, 2]
360
+ def sql_subscript(*sub)
361
+ Subscript.new(self, sub.flatten)
362
+ end
363
+ end
364
+
354
365
  class ComplexExpression
355
366
  include AliasMethods
356
367
  include CastMethods
357
368
  include OrderMethods
369
+ include SubscriptMethods
358
370
  end
359
371
 
360
372
  class GenericExpression
@@ -365,6 +377,7 @@ module Sequel
365
377
  include BooleanMethods
366
378
  include NumericMethods
367
379
  include StringMethods
380
+ include SubscriptMethods
368
381
  include InequalityMethods
369
382
  end
370
383
 
@@ -709,6 +722,9 @@ module Sequel
709
722
  # ruby array of all two pairs as an SQL array instead of an ordered
710
723
  # hash-like conditions specifier.
711
724
  class SQLArray < Expression
725
+ # The array of objects this SQLArray wraps
726
+ attr_reader :array
727
+
712
728
  # Create an object with the given array.
713
729
  def initialize(array)
714
730
  @array = array
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  MAJOR = 3
3
- MINOR = 0
3
+ MINOR = 1
4
4
  TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join('.')
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), 'spec_helper.rb')
2
2
 
3
3
  unless defined?(ADO_DB)
4
- ADO_DB = Sequel.ado(:host => 'MY_SQL_SERVER', :database => 'MyDB', :user => 'my_pwd', :password => 'my_usr')
4
+ ADO_DB = Sequel.ado(:host => 'MY_SQL_SERVER', :database => 'MyDB', :user => 'my_usr', :password => 'my_pwd')
5
5
  end
6
6
 
7
7
  context "An ADO dataset" do
@@ -14,6 +14,52 @@ context "An ADO dataset" do
14
14
  ADO_DB[:items].all
15
15
  }.should_not raise_error
16
16
  end
17
+
18
+ describe 'setting the :command_timeout option' do
19
+ before(:each) do
20
+ @conn_options = {:host => 'MY_SQL_SERVER',
21
+ :database => 'MyDB',
22
+ :user => 'my_usr',
23
+ :password => 'my_pwd',
24
+ :command_timeout => 120}
25
+ end
26
+
27
+ specify 'it should set the CommandTimeout parameter on the ADO handle' do
28
+ db = Sequel::ADO::Database.new(@conn_options)
29
+ db.connect(@conn_options).CommandTimeout.should == 120
30
+ end
31
+ end
32
+
33
+ describe 'when the :command_timeout option is not implicitly set' do
34
+ before(:each) do
35
+ @conn_options = {:host => 'MY_SQL_SERVER',
36
+ :database => 'MyDB',
37
+ :user => 'my_usr',
38
+ :password => 'my_pwd'}
39
+ end
40
+
41
+ specify 'it should remain as the default of 30 seconds' do
42
+ db = Sequel::ADO::Database.new(@conn_options)
43
+ db.connect(@conn_options).CommandTimeout.should == 30
44
+ end
45
+ end
46
+
47
+ describe 'setting the :provider option' do
48
+ before(:each) do
49
+ @conn_options = {:host => 'MY_SQL_SERVER',
50
+ :database => 'MyDB',
51
+ :user => 'my_usr',
52
+ :password => 'my_pwd',
53
+ :provider => "SQLOLEDB"}
54
+ end
55
+
56
+ specify 'it should set the CommandTimeout parameter on the ADO handle' do
57
+ db = Sequel::ADO::Database.new(@conn_options)
58
+ db.connect(@conn_options).Provider.should match /sqloledb/i
59
+ end
60
+ end
61
+
62
+
17
63
  end
18
64
 
19
65
  context "An MSSQL dataset" do