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.
- 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
|