sequel 2.8.0 → 2.9.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 (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