sequel 4.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/active_record.rdoc +2 -2
  4. data/doc/cheat_sheet.rdoc +0 -5
  5. data/doc/opening_databases.rdoc +3 -2
  6. data/doc/prepared_statements.rdoc +6 -0
  7. data/doc/release_notes/4.1.0.txt +85 -0
  8. data/doc/schema_modification.rdoc +9 -2
  9. data/lib/sequel/adapters/jdbc.rb +5 -0
  10. data/lib/sequel/adapters/mysql2.rb +24 -3
  11. data/lib/sequel/adapters/odbc.rb +6 -4
  12. data/lib/sequel/adapters/postgres.rb +25 -0
  13. data/lib/sequel/adapters/shared/mysql.rb +4 -29
  14. data/lib/sequel/adapters/shared/postgres.rb +14 -3
  15. data/lib/sequel/adapters/shared/sqlite.rb +4 -0
  16. data/lib/sequel/adapters/utils/replace.rb +36 -0
  17. data/lib/sequel/database/query.rb +1 -0
  18. data/lib/sequel/database/schema_generator.rb +12 -5
  19. data/lib/sequel/database/schema_methods.rb +2 -0
  20. data/lib/sequel/dataset/features.rb +5 -0
  21. data/lib/sequel/extensions/pg_json_ops.rb +0 -6
  22. data/lib/sequel/model/associations.rb +1 -1
  23. data/lib/sequel/plugins/instance_filters.rb +11 -1
  24. data/lib/sequel/plugins/pg_typecast_on_load.rb +3 -2
  25. data/lib/sequel/plugins/prepared_statements.rb +38 -9
  26. data/lib/sequel/plugins/update_primary_key.rb +10 -0
  27. data/lib/sequel/sql.rb +1 -1
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/mysql_spec.rb +1 -22
  30. data/spec/adapters/postgres_spec.rb +79 -2
  31. data/spec/core/database_spec.rb +10 -0
  32. data/spec/core/dataset_spec.rb +8 -3
  33. data/spec/core/expression_filters_spec.rb +1 -1
  34. data/spec/core/schema_spec.rb +17 -2
  35. data/spec/extensions/caching_spec.rb +2 -2
  36. data/spec/extensions/hook_class_methods_spec.rb +0 -4
  37. data/spec/extensions/instance_filters_spec.rb +22 -0
  38. data/spec/extensions/migration_spec.rb +5 -5
  39. data/spec/extensions/nested_attributes_spec.rb +4 -4
  40. data/spec/extensions/prepared_statements_spec.rb +37 -26
  41. data/spec/extensions/update_primary_key_spec.rb +13 -0
  42. data/spec/integration/dataset_test.rb +36 -0
  43. data/spec/model/associations_spec.rb +20 -2
  44. data/spec/model/hooks_spec.rb +1 -7
  45. metadata +5 -2
@@ -1,3 +1,5 @@
1
+ Sequel.require 'adapters/utils/replace'
2
+
1
3
  module Sequel
2
4
  module SQLite
3
5
  # No matter how you connect to SQLite, the following Database options
@@ -473,6 +475,8 @@ module Sequel
473
475
 
474
476
  # Instance methods for datasets that connect to an SQLite database
475
477
  module DatasetMethods
478
+ include Dataset::Replace
479
+
476
480
  SELECT_CLAUSE_METHODS = Dataset.clause_methods(:select, %w'select distinct columns from join where group having compounds order limit')
477
481
  CONSTANT_MAP = {:CURRENT_DATE=>"date(CURRENT_TIMESTAMP, 'localtime')".freeze, :CURRENT_TIMESTAMP=>"datetime(CURRENT_TIMESTAMP, 'localtime')".freeze, :CURRENT_TIME=>"time(CURRENT_TIMESTAMP, 'localtime')".freeze}
478
482
  EMULATED_FUNCTION_MAP = {:char_length=>'length'.freeze}
