sequel 2.8.0 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/CHANGELOG +34 -0
  2. data/COPYING +1 -1
  3. data/Rakefile +1 -1
  4. data/bin/sequel +12 -5
  5. data/doc/advanced_associations.rdoc +17 -3
  6. data/lib/sequel_core/adapters/informix.rb +1 -1
  7. data/lib/sequel_core/adapters/postgres.rb +12 -2
  8. data/lib/sequel_core/adapters/shared/mssql.rb +3 -1
  9. data/lib/sequel_core/adapters/shared/mysql.rb +14 -0
  10. data/lib/sequel_core/adapters/shared/oracle.rb +7 -6
  11. data/lib/sequel_core/adapters/shared/postgres.rb +170 -3
  12. data/lib/sequel_core/adapters/shared/progress.rb +1 -1
  13. data/lib/sequel_core/adapters/shared/sqlite.rb +11 -6
  14. data/lib/sequel_core/adapters/sqlite.rb +8 -0
  15. data/lib/sequel_core/dataset/sql.rb +23 -19
  16. data/lib/sequel_core/dataset/unsupported.rb +12 -0
  17. data/lib/sequel_core/schema/sql.rb +3 -1
  18. data/lib/sequel_model.rb +1 -1
  19. data/lib/sequel_model/associations.rb +1 -1
  20. data/lib/sequel_model/base.rb +2 -1
  21. data/lib/sequel_model/dataset_methods.rb +1 -1
  22. data/lib/sequel_model/eager_loading.rb +1 -1
  23. data/lib/sequel_model/exceptions.rb +7 -0
  24. data/lib/sequel_model/record.rb +22 -13
  25. data/lib/sequel_model/validations.rb +8 -4
  26. data/spec/adapters/mysql_spec.rb +17 -0
  27. data/spec/adapters/postgres_spec.rb +69 -0
  28. data/spec/adapters/sqlite_spec.rb +38 -3
  29. data/spec/integration/dataset_test.rb +51 -0
  30. data/spec/integration/schema_test.rb +4 -0
  31. data/spec/sequel_core/core_ext_spec.rb +2 -2
  32. data/spec/sequel_core/dataset_spec.rb +35 -1
  33. data/spec/sequel_core/schema_spec.rb +7 -0
  34. data/spec/sequel_model/association_reflection_spec.rb +13 -13
  35. data/spec/sequel_model/hooks_spec.rb +9 -5
  36. data/spec/sequel_model/validations_spec.rb +1 -1
  37. metadata +3 -2
@@ -12,7 +12,7 @@ module Sequel
12
12
  module DatasetMethods
13
13
  include Dataset::UnsupportedIntersectExcept
14
14
 
15
- SELECT_CLAUSE_ORDER = %w'limit distinct columns from join where group order having union'.freeze
15
+ SELECT_CLAUSE_ORDER = %w'limit distinct columns from join where group order having compounds'.freeze
16
16
 
17
17
  private
18
18
 
@@ -18,16 +18,15 @@ module Sequel
18
18
  # the column inside of a transaction.
19
19
  def alter_table_sql(table, op)
20
20
  case op[:op]
21
- when :add_column
21
+ when :add_column, :add_index, :drop_index
22
22
  super
23
- when :add_index
24
- index_definition_sql(table, op)
25
23
  when :drop_column
26
24
  columns_str = (schema_parse_table(table, {}).map{|c| c[0]} - Array(op[:name])).join(",")
