sequel 4.0.0 → 4.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 (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'