@@ -0,0 +1,36 @@
1
+ module Sequel
2
+ class Dataset
3
+ module Replace
4
+ INSERT = Dataset::INSERT
5
+ REPLACE = 'REPLACE'.freeze
6
+
7
+ # Execute a REPLACE statement on the database (deletes any duplicate
8
+ # rows before inserting).
9
+ def replace(*values)
10
+ execute_insert(replace_sql(*values))
11
+ end
12
+
13
+ # SQL statement for REPLACE
14
+ def replace_sql(*values)
15
+ clone(:replace=>true).insert_sql(*values)
16
+ end
17
+
18
+ # Replace multiple rows in a single query.
19
+ def multi_replace(*values)
20
+ clone(:replace=>true).multi_insert(*values)
21
+ end
22
+
23
+ # Databases using this module support REPLACE.
24
+ def supports_replace?
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ # If this is an replace instead of an insert, use replace instead
31
+ def insert_insert_sql(sql)
32
+ sql << (@opts[:replace] ? REPLACE : INSERT)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -72,6 +72,7 @@ module Sequel
72
72
  #
73
73
  # DB.run("SET some_server_variable = 42")
74
74
  def run(sql, opts=OPTS)
75
+ sql = literal(sql) if sql.is_a?(SQL::PlaceholderLiteralString)
75
76
  execute_ddl(sql, opts)
76
77
  nil
77
78
  end
@@ -116,12 +116,16 @@ module Sequel
116
116
  end
117
117
 
118
118
  # Adds a named constraint (or unnamed if name is nil) to the DDL,
119
- # with the given block or args.
119
+ # with the given block or args. To provide options for the constraint, pass
120
+ # a hash as the first argument.
120
121
  #
121
- # constraint(:blah, :num=>1..5) # CONSTRAINT blah CHECK num >= 1 AND num <= 5
122
- # check(:foo){num > 5} # CONSTRAINT foo CHECK num > 5
122
+ # constraint(:blah, :num=>1..5)
123
+ # # CONSTRAINT blah CHECK num >= 1 AND num <= 5
124
+ # constraint({:name=>:blah, :deferrable=>true}, :num=>1..5)
125
+ # # CONSTRAINT blah CHECK num >= 1 AND num <= 5 DEFERRABLE INITIALLY DEFERRED
123
126
  def constraint(name, *args, &block)
124
- constraints << {:name => name, :type => :check, :check => block || args}
127
+ opts = name.is_a?(Hash) ? name : {:name=>name}
128
+ constraints << opts.merge(:type=>:check, :check=>block || args)
125
129
  end
126
130
 
127
131
  # Add a foreign key in the table that references another table to the DDL. See column
@@ -319,8 +323,11 @@ module Sequel
319
323
  #
320
324
  # add_constraint(:valid_name, Sequel.like(:name, 'A%'))
321
325
  # # ADD CONSTRAINT valid_name CHECK (name LIKE 'A%')
326
+ # add_constraint({:name=>:valid_name, :deferrable=>true}, :num=>1..5)
327
+ # # CONSTRAINT valid_name CHECK (name LIKE 'A%') DEFERRABLE INITIALLY DEFERRED
322
328
  def add_constraint(name, *args, &block)
323
- @operations << {:op => :add_constraint, :name => name, :type => :check, :check => block || args}
329
+ opts = name.is_a?(Hash) ? name : {:name=>name}
330
+ @operations << opts.merge(:op=>:add_constraint, :type=>:check, :check=>block || args)
324
331
  end
325
332
 
326
333
  # Add a unique constraint to the given column(s)
@@ -156,6 +156,8 @@ module Sequel
156
156
  #
157
157
  # PostgreSQL specific options:
158
158
  # :unlogged :: Create the table as an unlogged table.
159
+ # :inherits :: Inherit from a different tables. An array can be
160
+ # specified to inherit from multiple tables.
159
161
  #
160
162
  # See <tt>Schema::Generator</tt> and the {"Schema Modification" guide}[link:files/doc/schema_modification_rdoc.html].
161
163
  def create_table(name, options=OPTS, &block)