27
- ["CREATE TEMPORARY TABLE #{table}_backup(#{columns_str})",
25
+ defined_columns_str = column_list_sql parse_pragma(table, {}).reject{ |c| c[:name] == op[:name].to_s}
26
+ ["CREATE TEMPORARY TABLE #{table}_backup(#{defined_columns_str})",
28
27
  "INSERT INTO #{table}_backup SELECT #{columns_str} FROM #{table}",
29
28
  "DROP TABLE #{table}",
30
- "CREATE TABLE #{table}(#{columns_str})",
29
+ "CREATE TABLE #{table}(#{defined_columns_str})",
31
30
  "INSERT INTO #{table} SELECT #{columns_str} FROM #{table}_backup",
32
31
  "DROP TABLE #{table}_backup"]
33
32
  else
@@ -94,6 +93,12 @@ module Sequel
94
93
  # SQLite supports schema parsing using the table_info PRAGMA, so
95
94
  # parse the output of that into the format Sequel expects.
96
95
  def schema_parse_table(table_name, opts)
96
+ parse_pragma(table_name, opts).map do |row|
97
+ [row.delete(:name).to_sym, row]
98
+ end
99
+ end
100
+
101
+ def parse_pragma(table_name, opts)
97
102
  self["PRAGMA table_info(?)", table_name].map do |row|
98
103
  row.delete(:cid)
99
104
  row[:allow_null] = row.delete(:notnull).to_i == 0
@@ -102,7 +107,7 @@ module Sequel
102
107
  row[:default] = nil if row[:default].blank?
103
108
  row[:db_type] = row.delete(:type)
104
109
  row[:type] = schema_column_type(row[:db_type])
105
- [row.delete(:name).to_sym, row]
110
+ row
106
111
  end
107
112
  end
108
113
  end
@@ -31,6 +31,7 @@ module Sequel
31
31
  db = ::SQLite3::Database.new(opts[:database])
32
32
  db.busy_timeout(opts.fetch(:timeout, 5000))
33
33
  db.type_translation = true
34
+
34
35
  # Handle datetime's with Sequel.datetime_class
35
36
  prok = proc do |t,v|
36
37
  v = Time.at(v.to_i).iso8601 if UNIX_EPOCH_TIME_FORMAT.match(v)
@@ -38,6 +39,13 @@ module Sequel
38
39
  end
39
40
  db.translator.add_translator("timestamp", &prok)
40
41
  db.translator.add_translator("datetime", &prok)
42
+
43
+ # Handle numeric values with BigDecimal
44
+ prok = proc{|t,v| BigDecimal.new(v) rescue v}
45
+ db.translator.add_translator("numeric", &prok)
46
+ db.translator.add_translator("decimal", &prok)
47
+ db.translator.add_translator("money", &prok)
48
+
41
49
  db
42
50
  end
43
51
 
@@ -6,13 +6,13 @@ module Sequel
6
6
  COLUMN_REF_RE1 = /\A([\w ]+)__([\w ]+)___([\w ]+)\z/.freeze
7
7
  COLUMN_REF_RE2 = /\A([\w ]+)___([\w ]+)\z/.freeze
8
8
  COLUMN_REF_RE3 = /\A([\w ]+)__([\w ]+)\z/.freeze
9
- COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit]
9
+ COUNT_FROM_SELF_OPTS = [:distinct, :group, :sql, :limit, :compounds]
10
10
  DATE_FORMAT = "DATE '%Y-%m-%d'".freeze
11
11
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
12
12
  NULL = "NULL".freeze
13
13
  QUESTION_MARK = '?'.freeze
14
14
  STOCK_COUNT_OPTS = {:select => ["COUNT(*)".lit], :order => nil}.freeze
15
- SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having intersect union except order limit'.freeze
15
+ SELECT_CLAUSE_ORDER = %w'distinct columns from join where group having compounds order limit'.freeze
16
16
  TIMESTAMP_FORMAT = "TIMESTAMP '%Y-%m-%d %H:%M:%S'".freeze
17
17
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
18
18
  WILDCARD = '*'.freeze
@@ -104,7 +104,7 @@ module Sequel
104
104
  # DB[:items].except(DB[:other_items]).sql
105
105
  # #=> "SELECT * FROM items EXCEPT SELECT * FROM other_items"
106
106
  def except(dataset, all = false)
107
- clone(:except => dataset, :except_all => all)
107
+ compound_clone(:except, dataset, all)
108
108
  end
109
109
 
110
110
  # Performs the inverse of Dataset#filter.
@@ -316,7 +316,7 @@ module Sequel
316
316
  # DB[:items].intersect(DB[:other_items]).sql
317
317
  # #=> "SELECT * FROM items INTERSECT SELECT * FROM other_items"
318
318
  def intersect(dataset, all = false)
319
- clone(:intersect => dataset, :intersect_all => all)
319
+ compound_clone(:intersect, dataset, all)
320
320
  end
321
321
 
