sequel 2.8.0 → 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +34 -0
- data/COPYING +1 -1
- data/Rakefile +1 -1
- data/bin/sequel +12 -5
- data/doc/advanced_associations.rdoc +17 -3
- data/lib/sequel_core/adapters/informix.rb +1 -1
- data/lib/sequel_core/adapters/postgres.rb +12 -2
- data/lib/sequel_core/adapters/shared/mssql.rb +3 -1
- data/lib/sequel_core/adapters/shared/mysql.rb +14 -0
- data/lib/sequel_core/adapters/shared/oracle.rb +7 -6
- data/lib/sequel_core/adapters/shared/postgres.rb +170 -3
- data/lib/sequel_core/adapters/shared/progress.rb +1 -1
- data/lib/sequel_core/adapters/shared/sqlite.rb +11 -6
- data/lib/sequel_core/adapters/sqlite.rb +8 -0
- data/lib/sequel_core/dataset/sql.rb +23 -19
- data/lib/sequel_core/dataset/unsupported.rb +12 -0
- data/lib/sequel_core/schema/sql.rb +3 -1
- data/lib/sequel_model.rb +1 -1
- data/lib/sequel_model/associations.rb +1 -1
- data/lib/sequel_model/base.rb +2 -1
- data/lib/sequel_model/dataset_methods.rb +1 -1
- data/lib/sequel_model/eager_loading.rb +1 -1
- data/lib/sequel_model/exceptions.rb +7 -0
- data/lib/sequel_model/record.rb +22 -13
- data/lib/sequel_model/validations.rb +8 -4
- data/spec/adapters/mysql_spec.rb +17 -0
- data/spec/adapters/postgres_spec.rb +69 -0
- data/spec/adapters/sqlite_spec.rb +38 -3
- data/spec/integration/dataset_test.rb +51 -0
- data/spec/integration/schema_test.rb +4 -0
- data/spec/sequel_core/core_ext_spec.rb +2 -2
- data/spec/sequel_core/dataset_spec.rb +35 -1
- data/spec/sequel_core/schema_spec.rb +7 -0
- data/spec/sequel_model/association_reflection_spec.rb +13 -13
- data/spec/sequel_model/hooks_spec.rb +9 -5
- data/spec/sequel_model/validations_spec.rb +1 -1
- 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
|
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
|
-
|
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}(#{
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
885
|
-
|
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
|
-
|
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,
|
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
|
data/lib/sequel_model/base.rb
CHANGED
@@ -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
|
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
|
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
|
data/lib/sequel_model/record.rb
CHANGED
@@ -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=#{
|
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
|
-
|
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,
|
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,
|
360
|
-
raise(Sequel::Error,
|
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,
|
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,
|
415
|
-
raise(Sequel::Error,
|
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
|
-
|
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(
|
458
|
-
|
459
|
-
|
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
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
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.
|
data/spec/adapters/mysql_spec.rb
CHANGED
@@ -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
|