@@ -117,6 +117,11 @@ module Sequel
117
117
  false
118
118
  end
119
119
 
120
+ # Whether the dataset supports REPLACE syntax, false by default.
121
+ def supports_replace?
122
+ false
123
+ end
124
+
120
125
  # Whether the RETURNING clause is supported for the given type of query.
121
126
  # +type+ can be :insert, :update, or :delete.
122
127
  def supports_returning?(type)
@@ -184,12 +184,6 @@ module Sequel
184
184
  a.is_a?(Array) || (defined?(PGArray) && a.is_a?(PGArray)) || (defined?(ArrayOp) && a.is_a?(ArrayOp))
185
185
  end
186
186
 
187
- # Return a placeholder literal with the given str and args, wrapped
188
- # in an SQL::StringExpression, used by operators that return text.
189
- def text_op(str, args)
190
- Sequel::SQL::StringExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(str, [self, args]))
191
- end
192
-
193
187
  # Automatically wrap argument in a PGArray if it is a plain Array.
194
188
  # Requires that the pg_array extension has been loaded to work.
195
189
  def wrap_array(arg)
@@ -1512,7 +1512,7 @@ module Sequel
1512
1512
  if o.is_a?(Hash)
1513
1513
  o = klass.new(o)
1514
1514
  elsif o.is_a?(Integer) || o.is_a?(String) || o.is_a?(Array)
1515
- o = klass[o]
1515
+ o = klass.with_pk!(o)
1516
1516
  elsif !o.is_a?(klass)
1517
1517
  raise(Sequel::Error, "associated object #{o.inspect} not of correct type #{klass}")
1518
1518
  end
@@ -22,7 +22,7 @@ module Sequel
22
22
  #
23
23
  # # Attempting to delete the object where the filter doesn't
24
24
  # # match any rows raises an error.
25
- # i1.delete # raises Sequel::Error
25
+ # i1.delete # raises Sequel::NoExistingObject
26
26
  #
27
27
  # # The other object that represents the same row has no
28
28
  # # instance filters, and can be updated normally.
@@ -108,6 +108,16 @@ module Sequel
108
108
  def _update_dataset
109
109
  apply_instance_filters(super)
110
110
  end
111
+
112
+ # Only use prepared statements for update and delete queries
113
+ # if there are no instance filters.
114
+ def use_prepared_statements_for?(type)
115
+ if (type == :update || type == :delete) && !instance_filters.empty?
116
+ false
117
+ else
118
+ super
119
+ end
120
+ end
111
121
  end
112
122
  end
113
123
  end
@@ -21,8 +21,9 @@ module Sequel
21
21
  # Album.add_pg_typecast_on_load_columns :aliases, :config
22
22
  #
23
23
  # This plugin only handles values that the adapter returns as strings. If
24
- # the adapter returns a value other than a string for a column, that value
25
- # will be used directly without typecasting.
24
+ # the adapter returns a value other than a string, this plugin will have no
25
+ # effect. You may be able to use the regular typecast_on_load plugin to
26
+ # handle those cases.
26
27
  module PgTypecastOnLoad
27
28
  # Call add_pg_typecast_on_load_columns on the passed column arguments.
28
29
  def self.configure(model, *columns)
@@ -1,4 +1,16 @@
1
1
  module Sequel
2
+ class Model
3
+ module InstanceMethods
4
+ # Whether prepared statements should be used for the given type of query
5
+ # (:insert, :insert_select, :refresh, :update, or :delete). True by default,
6
+ # can be overridden in other plugins to disallow prepared statements for
7
+ # specific types of queries.
8
+ def use_prepared_statements_for?(type)
9
+ true
10
+ end
11
+ end
12
+ end
13
+
2
14
  module Plugins
3
15
  # The prepared_statements plugin modifies the model to use prepared statements for
4
16
  # instance level deletes and saves, as well as class level lookups by
@@ -11,9 +23,6 @@ module Sequel
11
23
  # of prepared statements that can be created, unless you tightly control how your
12
24
  # model instances are saved.