322
322
  # Inverts the current filter
@@ -471,7 +471,9 @@ module Sequel
471
471
  when Integer, Float
472
472
  v.to_s
473
473
  when BigDecimal
474
- v.to_s("F")
474
+ d = v.to_s("F")
475
+ d = "'#{d}'" if v.nan? || v.infinite?
476
+ d
475
477
  when NilClass
476
478
  NULL
477
479
  when TrueClass
@@ -680,7 +682,7 @@ module Sequel
680
682
  # DB[:items].union(DB[:other_items]).sql
681
683
  # #=> "SELECT * FROM items UNION SELECT * FROM other_items"
682
684
  def union(dataset, all = false)
683
- clone(:union => dataset, :union_all => all)
685
+ compound_clone(:union, dataset, all)
684
686
  end
685
687
 
686
688
  # Returns a copy of the dataset with the distinct option.
@@ -769,6 +771,11 @@ module Sequel
769
771
  end
770
772
  end
771
773
 
774
+ # Add the dataset to the list of compounds
775
+ def compound_clone(type, dataset, all)
776
+ clone(:compounds=>Array(@opts[:compounds]).map{|x| x.dup} + [[type, dataset, all]])
777
+ end
778
+
772
779
  # Converts an array of expressions into a comma separated string of
773
780
  # expressions.
774
781
  def expression_list(columns)
@@ -880,9 +887,16 @@ module Sequel
880
887
  end
881
888
  end
882
889
 
883
- # Modify the sql to add a dataset to the EXCEPT clause
884
- def select_except_sql(sql, opts)
885
- sql << " EXCEPT#{' ALL' if opts[:except_all]} #{opts[:except].sql}" if opts[:except]
890
+ # Modify the sql to add a dataset to the via an EXCEPT, INTERSECT, or UNION clause.
891
+ # This uses a subselect for the compound datasets used, because using parantheses doesn't
892
+ # work on all databases. I consider this an ugly hack, but can't I think of a better default.
893
+ def select_compounds_sql(sql, opts)
894
+ return unless opts[:compounds]
895
+ opts[:compounds].each do |type, dataset, all|
896
+ compound_sql = subselect_sql(dataset)
897
+ compound_sql = "SELECT * FROM (#{compound_sql})" if dataset.opts[:compounds]
898
+ sql.replace("#{sql} #{type.to_s.upcase}#{' ALL' if all} #{compound_sql}")
899
+ end
886
900
  end
887
901
 
888
902
  # Modify the sql to add the list of tables to select FROM
@@ -900,11 +914,6 @@ module Sequel
900
914
  sql << " HAVING #{literal(opts[:having])}" if opts[:having]
901
915
  end
902
916
 
903
- # Modify the sql to add a dataset to the INTERSECT clause
904
- def select_intersect_sql(sql, opts)
905
- sql << " INTERSECT#{' ALL' if opts[:intersect_all]} #{opts[:intersect].sql}" if opts[:intersect]
906
- end
907
-
908
917
  # Modify the sql to add the list of tables to JOIN to
909
918
  def select_join_sql(sql, opts)
910
919
  opts[:join].each{|j| sql << literal(j)} if opts[:join]
@@ -921,11 +930,6 @@ module Sequel
921
930
  sql << " ORDER BY #{expression_list(opts[:order])}" if opts[:order]
922
931
  end
923
932
 
924
- # Modify the sql to add a dataset to the UNION clause
925
- def select_union_sql(sql, opts)
926
- sql << " UNION#{' ALL' if opts[:union_all]} #{opts[:union].sql}" if opts[:union]
927
- end
928
-
929
933
  # Modify the sql to add the filter criteria in the WHERE clause
930
934
  def select_where_sql(sql, opts)
931
935
  sql << " WHERE #{literal(opts[:where])}" if opts[:where]
@@ -11,6 +11,18 @@ class Sequel::Dataset
11
11
  def intersect(ds, all=false)
12
12
  raise(Sequel::Error, "INTERSECT not supported")
13
13
  end
