sequel 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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