13
25
  #
14
- # This plugin does not work correctly with the instance filters plugin
15
- # or the update_primary_key plugin.
16
- #
17
26
  # Usage:
18
27
  #
19
28
  # # Make all model subclasses use prepared statements (called before loading subclasses)
@@ -133,30 +142,50 @@ module Sequel
133
142
 
134
143
  # Use a prepared statement to delete the row.
135
144
  def _delete_without_checking
136
- model.send(:prepared_delete).call(pk_hash)
145
+ if use_prepared_statements_for?(:delete)
146
+ model.send(:prepared_delete).call(pk_hash)
147
+ else
148
+ super
149
+ end
137
150
  end
138
151
 
139
152
  # Use a prepared statement to insert the values into the model's dataset.
140
153
  def _insert_raw(ds)
141
- model.send(:prepared_insert, @values.keys).call(@values)
154
+ if use_prepared_statements_for?(:insert)
155
+ model.send(:prepared_insert, @values.keys).call(@values)
156
+ else
157
+ super
158
+ end
142
159
  end
143
160
 
144
161
  # Use a prepared statement to insert the values into the model's dataset
145
162
  # and return the new column values.
146
163
  def _insert_select_raw(ds)
147
- if ps = model.send(:prepared_insert_select, @values.keys)
148
- ps.call(@values)
164
+ if use_prepared_statements_for?(:insert_select)
165
+ if ps = model.send(:prepared_insert_select, @values.keys)
166
+ ps.call(@values)
167
+ end
168
+ else
169
+ super
149
170
  end
150
171
  end
151
172
 
152
173
  # Use a prepared statement to refresh this model's column values.
153
174
  def _refresh_get(ds)
154
- model.send(:prepared_refresh).call(pk_hash)
175
+ if use_prepared_statements_for?(:refresh)
176
+ model.send(:prepared_refresh).call(pk_hash)
177
+ else
178
+ super
179
+ end
155
180
  end
156
181
 
157
182
  # Use a prepared statement to update this model's columns in the database.
158
183
  def _update_without_checking(columns)
159
- model.send(:prepared_update, columns.keys).call(columns.merge(pk_hash))
184
+ if use_prepared_statements_for?(:update)
185
+ model.send(:prepared_update, columns.keys).call(columns.merge(pk_hash))
186
+ else
187
+ super
188
+ end
160
189
  end
161
190
  end
162
191
  end
@@ -54,6 +54,16 @@ module Sequel
54
54
  associations.delete(k) if model.association_reflection(k)[:type] != :many_to_one
55
55
  end
56
56
  end
57
+
58
+ # Do not use prepared statements for update queries, since they don't work
59
+ # in the case where the primary key has changed.
60
+ def use_prepared_statements_for?(type)
61
+ if type == :update
62
+ false
63
+ else
64
+ super
65
+ end
66
+ end
57
67
  end
58
68
  end
59
69
  end
data/lib/sequel/sql.rb CHANGED
@@ -1624,7 +1624,7 @@ module Sequel
1624
1624
  fun_args = ::Kernel.Array(opts[:*] ? WILDCARD : opts[:args])
1625
1625
  WindowFunction.new(Function.new(m, *fun_args), Window.new(opts))
1626
1626
  else
1627
- raise Error, 'unsupported VirtualRow method argument used with block'
1627
+ Kernel.raise(Error, 'unsupported VirtualRow method argument used with block')
1628
1628
  end
1629
1629
  end
1630
1630
  elsif args.empty?
@@ -3,7 +3,7 @@ module Sequel
3
3
  MAJOR = 4
4
4
  # The minor version of Sequel. Bumped for every non-patch level
5
5
  # release, generally around once a month.
6
- MINOR = 0
6
+ MINOR = 1
7
7
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
8
8
  # releases that fix regressions from previous versions.
9
9
  TINY = 0
@@ -1070,27 +1070,6 @@ describe "MySQL::Dataset#replace" do
1070
1070
  @d.replace({})