14
+
15
+ private
16
+
17
+ # Since EXCEPT and INTERSECT are not supported, and order shouldn't matter
18
+ # when UNION is used, don't worry about parantheses. This may potentially
19
+ # give incorrect results if UNION ALL is used.
20
+ def select_compounds_sql(sql, opts)
21
+ return unless opts[:compounds]
22
+ opts[:compounds].each do |type, dataset, all|
23
+ sql << " #{type.to_s.upcase}#{' ALL' if all} #{subselect_sql(dataset)}"
24
+ end
25
+ end
14
26
  end
15
27
 
16
28
  # This module should be included in the dataset class for all databases that
@@ -243,7 +243,9 @@ module Sequel
243
243
 
244
244
  if table_name
245
245
  if respond_to?(:schema_parse_table, true)
246
- @schemas[quoted_name] = schema_parse_table(table_name, opts)
246
+ cols = schema_parse_table(table_name, opts)
247
+ raise(Error, 'schema parsing returned no columns, table probably doesn\'t exist') if cols.blank?
248
+ @schemas[quoted_name] = cols
247
249
  else
248
250
  raise Error, 'schema parsing is not implemented on this database'
249
251
  end
data/lib/sequel_model.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'sequel_core'
2
2
  %w"inflector base hooks record schema association_reflection dataset_methods
3
- associations caching plugins validations eager_loading".each do |f|
3
+ associations caching plugins validations eager_loading exceptions".each do |f|
4
4
  require "sequel_model/#{f}"
5
5
  end
6
6
 
@@ -388,7 +388,7 @@ module Sequel::Model::Associations
388
388
  association_module_private_def(opts._setter_method){|o| send(:"#{key}=", (o.send(opts.primary_key) if o))}
389
389
 
390
390
  association_module_def(opts.setter_method) do |o|
391
- raise(Sequel::Error, 'model object does not have a primary key') if o && !o.pk
391
+ raise(Sequel::Error, "model object #{model} does not have a primary key") if o && !o.pk
392
392
  old_val = send(opts.association_method)
393
393
  return o if old_val == o
394
394
  return if old_val and run_association_callbacks(opts, :before_remove, old_val) == false
@@ -208,6 +208,7 @@ module Sequel
208
208
  subclass.instance_variable_set(iv, sup_class_value)
209
209
  end
210
210
  unless ivs.include?("@dataset")
211
+ db
211
212
  begin
212
213
  if sup_class == Model
213
214
  subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.blank?
@@ -491,7 +492,7 @@ module Sequel
491
492
 
492
493
  # Module that the class includes that holds methods the class adds for column accessors and
493
494
  # associations so that the methods can be overridden with super
494
- def self.overridable_methods_module
495
+ def self.overridable_methods_module # :nodoc:
495
496
  include(@overridable_methods_module = Module.new) unless @overridable_methods_module
496
497
  @overridable_methods_module
497
498
  end
@@ -8,7 +8,7 @@ module Sequel::Model::DatasetMethods
8
8
  def destroy
9
9
  raise(Error, "No model associated with this dataset") unless @opts[:models]
10
10
  count = 0
11
- @db.transaction {each {|r| count += 1; r.destroy}}
11
+ @db.transaction{all{|r| count += 1; r.destroy}}
12
12
  count
13
13
  end
14
14
 
@@ -55,7 +55,7 @@ module Sequel::Model::Associations::EagerLoading
55
55
  # need to filter based on columns in associated tables, look at #eager_graph
56
56
  # or join the tables you need to filter on manually.
57
57
  #
58
- # Each association's order, if definied, is respected. Eager also works
58
+ # Each association's order, if defined, is respected. Eager also works
59
59
  # on a limited dataset, but does not use any :limit options for associations.
60
60
  # If the association uses a block or has an :eager_block argument, it is used.
61
61
  def eager(*associations)
@@ -0,0 +1,7 @@
1
+ module Sequel
2
+ # This exception will be raised when raise_on_save_failure is set and a validation fails
3
+ class ValidationFailed < Error;end
4
+
5
+ # This exception will be raised when raise_on_save_failure is set and a before hook fails
6
+ class BeforeHookFailed < Error;end
7
+ end
@@ -135,7 +135,7 @@ module Sequel
135
135
  # Returns a string representation of the model instance including
136
136
  # the class name and values.
137
137
  def inspect
138
- "#<#{model.name} @values=#{@values.inspect}>"
138
+ "#<#{model.name} @values=#{inspect_values}>"
139
139
  end
140
140
 
141
141
  # Returns attribute names as an array of symbols.
@@ -187,8 +187,7 @@ module Sequel
187
187
  # Otherwise, returns self. You can provide an optional list of
188
188
  # columns to update, in which case it only updates those columns.
189
189
  def save(*columns)
190
- return save_failure(:save) unless valid?
191
- save!(*columns)
190
+ valid? ? save!(*columns) : save_failure(:invalid)
192
191
  end
193
192
 
194
193
  # Creates or updates the record, without attempting to validate
@@ -342,7 +341,7 @@ module Sequel
342
341
 
343
342
  # Backbone behind association_dataset
344
343
  def _dataset(opts)
345
- raise(Sequel::Error, 'model object does not have a primary key') if opts.dataset_need_primary_key? && !pk
344
+ raise(Sequel::Error, "model object #{model} does not have a primary key") if opts.dataset_need_primary_key? && !pk
346
345
  ds = send(opts._dataset_method)
347
346
  opts[:extend].each{|m| ds.extend(m)}
348
347
  ds = ds.select(*opts.select) if opts.select
@@ -356,8 +355,8 @@ module Sequel
356
355
 
357
356
  # Add the given associated object to the given association
358
357
  def add_associated_object(opts, o)
359
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
360
- raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
358
+ raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
359
+ raise(Sequel::Error, "associated object #{o.model} does not have a primary key") if opts.need_associated_primary_key? && !o.pk
361
360
  return if run_association_callbacks(opts, :before_add, o) == false
362
361
  send(opts._add_method, o)
363
362
  associations[opts[:name]].push(o) if associations.include?(opts[:name])
@@ -378,6 +377,11 @@ module Sequel
378
377
  end
379
378
  end
380
379
 
380
+ # Default inspection output for a record, overwrite to change the way #inspect prints the @values hash
381
+ def inspect_values
382
+ @values.inspect
383
+ end
384
+
381
385
  # Load the associated objects using the dataset
382
386
  def load_associated_objects(opts, reload=false)
383
387
  name = opts[:name]
@@ -402,7 +406,7 @@ module Sequel
402
406
 
403
407
  # Remove all associated objects from the given association
404
408
  def remove_all_associated_objects(opts)
405
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
409
+ raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
406
410
  send(opts._remove_all_method)
407
411
  ret = associations[opts[:name]].each{|o| remove_reciprocal_object(opts, o)} if associations.include?(opts[:name])
408
412
  associations[opts[:name]] = []
@@ -411,8 +415,8 @@ module Sequel
411
415
 
412
416
  # Remove the given associated object from the given association
413
417
  def remove_associated_object(opts, o)
414
- raise(Sequel::Error, 'model object does not have a primary key') unless pk
415
- raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
418
+ raise(Sequel::Error, "model object #{model} does not have a primary key") unless pk
419
+ raise(Sequel::Error, "associated object #{o.model} does not have a primary key") if opts.need_associated_primary_key? && !o.pk
416
420
  return if run_association_callbacks(opts, :before_remove, o) == false
417
421
  send(opts._remove_method, o)
418
422
  associations[opts[:name]].delete_if{|x| o === x} if associations.include?(opts[:name])
@@ -447,16 +451,21 @@ module Sequel
447
451
  raise Error, "callbacks should either be Procs or Symbols"
448
452
  end
449
453
  if res == false and stop_on_false
450
- save_failure("modify association for", raise_error)
454
+ raise(BeforeHookFailed, "Unable to modify association for record: one of the #{callback_type} hooks returned false") if raise_error
451
455
  return false
452
456
  end
453
457
  end
454
458
  end
455
459
 
456
460
  # Raise an error if raise_on_save_failure is true
457
- def save_failure(action, raise_error = nil)
458
- raise_error = raise_on_save_failure if raise_error.nil?
459
- raise(Error, "unable to #{action} record") if raise_error
461
+ def save_failure(type)
462
+ if raise_on_save_failure
463
+ if type == :invalid
464
+ raise ValidationFailed, errors.full_messages.join(', ')
465
+ else
466
+ raise BeforeHookFailed, "one of the before_#{type} hooks returned false"
467
+ end
468
+ end
460
469
  end