1071
1071
  @d.all.should == [{:id=>1, :value=>2}]
1072
1072
  end
1073
-
1074
- specify "should use support arrays, datasets, and multiple values" do
1075
- @d.replace([1, 2])
1076
- @d.all.should == [{:id=>1, :value=>2}]
1077
- @d.replace(1, 2)
1078
- @d.all.should == [{:id=>1, :value=>2}]
1079
- @d.replace(@d)
1080
- @d.all.should == [{:id=>1, :value=>2}]
1081
- end
1082
-
1083
- specify "should create a record if the condition is not met" do
1084
- @d.replace(:id => 111, :value => 333)
1085
- @d.all.should == [{:id => 111, :value => 333}]
1086
- end
1087
-
1088
- specify "should update a record if the condition is met" do
1089
- @d << {:id => 111}
1090
- @d.all.should == [{:id => 111, :value => nil}]
1091
- @d.replace(:id => 111, :value => 333)
1092
- @d.all.should == [{:id => 111, :value => 333}]
1093
- end
1094
1073
  end
1095
1074
 
1096
1075
  describe "MySQL::Dataset#complex_expression_sql" do
@@ -1303,5 +1282,5 @@ if DB.adapter_scheme == :mysql2
1303
1282
  specify "should correctly handle early returning when streaming results" do
1304
1283
  3.times{@ds.each{|r| break r[:a]}.should == 0}
1305
1284
  end
1306
- end if false
1285
+ end
1307
1286
  end
@@ -17,8 +17,7 @@ describe "PostgreSQL", '#create_table' do
17
17
  DB.sqls.clear
18
18
  end
19
19
  after do
20
- @db.drop_table?(:tmp_dolls)
21
- @db.drop_table?(:unlogged_dolls)
20
+ @db.drop_table?(:tmp_dolls, :unlogged_dolls)
22
21
  end
23
22
 
24
23
  specify "should create a temporary table" do
@@ -35,6 +34,27 @@ describe "PostgreSQL", '#create_table' do
35
34
  end
36
35
  end
37
36
 
37
+ specify "should create a table inheriting from another table" do
38
+ @db.create_table(:unlogged_dolls){text :name}
39
+ @db.create_table(:tmp_dolls, :inherits=>:unlogged_dolls){}
40
+ @db[:tmp_dolls].insert('a')
41
+ @db[:unlogged_dolls].all.should == [{:name=>'a'}]
42
+ end
43
+
44
+ specify "should create a table inheriting from multiple tables" do
45
+ begin
46
+ @db.create_table(:unlogged_dolls){text :name}
47
+ @db.create_table(:tmp_dolls){text :bar}
48
+ @db.create_table!(:items, :inherits=>[:unlogged_dolls, :tmp_dolls]){text :foo}
49
+ @db[:items].insert(:name=>'a', :bar=>'b', :foo=>'c')
50
+ @db[:unlogged_dolls].all.should == [{:name=>'a'}]
51
+ @db[:tmp_dolls].all.should == [{:bar=>'b'}]
52
+ @db[:items].all.should == [{:name=>'a', :bar=>'b', :foo=>'c'}]
53
+ ensure
54
+ @db.drop_table?(:items)
55
+ end
56
+ end
57
+
38
58
  specify "should not allow to pass both :temp and :unlogged" do
39
59
  proc do
40
60
  @db.create_table(:temp_unlogged_dolls, :temp => true, :unlogged => true){text :name}
@@ -232,6 +252,43 @@ describe "A PostgreSQL dataset" do
232
252
  proc{@db[:atest].insert(2)}.should raise_error(Sequel::Postgres::ExclusionConstraintViolation)
233
253
  @db.alter_table(:atest){drop_constraint 'atest_ex'}
234
254
  end if DB.server_version >= 90000