461
470
 
462
471
  # Set the columns, filtered by the only and except arrays.
@@ -384,10 +384,14 @@ module Sequel
384
384
  # Validates the object.
385
385
  def validate
386
386
  errors.clear
387
- return false if before_validation == false
388
- self.class.validate(self)
389
- after_validation
390
- nil
387
+ if before_validation == false
388
+ save_failure(:validation)
389
+ false
390
+ else
391
+ self.class.validate(self)
392
+ after_validation
393
+ nil
394
+ end
391
395
  end
392
396
 
393
397
  # Validates the object and returns true if no errors are reported.
@@ -443,6 +443,13 @@ context "A MySQL database" do
443
443
  specify "should support drop_index" do
444
444
  @db.drop_index :test2, :value
445
445
  end
446
+
447
+ specify "should support add_foreign_key" do
448
+ @db.alter_table :test2 do
449
+ add_foreign_key :value2, :test2, :key=>:value
450
+ end
451
+ @db[:test2].columns.should == [:name, :value, :zyx, :ert, :xyz, :value2]
452
+ end
446
453
  end
447
454
 
448
455
  context "A MySQL database" do
@@ -471,6 +478,16 @@ context "A MySQL database" do
471
478
  ]
472
479
  end
473
480
 
481
+ specify "should correctly format ALTER TABLE statements with foreign keys" do
482
+ g = Sequel::Schema::AlterTableGenerator.new(@db) do
483
+ add_foreign_key :p_id, :users, :key => :id, :null => false, :on_delete => :cascade
484
+ end
485
+ @db.alter_table_sql_list(:items, g.operations).should == [[
486
+ "ALTER TABLE items ADD COLUMN p_id integer NOT NULL",
487
+ "ALTER TABLE items ADD FOREIGN KEY (p_id) REFERENCES users(id) ON DELETE CASCADE"
488
+ ]]
489
+ end
490
+
474
491
  specify "should accept repeated raw sql statements using Database#<<" do
475
492
  @db << 'DELETE FROM items'
476
493
  @db[:items].count.should == 0
@@ -601,3 +601,72 @@ if POSTGRES_DB.server_version >= 80300
601
601
  end
602
602
  end
603
603
  end