255
+
256
+ specify "should support deferrable exclusion constraints" do
257
+ @db.create_table!(:atest){Integer :t; exclude [[Sequel.desc(:t, :nulls=>:last), '=']], :using=>:btree, :where=>proc{t > 0}, :deferrable => true}
258
+ proc do
259
+ @db.transaction do
260
+ @db[:atest].insert(2)
261
+ proc{@db[:atest].insert(2)}.should_not raise_error
262
+ end
263
+ end.should raise_error(Sequel::Postgres::ExclusionConstraintViolation)
264
+ end if DB.server_version >= 90000
265
+
266
+ specify "should support Database#error_info for getting info hash on the given error" do
267
+ @db.create_table!(:atest){Integer :t; Integer :t2, :null=>false, :default=>1; constraint :f, :t=>0}
268
+ begin
269
+ @db[:atest].insert(1)
270
+ rescue => e
271
+ end
272
+ e.should_not be_nil
273
+ info = @db.error_info(e)
274
+ info[:schema].should == 'public'
275
+ info[:table].should == 'atest'
276
+ info[:constraint].should == 'f'
277
+ info[:column].should be_nil
278
+ info[:type].should be_nil
279
+
280
+ begin
281
+ @db[:atest].insert(0, nil)
282
+ rescue => e
283
+ end
284
+ e.should_not be_nil
285
+ info = @db.error_info(e.wrapped_exception)
286
+ info[:schema].should == 'public'
287
+ info[:table].should == 'atest'
288
+ info[:constraint].should be_nil
289
+ info[:column].should == 't2'
290
+ info[:type].should be_nil
291
+ end if DB.server_version >= 90300 && DB.adapter_scheme == :postgres && SEQUEL_POSTGRES_USES_PG && Object.const_defined?(:PG) && ::PG.const_defined?(:Constants) && ::PG::Constants.const_defined?(:PG_DIAG_SCHEMA_NAME)
235
292
 
236
293
  specify "should support Database#do for executing anonymous code blocks" do
237
294
  @db.drop_table?(:btest)
@@ -255,6 +312,19 @@ describe "A PostgreSQL dataset" do
255
312
  proc{@db.alter_table(:atest){validate_constraint :atest_fk}}.should_not raise_error
256
313
  end if DB.server_version >= 90200
257
314
 
315
+ specify "should support adding check constarints that are not yet valid, and validating them later" do
316
+ @db.create_table!(:atest){Integer :a}
317
+ @db[:atest].insert(5)
318
+ @db.alter_table(:atest){add_constraint({:name=>:atest_check, :not_valid=>true}){a >= 10}}
319
+ @db[:atest].insert(10)
320
+ proc{@db[:atest].insert(6)}.should raise_error(Sequel::DatabaseError)
321
+
322
+ proc{@db.alter_table(:atest){validate_constraint :atest_check}}.should raise_error(Sequel::DatabaseError)
323
+ @db[:atest].where{a < 10}.update(:a=>Sequel.+(:a, 10))
324
+ @db.alter_table(:atest){validate_constraint :atest_check}
325
+ proc{@db.alter_table(:atest){validate_constraint :atest_check}}.should_not raise_error
326
+ end if DB.server_version >= 90200
327
+
258
328
  specify "should support :using when altering a column's type" do
259
329
  @db.create_table!(:atest){Integer :t}
260
330
  @db[:atest].insert(1262304000)
@@ -927,6 +997,13 @@ describe "Postgres::Database schema qualified tables" do
927
997
  @db.table_exists?(:schema_test__schema_test).should == true
928
998
  end
929
999
 
1000
+ specify "should be able to add and drop indexes in a schema" do
1001
+ @db.create_table(:schema_test__schema_test){Integer :i, :index=>true}
1002
+ @db.indexes(:schema_test__schema_test).keys.should == [:schema_test_schema_test_i_index]
1003
+ @db.drop_index :schema_test__schema_test, :i
1004
+ @db.indexes(:schema_test__schema_test).keys.should == []
1005
+ end
1006
+
930
1007
  specify "should be able to get primary keys for tables in a given schema" do
931
1008
  @db.create_table(:schema_test__schema_test){primary_key :i}
932
1009
  @db.primary_key(:schema_test__schema_test).should == 'i'