604
+
605
+ context "Postgres::Database functions, languages, and triggers" do
606
+ setup do
607
+ @d = POSTGRES_DB
608
+ end
609
+ teardown do
610
+ @d.drop_function('tf', :if_exists=>true, :cascade=>true)
611
+ @d.drop_function('tf', :if_exists=>true, :cascade=>true, :args=>%w'integer integer')
612
+ @d.drop_language(:plpgsql, :if_exists=>true, :cascade=>true)
613
+ @d.drop_trigger(:test5, :identity, :if_exists=>true, :cascade=>true)
614
+ end
615
+
616
+ specify "#create_function and #drop_function should create and drop functions" do
617
+ proc{@d['SELECT tf()'].all}.should raise_error(Sequel::DatabaseError)
618
+ args = ['tf', 'SELECT 1', {:returns=>:integer}]
619
+ @d.create_function_sql(*args).should =~ /\A\s*CREATE FUNCTION tf\(\)\s+RETURNS integer\s+LANGUAGE SQL\s+AS 'SELECT 1'\s*\z/
620
+ @d.create_function(*args)
621
+ rows = @d['SELECT tf()'].all.should == [{:tf=>1}]
622
+ @d.drop_function_sql('tf').should == 'DROP FUNCTION tf()'
623
+ @d.drop_function('tf')
624
+ proc{@d['SELECT tf()'].all}.should raise_error(Sequel::DatabaseError)
625
+ end
626
+
627
+ specify "#create_function and #drop_function should support options" do
628
+ args = ['tf', 'SELECT $1 + $2', {:args=>[[:integer, :a], :integer], :replace=>true, :returns=>:integer, :language=>'SQL', :behavior=>:immutable, :strict=>true, :security_definer=>true, :cost=>2, :set=>{:search_path => 'public'}}]
629
+ @d.create_function_sql(*args).should =~ /\A\s*CREATE OR REPLACE FUNCTION tf\(a integer, integer\)\s+RETURNS integer\s+LANGUAGE SQL\s+IMMUTABLE\s+STRICT\s+SECURITY DEFINER\s+COST 2\s+SET search_path = public\s+AS 'SELECT \$1 \+ \$2'\s*\z/
630
+ @d.create_function(*args)
631
+ # Make sure replace works
632
+ @d.create_function(*args)
633
+ rows = @d['SELECT tf(1, 2)'].all.should == [{:tf=>3}]
634
+ args = ['tf', {:if_exists=>true, :cascade=>true, :args=>[[:integer, :a], :integer]}]
635
+ @d.drop_function_sql(*args).should == 'DROP FUNCTION IF EXISTS tf(a integer, integer) CASCADE'
636
+ @d.drop_function(*args)
637
+ # Make sure if exists works
638
+ @d.drop_function(*args)
639
+ end
640
+
641
+ specify "#create_language and #drop_language should create and drop languages" do
642
+ @d.create_language_sql(:plpgsql).should == 'CREATE LANGUAGE plpgsql'
643
+ @d.create_language(:plpgsql)
644
+ proc{@d.create_language(:plpgsql)}.should raise_error(Sequel::DatabaseError)
645
+ @d.drop_language_sql(:plpgsql).should == 'DROP LANGUAGE plpgsql'
646
+ @d.drop_language(:plpgsql)
647
+ proc{@d.drop_language(:plpgsql)}.should raise_error(Sequel::DatabaseError)
648
+ @d.create_language_sql(:plpgsql, :trusted=>true, :handler=>:a, :validator=>:b).should == 'CREATE TRUSTED LANGUAGE plpgsql HANDLER a VALIDATOR b'
649
+ @d.drop_language_sql(:plpgsql, :if_exists=>true, :cascade=>true).should == 'DROP LANGUAGE IF EXISTS plpgsql CASCADE'
650
+ # Make sure if exists works
651
+ @d.drop_language(:plpgsql, :if_exists=>true, :cascade=>true)
652
+ end
653
+
654
+ specify "#create_trigger and #drop_trigger should create and drop triggers" do
655
+ @d.create_language(:plpgsql)
656
+ @d.create_function(:tf, 'BEGIN IF NEW.value IS NULL THEN RAISE EXCEPTION \'Blah\'; END IF; RETURN NEW; END;', :language=>:plpgsql, :returns=>:trigger)
657
+ @d.create_trigger_sql(:test, :identity, :tf, :each_row=>true).should == 'CREATE TRIGGER identity BEFORE INSERT OR UPDATE OR DELETE ON public.test FOR EACH ROW EXECUTE PROCEDURE tf()'
658
+ @d.create_trigger(:test, :identity, :tf, :each_row=>true)
659
+ @d[:test].insert(:name=>'a', :value=>1)
660
+ @d[:test].filter(:name=>'a').all.should == [{:name=>'a', :value=>1}]
661
+ proc{@d[:test].filter(:name=>'a').update(:value=>nil)}.should raise_error(Sequel::DatabaseError)
662
+ @d[:test].filter(:name=>'a').all.should == [{:name=>'a', :value=>1}]
663
+ @d[:test].filter(:name=>'a').update(:value=>3)
664
+ @d[:test].filter(:name=>'a').all.should == [{:name=>'a', :value=>3}]
665
+ @d.drop_trigger_sql(:test, :identity).should == 'DROP TRIGGER identity ON public.test'
666
+ @d.drop_trigger(:test, :identity)
667
+ @d.create_trigger_sql(:test, :identity, :tf, :after=>true, :events=>:insert, :args=>[1, 'a']).should == 'CREATE TRIGGER identity AFTER INSERT ON public.test EXECUTE PROCEDURE tf(1, \'a\')'
668
+ @d.drop_trigger_sql(:test, :identity, :if_exists=>true, :cascade=>true).should == 'DROP TRIGGER IF EXISTS identity ON public.test CASCADE'
669
+ # Make sure if exists works
670
+ @d.drop_trigger(:test, :identity, :if_exists=>true, :cascade=>true)
671
+ end
672
+ end