sequel 2.6.0 → 2.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +64 -0
- data/Rakefile +1 -1
- data/lib/sequel_core/adapters/jdbc.rb +6 -2
- data/lib/sequel_core/adapters/jdbc/oracle.rb +23 -0
- data/lib/sequel_core/adapters/oracle.rb +4 -77
- data/lib/sequel_core/adapters/postgres.rb +39 -26
- data/lib/sequel_core/adapters/shared/mssql.rb +0 -1
- data/lib/sequel_core/adapters/shared/mysql.rb +1 -1
- data/lib/sequel_core/adapters/shared/oracle.rb +82 -0
- data/lib/sequel_core/adapters/shared/postgres.rb +65 -46
- data/lib/sequel_core/core_ext.rb +10 -0
- data/lib/sequel_core/core_sql.rb +7 -0
- data/lib/sequel_core/database.rb +22 -0
- data/lib/sequel_core/database/schema.rb +1 -1
- data/lib/sequel_core/dataset.rb +29 -11
- data/lib/sequel_core/dataset/sql.rb +27 -7
- data/lib/sequel_core/migration.rb +20 -2
- data/lib/sequel_core/object_graph.rb +24 -10
- data/lib/sequel_core/schema/generator.rb +22 -9
- data/lib/sequel_core/schema/sql.rb +13 -9
- data/lib/sequel_core/sql.rb +27 -2
- data/lib/sequel_model/association_reflection.rb +251 -141
- data/lib/sequel_model/associations.rb +114 -61
- data/lib/sequel_model/base.rb +25 -21
- data/lib/sequel_model/eager_loading.rb +17 -40
- data/lib/sequel_model/hooks.rb +25 -24
- data/lib/sequel_model/record.rb +29 -51
- data/lib/sequel_model/schema.rb +1 -1
- data/lib/sequel_model/validations.rb +13 -3
- data/spec/adapters/postgres_spec.rb +104 -18
- data/spec/adapters/spec_helper.rb +4 -1
- data/spec/integration/eager_loader_test.rb +5 -4
- data/spec/integration/spec_helper.rb +4 -1
- data/spec/sequel_core/connection_pool_spec.rb +24 -24
- data/spec/sequel_core/core_sql_spec.rb +12 -0
- data/spec/sequel_core/dataset_spec.rb +77 -2
- data/spec/sequel_core/expression_filters_spec.rb +6 -0
- data/spec/sequel_core/object_graph_spec.rb +40 -2
- data/spec/sequel_core/schema_spec.rb +13 -0
- data/spec/sequel_model/association_reflection_spec.rb +8 -8
- data/spec/sequel_model/associations_spec.rb +164 -3
- data/spec/sequel_model/caching_spec.rb +2 -1
- data/spec/sequel_model/eager_loading_spec.rb +107 -3
- data/spec/sequel_model/hooks_spec.rb +38 -22
- data/spec/sequel_model/model_spec.rb +11 -35
- data/spec/sequel_model/plugins_spec.rb +4 -2
- data/spec/sequel_model/record_spec.rb +8 -5
- data/spec/sequel_model/validations_spec.rb +25 -0
- data/spec/spec_config.rb +4 -3
- metadata +21 -19
@@ -128,32 +128,13 @@ module Sequel::Model::Associations::EagerLoading
|
|
128
128
|
klass = r.associated_class
|
129
129
|
assoc_name = r[:name]
|
130
130
|
assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
|
131
|
-
|
132
|
-
conditions = r[:graph_conditions]
|
133
|
-
use_only_conditions = r.include?(:graph_only_conditions)
|
134
|
-
only_conditions = r[:graph_only_conditions]
|
135
|
-
select = r[:graph_select]
|
136
|
-
graph_block = r[:graph_block]
|
137
|
-
ds = case assoc_type = r[:type]
|
138
|
-
when :many_to_one
|
139
|
-
ds.graph(klass, use_only_conditions ? only_conditions : [[klass.primary_key, r[:key].qualify(ta)]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
140
|
-
when :one_to_many
|
141
|
-
ds = ds.graph(klass, use_only_conditions ? only_conditions : [[r[:key], model.primary_key.qualify(ta)]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
142
|
-
# We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
|
143
|
-
ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = r.reciprocal
|
144
|
-
ds
|
145
|
-
when :many_to_many
|
146
|
-
use_jt_only_conditions = r.include?(:graph_join_table_only_conditions)
|
147
|
-
ds = ds.graph(r[:join_table], use_jt_only_conditions ? r[:graph_join_table_only_conditions] : [[r[:left_key], model.primary_key.qualify(ta)]] + r[:graph_join_table_conditions], :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]), :join_type=>r[:graph_join_table_join_type], &r[:graph_join_table_block])
|
148
|
-
ds.graph(klass, use_only_conditions ? only_conditions : [[klass.primary_key, r[:right_key]]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
149
|
-
end
|
150
|
-
|
131
|
+
ds = r[:eager_grapher].call(ds, assoc_table_alias, ta)
|
151
132
|
ds = ds.order_more(*Array(r[:order]).map{|c| eager_graph_qualify_order(assoc_table_alias, c)}) if r[:order] and r[:order_eager_graph]
|
152
133
|
eager_graph = ds.opts[:eager_graph]
|
153
134
|
eager_graph[:requirements][assoc_table_alias] = requirements.dup
|
154
135
|
eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
|
155
|
-
eager_graph[:alias_association_type_map][assoc_table_alias] =
|
156
|
-
ds = ds.eager_graph_associations(ds,
|
136
|
+
eager_graph[:alias_association_type_map][assoc_table_alias] = r.returns_array?
|
137
|
+
ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
|
157
138
|
ds
|
158
139
|
end
|
159
140
|
|
@@ -240,7 +221,7 @@ module Sequel::Model::Associations::EagerLoading
|
|
240
221
|
end
|
241
222
|
|
242
223
|
# Remove duplicate records from all associations if this graph could possibly be a cartesian product
|
243
|
-
eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.
|
224
|
+
eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.values.select{|v| v}.length > 1
|
244
225
|
|
245
226
|
# Replace the array of object graphs with an array of model objects
|
246
227
|
record_graphs.replace(records)
|
@@ -286,7 +267,7 @@ module Sequel::Model::Associations::EagerLoading
|
|
286
267
|
# Don't clobber the instance variable array for *_to_many associations if it has already been setup
|
287
268
|
dependency_map.keys.each do |ta|
|
288
269
|
assoc_name = alias_map[ta]
|
289
|
-
current.associations[assoc_name] = type_map[ta]
|
270
|
+
current.associations[assoc_name] = type_map[ta] ? [] : nil unless current.associations.include?(assoc_name)
|
290
271
|
end
|
291
272
|
dependency_map.each do |ta, deps|
|
292
273
|
next unless rec = record_graph[ta]
|
@@ -297,12 +278,12 @@ module Sequel::Model::Associations::EagerLoading
|
|
297
278
|
records_map[ta][rec.pk] = rec
|
298
279
|
end
|
299
280
|
assoc_name = alias_map[ta]
|
300
|
-
case
|
301
|
-
when
|
281
|
+
case type_map[ta]
|
282
|
+
when false
|
302
283
|
current.associations[assoc_name] = rec
|
303
284
|
else
|
304
285
|
current.associations[assoc_name].push(rec)
|
305
|
-
if
|
286
|
+
if reciprocal = reciprocal_map[ta]
|
306
287
|
rec.associations[reciprocal] = current
|
307
288
|
end
|
308
289
|
end
|
@@ -319,7 +300,7 @@ module Sequel::Model::Associations::EagerLoading
|
|
319
300
|
def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
|
320
301
|
records.each do |record|
|
321
302
|
dependency_map.each do |ta, deps|
|
322
|
-
list = if type_map[ta]
|
303
|
+
list = if !type_map[ta]
|
323
304
|
item = record.send(alias_map[ta])
|
324
305
|
[item] if item
|
325
306
|
else
|
@@ -361,27 +342,23 @@ module Sequel::Model::Associations::EagerLoading
|
|
361
342
|
# and values being an array of current model objects with that
|
362
343
|
# specific foreign/primary key
|
363
344
|
key_hash = {}
|
364
|
-
# array of attribute_values keys to monitor
|
365
|
-
keys = []
|
366
345
|
# Reflections for all associations to eager load
|
367
346
|
reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}
|
368
347
|
|
369
348
|
# Populate keys to monitor
|
370
|
-
reflections.each
|
371
|
-
key = reflection[:type] == :many_to_one ? reflection[:key] : model.primary_key
|
372
|
-
next if key_hash[key]
|
373
|
-
key_hash[key] = {}
|
374
|
-
keys << key
|
375
|
-
end
|
349
|
+
reflections.each{|reflection| key_hash[reflection.eager_loader_key] ||= Hash.new{|h,k| h[k] = []}}
|
376
350
|
|
377
351
|
# Associate each object with every key being monitored
|
378
|
-
a.each do |
|
379
|
-
|
380
|
-
|
352
|
+
a.each do |rec|
|
353
|
+
key_hash.each do |key, id_map|
|
354
|
+
id_map[rec[key]] << rec if rec[key]
|
381
355
|
end
|
382
356
|
end
|
383
357
|
|
384
|
-
reflections.each
|
358
|
+
reflections.each do |r|
|
359
|
+
r[:eager_loader].call(key_hash, a, eager_assoc[r[:name]])
|
360
|
+
a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty?
|
361
|
+
end
|
385
362
|
end
|
386
363
|
|
387
364
|
# Build associations from the graph if #eager_graph was used,
|
data/lib/sequel_model/hooks.rb
CHANGED
@@ -8,12 +8,14 @@ module Sequel
|
|
8
8
|
# Hooks that are only for internal use
|
9
9
|
PRIVATE_HOOKS = [:before_update_values, :before_delete]
|
10
10
|
|
11
|
-
# Returns true if
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
# Returns true if there are any hook blocks for the given hook.
|
12
|
+
def self.has_hooks?(hook)
|
13
|
+
!@hooks[hook].empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
# Yield every block related to the given hook.
|
17
|
+
def self.hook_blocks(hook)
|
18
|
+
@hooks[hook].each{|k,v| yield v}
|
17
19
|
end
|
18
20
|
|
19
21
|
### Private Class Methods ###
|
@@ -26,7 +28,7 @@ module Sequel
|
|
26
28
|
(raise Error, 'No hook method specified') unless tag
|
27
29
|
block = proc {send tag}
|
28
30
|
end
|
29
|
-
h = hooks[hook]
|
31
|
+
h = @hooks[hook]
|
30
32
|
if tag && (old = h.find{|x| x[0] == tag})
|
31
33
|
old[1] = block
|
32
34
|
else
|
@@ -34,28 +36,27 @@ module Sequel
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
((self == Model ? [] : superclass.send(:all_hooks, hook)) + hooks[hook].collect{|x| x[1]})
|
41
|
-
end
|
42
|
-
|
43
|
-
# Returns the hooks hash for this model class.
|
44
|
-
def self.hooks #:nodoc:
|
45
|
-
@hooks ||= Hash.new {|h, k| h[k] = []}
|
39
|
+
# Define a hook instance method that calls the run_hooks instance method.
|
40
|
+
def self.define_hook_instance_method(hook) #:nodoc:
|
41
|
+
class_eval("def #{hook}; run_hooks(:#{hook}); end")
|
46
42
|
end
|
47
43
|
|
48
|
-
|
49
|
-
# Returns false if any hook returns false.
|
50
|
-
def self.run_hooks(hook, object) #:nodoc:
|
51
|
-
all_hooks(hook).each{|b| return false if object.instance_eval(&b) == false}
|
52
|
-
end
|
44
|
+
private_class_method :add_hook, :define_hook_instance_method
|
53
45
|
|
54
|
-
|
46
|
+
private
|
55
47
|
|
48
|
+
# Runs all hook blocks of given hook type on this object.
|
49
|
+
# Stops running hook blocks and returns false if any hook block returns false.
|
50
|
+
def run_hooks(hook)
|
51
|
+
model.hook_blocks(hook){|block| return false if instance_eval(&block) == false}
|
52
|
+
end
|
53
|
+
|
54
|
+
# For performance reasons, we define empty hook instance methods, which are
|
55
|
+
# overwritten with real hook instance methods whenever the hook class method is called.
|
56
56
|
(HOOKS + PRIVATE_HOOKS).each do |hook|
|
57
|
-
|
58
|
-
|
57
|
+
@hooks[hook] = []
|
58
|
+
instance_eval("def #{hook}(method = nil, &block); define_hook_instance_method(:#{hook}); add_hook(:#{hook}, method, &block) end")
|
59
|
+
class_eval("def #{hook}; end")
|
59
60
|
end
|
60
61
|
end
|
61
62
|
end
|
data/lib/sequel_model/record.rb
CHANGED
@@ -12,35 +12,18 @@ module Sequel
|
|
12
12
|
# The columns that have been updated. This isn't completely accurate,
|
13
13
|
# see Model#[]=.
|
14
14
|
attr_reader :changed_columns
|
15
|
-
|
16
|
-
# Whether this model instance should raise an exception instead of
|
17
|
-
# returning nil on a failure to save/save_changes/etc.
|
18
|
-
attr_writer :raise_on_save_failure
|
19
|
-
|
20
|
-
# Whether this model instance should raise an error when it cannot typecast
|
21
|
-
# data for a column correctly.
|
22
|
-
attr_writer :raise_on_typecast_failure
|
23
|
-
|
24
|
-
# Whether this model instance should raise an error if attempting
|
25
|
-
# to call a method through set/update and their variants that either
|
26
|
-
# doesn't exist or access to it is denied.
|
27
|
-
attr_writer :strict_param_setting
|
28
|
-
|
29
|
-
# Whether this model instance should typecast the empty string ('') to
|
30
|
-
# nil for columns that are non string or blob.
|
31
|
-
attr_writer :typecast_empty_string_to_nil
|
32
|
-
|
33
|
-
# Whether this model instance should typecast on attribute assignment
|
34
|
-
attr_writer :typecast_on_assignment
|
35
15
|
|
36
16
|
# The hash of attribute values. Keys are symbols with the names of the
|
37
17
|
# underlying database columns.
|
38
18
|
attr_reader :values
|
39
19
|
|
40
20
|
class_attr_reader :columns, :dataset, :db, :primary_key, :str_columns
|
21
|
+
class_attr_overridable :db_schema, :raise_on_save_failure, :raise_on_typecast_failure, :strict_param_setting, :typecast_empty_string_to_nil, :typecast_on_assignment
|
22
|
+
remove_method :db_schema=
|
41
23
|
|
42
24
|
# Creates new instance with values set to passed-in Hash.
|
43
|
-
# If a block is given, yield the instance to the block
|
25
|
+
# If a block is given, yield the instance to the block unless
|
26
|
+
# from_db is true.
|
44
27
|
# This method runs the after_initialize hook after
|
45
28
|
# it has optionally yielded itself to the block.
|
46
29
|
#
|
@@ -49,16 +32,9 @@ module Sequel
|
|
49
32
|
# string keys will work if from_db is false.
|
50
33
|
# * from_db - should only be set by Model.load, forget it
|
51
34
|
# exists.
|
52
|
-
def initialize(values =
|
53
|
-
values ||= {}
|
35
|
+
def initialize(values = {}, from_db = false, &block)
|
54
36
|
@associations = {}
|
55
|
-
@db_schema = model.db_schema
|
56
37
|
@changed_columns = []
|
57
|
-
@raise_on_save_failure = model.raise_on_save_failure
|
58
|
-
@strict_param_setting = model.strict_param_setting
|
59
|
-
@typecast_on_assignment = model.typecast_on_assignment
|
60
|
-
@typecast_empty_string_to_nil = model.typecast_empty_string_to_nil
|
61
|
-
@raise_on_typecast_failure = model.raise_on_typecast_failure
|
62
38
|
if from_db
|
63
39
|
@new = false
|
64
40
|
@values = values
|
@@ -66,10 +42,9 @@ module Sequel
|
|
66
42
|
@values = {}
|
67
43
|
@new = true
|
68
44
|
set(values)
|
45
|
+
@changed_columns.clear
|
46
|
+
yield self if block
|
69
47
|
end
|
70
|
-
@changed_columns.clear
|
71
|
-
|
72
|
-
yield self if block
|
73
48
|
after_initialize
|
74
49
|
end
|
75
50
|
|
@@ -391,12 +366,11 @@ module Sequel
|
|
391
366
|
# Add/Set the current object to/as the given object's reciprocal association.
|
392
367
|
def add_reciprocal_object(opts, o)
|
393
368
|
return unless reciprocal = opts.reciprocal
|
394
|
-
|
395
|
-
when :many_to_many, :many_to_one
|
369
|
+
if opts.reciprocal_array?
|
396
370
|
if array = o.associations[reciprocal] and !array.include?(self)
|
397
371
|
array.push(self)
|
398
372
|
end
|
399
|
-
|
373
|
+
else
|
400
374
|
o.associations[reciprocal] = self
|
401
375
|
end
|
402
376
|
end
|
@@ -407,14 +381,14 @@ module Sequel
|
|
407
381
|
if @associations.include?(name) and !reload
|
408
382
|
@associations[name]
|
409
383
|
else
|
410
|
-
objs = if opts.
|
384
|
+
objs = if opts.returns_array?
|
385
|
+
send(opts.dataset_method).all
|
386
|
+
else
|
411
387
|
if !opts[:key]
|
412
388
|
send(opts.dataset_method).all.first
|
413
389
|
elsif send(opts[:key])
|
414
390
|
send(opts.dataset_method).first
|
415
391
|
end
|
416
|
-
else
|
417
|
-
objs = send(opts.dataset_method).all
|
418
392
|
end
|
419
393
|
run_association_callbacks(opts, :after_load, objs)
|
420
394
|
# Only one_to_many associations should set the reciprocal object
|
@@ -447,21 +421,19 @@ module Sequel
|
|
447
421
|
# Remove/unset the current object from/as the given object's reciprocal association.
|
448
422
|
def remove_reciprocal_object(opts, o)
|
449
423
|
return unless reciprocal = opts.reciprocal
|
450
|
-
|
451
|
-
when :many_to_many, :many_to_one
|
424
|
+
if opts.reciprocal_array?
|
452
425
|
if array = o.associations[reciprocal]
|
453
426
|
array.delete_if{|x| self === x}
|
454
427
|
end
|
455
|
-
|
428
|
+
else
|
456
429
|
o.associations[reciprocal] = nil
|
457
430
|
end
|
458
431
|
end
|
459
432
|
|
460
433
|
# Run the callback for the association with the object.
|
461
434
|
def run_association_callbacks(reflection, callback_type, object)
|
462
|
-
raise_error =
|
463
|
-
|
464
|
-
stop_on_false = true if [:before_add, :before_remove].include?(callback_type)
|
435
|
+
raise_error = raise_on_save_failure || !reflection.returns_array?
|
436
|
+
stop_on_false = [:before_add, :before_remove].include?(callback_type)
|
465
437
|
reflection[callback_type].each do |cb|
|
466
438
|
res = case cb
|
467
439
|
when Symbol
|
@@ -480,7 +452,7 @@ module Sequel
|
|
480
452
|
|
481
453
|
# Raise an error if raise_on_save_failure is true
|
482
454
|
def save_failure(action, raise_error = nil)
|
483
|
-
raise_error =
|
455
|
+
raise_error = raise_on_save_failure if raise_error.nil?
|
484
456
|
raise(Error, "unable to #{action} record") if raise_error
|
485
457
|
end
|
486
458
|
|
@@ -488,14 +460,14 @@ module Sequel
|
|
488
460
|
def set_restricted(hash, only, except)
|
489
461
|
columns_not_set = model.instance_variable_get(:@columns).blank?
|
490
462
|
meths = setter_methods(only, except)
|
491
|
-
|
463
|
+
strict = strict_param_setting
|
492
464
|
hash.each do |k,v|
|
493
465
|
m = "#{k}="
|
494
466
|
if meths.include?(m)
|
495
467
|
send(m, v)
|
496
468
|
elsif columns_not_set && (Symbol === k)
|
497
469
|
self[k] = v
|
498
|
-
elsif
|
470
|
+
elsif strict
|
499
471
|
raise Error, "method #{m} doesn't exist or access is restricted to it"
|
500
472
|
end
|
501
473
|
end
|
@@ -535,13 +507,13 @@ module Sequel
|
|
535
507
|
# typecast_value method, so database adapters can override/augment the handling
|
536
508
|
# for database specific column types.
|
537
509
|
def typecast_value(column, value)
|
538
|
-
return value unless
|
539
|
-
value = nil if value == '' and
|
540
|
-
raise(Error::InvalidValue, "nil/NULL is not allowed for the #{column} column") if
|
510
|
+
return value unless typecast_on_assignment && db_schema && (col_schema = db_schema[column]) && !model.serialized?(column)
|
511
|
+
value = nil if value == '' and typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type])
|
512
|
+
raise(Error::InvalidValue, "nil/NULL is not allowed for the #{column} column") if raise_on_typecast_failure && value.nil? && (col_schema[:allow_null] == false)
|
541
513
|
begin
|
542
514
|
model.db.typecast_value(col_schema[:type], value)
|
543
515
|
rescue Error::InvalidValue
|
544
|
-
if
|
516
|
+
if raise_on_typecast_failure
|
545
517
|
raise
|
546
518
|
else
|
547
519
|
value
|
@@ -549,6 +521,12 @@ module Sequel
|
|
549
521
|
end
|
550
522
|
end
|
551
523
|
|
524
|
+
# Call uniq! on the given array. This is used by the :uniq option,
|
525
|
+
# and is an actual method for memory reasons.
|
526
|
+
def array_uniq!(a)
|
527
|
+
a.uniq!
|
528
|
+
end
|
529
|
+
|
552
530
|
# Set the columns, filtered by the only and except arrays.
|
553
531
|
def update_restricted(hash, only, except)
|
554
532
|
set_restricted(hash, only, except)
|
data/lib/sequel_model/schema.rb
CHANGED
@@ -18,6 +18,11 @@ module Sequel
|
|
18
18
|
def add(att, msg)
|
19
19
|
self[att] << msg
|
20
20
|
end
|
21
|
+
|
22
|
+
# Return the total number of error messages.
|
23
|
+
def count
|
24
|
+
full_messages.length
|
25
|
+
end
|
21
26
|
|
22
27
|
# Returns an array of fully-formatted error messages.
|
23
28
|
def full_messages
|
@@ -28,9 +33,10 @@ module Sequel
|
|
28
33
|
end
|
29
34
|
end
|
30
35
|
|
31
|
-
# Returns the errors for the given attribute
|
36
|
+
# Returns the array of errors for the given attribute, or nil
|
37
|
+
# if there are no errors for the attribute.
|
32
38
|
def on(att)
|
33
|
-
self[att]
|
39
|
+
self[att] if include?(att)
|
34
40
|
end
|
35
41
|
end
|
36
42
|
|
@@ -304,7 +310,11 @@ module Sequel
|
|
304
310
|
end
|
305
311
|
|
306
312
|
# Validates only if the fields in the model (specified by atts) are
|
307
|
-
# unique in the database.
|
313
|
+
# unique in the database. Pass an array of fields instead of multiple
|
314
|
+
# fields to specify that the combination of fields must be unique,
|
315
|
+
# instead of that each field should have a unique value.
|
316
|
+
#
|
317
|
+
# You should also add a unique index in the
|
308
318
|
# database, as this suffers from a fairly obvious race condition.
|
309
319
|
#
|
310
320
|
# Possible Options:
|
@@ -343,8 +343,8 @@ context "A PostgreSQL database" do
|
|
343
343
|
full_text_index [:title, :body]
|
344
344
|
end
|
345
345
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
346
|
-
"CREATE TABLE posts (title text, body text)",
|
347
|
-
"CREATE INDEX posts_title_body_index ON posts USING gin (to_tsvector('simple', title || body))"
|
346
|
+
"CREATE TABLE public.posts (title text, body text)",
|
347
|
+
"CREATE INDEX posts_title_body_index ON posts USING gin (to_tsvector('simple', (COALESCE(title, '') || ' ' || COALESCE(body, ''))))"
|
348
348
|
]
|
349
349
|
end
|
350
350
|
|
@@ -355,20 +355,20 @@ context "A PostgreSQL database" do
|
|
355
355
|
full_text_index [:title, :body], :language => 'french'
|
356
356
|
end
|
357
357
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
358
|
-
"CREATE TABLE posts (title text, body text)",
|
359
|
-
"CREATE INDEX posts_title_body_index ON posts USING gin (to_tsvector('french', title || body))"
|
358
|
+
"CREATE TABLE public.posts (title text, body text)",
|
359
|
+
"CREATE INDEX posts_title_body_index ON posts USING gin (to_tsvector('french', (COALESCE(title, '') || ' ' || COALESCE(body, ''))))"
|
360
360
|
]
|
361
361
|
end
|
362
362
|
|
363
363
|
specify "should support full_text_search" do
|
364
364
|
POSTGRES_DB[:posts].full_text_search(:title, 'ruby').sql.should ==
|
365
|
-
"SELECT * FROM posts WHERE (to_tsvector(title) @@ to_tsquery('ruby'))"
|
365
|
+
"SELECT * FROM posts WHERE (to_tsvector('simple', (COALESCE(title, ''))) @@ to_tsquery('simple', 'ruby'))"
|
366
366
|
|
367
367
|
POSTGRES_DB[:posts].full_text_search([:title, :body], ['ruby', 'sequel']).sql.should ==
|
368
|
-
"SELECT * FROM posts WHERE (to_tsvector(title || body) @@ to_tsquery('ruby | sequel'))"
|
368
|
+
"SELECT * FROM posts WHERE (to_tsvector('simple', (COALESCE(title, '') || ' ' || COALESCE(body, ''))) @@ to_tsquery('simple', 'ruby | sequel'))"
|
369
369
|
|
370
370
|
POSTGRES_DB[:posts].full_text_search(:title, 'ruby', :language => 'french').sql.should ==
|
371
|
-
"SELECT * FROM posts WHERE (to_tsvector('french', title) @@ to_tsquery('french', 'ruby'))"
|
371
|
+
"SELECT * FROM posts WHERE (to_tsvector('french', (COALESCE(title, ''))) @@ to_tsquery('french', 'ruby'))"
|
372
372
|
end
|
373
373
|
|
374
374
|
specify "should support spatial indexes" do
|
@@ -377,7 +377,7 @@ context "A PostgreSQL database" do
|
|
377
377
|
spatial_index [:geom]
|
378
378
|
end
|
379
379
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
380
|
-
"CREATE TABLE posts (geom geometry)",
|
380
|
+
"CREATE TABLE public.posts (geom geometry)",
|
381
381
|
"CREATE INDEX posts_geom_index ON posts USING gist (geom)"
|
382
382
|
]
|
383
383
|
end
|
@@ -388,7 +388,7 @@ context "A PostgreSQL database" do
|
|
388
388
|
index :title, :type => 'hash'
|
389
389
|
end
|
390
390
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
391
|
-
"CREATE TABLE posts (title varchar(5))",
|
391
|
+
"CREATE TABLE public.posts (title varchar(5))",
|
392
392
|
"CREATE INDEX posts_title_index ON posts USING hash (title)"
|
393
393
|
]
|
394
394
|
end
|
@@ -399,7 +399,7 @@ context "A PostgreSQL database" do
|
|
399
399
|
index :title, :type => 'hash', :unique => true
|
400
400
|
end
|
401
401
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
402
|
-
"CREATE TABLE posts (title varchar(5))",
|
402
|
+
"CREATE TABLE public.posts (title varchar(5))",
|
403
403
|
"CREATE UNIQUE INDEX posts_title_index ON posts USING hash (title)"
|
404
404
|
]
|
405
405
|
end
|
@@ -410,7 +410,7 @@ context "A PostgreSQL database" do
|
|
410
410
|
index :title, :where => {:something => 5}
|
411
411
|
end
|
412
412
|
POSTGRES_DB.create_table_sql_list(:posts, *g.create_info).should == [
|
413
|
-
"CREATE TABLE posts (title varchar(5))",
|
413
|
+
"CREATE TABLE public.posts (title varchar(5))",
|
414
414
|
"CREATE INDEX posts_title_index ON posts (title) WHERE (something = 5)"
|
415
415
|
]
|
416
416
|
end
|
@@ -496,22 +496,108 @@ context "Postgres::Dataset#insert" do
|
|
496
496
|
end
|
497
497
|
end
|
498
498
|
|
499
|
+
context "Postgres::Database schema qualified tables" do
|
500
|
+
setup do
|
501
|
+
POSTGRES_DB << "CREATE SCHEMA schema_test"
|
502
|
+
POSTGRES_DB.instance_variable_set(:@primary_keys, {})
|
503
|
+
POSTGRES_DB.instance_variable_set(:@primary_key_sequences, {})
|
504
|
+
end
|
505
|
+
teardown do
|
506
|
+
POSTGRES_DB << "DROP SCHEMA schema_test CASCADE"
|
507
|
+
POSTGRES_DB.default_schema = :public
|
508
|
+
end
|
509
|
+
|
510
|
+
specify "should be able to create, drop, select and insert into tables in a given schema" do
|
511
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){primary_key :i}
|
512
|
+
POSTGRES_DB[:schema_test__schema_test].first.should == nil
|
513
|
+
POSTGRES_DB[:schema_test__schema_test].insert(:i=>1).should == 1
|
514
|
+
POSTGRES_DB[:schema_test__schema_test].first.should == {:i=>1}
|
515
|
+
POSTGRES_DB.from('schema_test.schema_test'.lit).first.should == {:i=>1}
|
516
|
+
POSTGRES_DB.drop_table(:schema_test__schema_test)
|
517
|
+
POSTGRES_DB.create_table(:schema_test.qualify(:schema_test)){integer :i}
|
518
|
+
POSTGRES_DB[:schema_test__schema_test].first.should == nil
|
519
|
+
POSTGRES_DB.from('schema_test.schema_test'.lit).first.should == nil
|
520
|
+
POSTGRES_DB.drop_table(:schema_test.qualify(:schema_test))
|
521
|
+
end
|
522
|
+
|
523
|
+
specify "#tables should include only tables in the public schema if no schema is given" do
|
524
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){integer :i}
|
525
|
+
POSTGRES_DB.tables.should_not include(:schema_test)
|
526
|
+
end
|
527
|
+
|
528
|
+
specify "#tables should return tables in the schema provided by the :schema argument" do
|
529
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){integer :i}
|
530
|
+
POSTGRES_DB.tables(:schema=>:schema_test).should == [:schema_test]
|
531
|
+
end
|
532
|
+
|
533
|
+
specify "#table_exists? should assume the public schema if no schema is provided" do
|
534
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){integer :i}
|
535
|
+
POSTGRES_DB.table_exists?(:schema_test).should == false
|
536
|
+
end
|
537
|
+
|
538
|
+
specify "#table_exists? should see if the table is in a given schema" do
|
539
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){integer :i}
|
540
|
+
POSTGRES_DB.table_exists?(:schema_test__schema_test).should == true
|
541
|
+
end
|
542
|
+
|
543
|
+
specify "should be able to get primary keys for tables in a given schema" do
|
544
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){primary_key :i}
|
545
|
+
POSTGRES_DB.synchronize{|c| POSTGRES_DB.send(:primary_key_for_table, c, :schema_test__schema_test).should == 'i'}
|
546
|
+
end
|
547
|
+
|
548
|
+
specify "should be able to get serial sequences for tables in a given schema" do
|
549
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){primary_key :i}
|
550
|
+
POSTGRES_DB.synchronize{|c| POSTGRES_DB.send(:primary_key_sequence_for_table, c, :schema_test__schema_test).should == '"schema_test"."schema_test_i_seq"'}
|
551
|
+
end
|
552
|
+
|
553
|
+
specify "should be able to get custom sequences for tables in a given schema" do
|
554
|
+
POSTGRES_DB << "CREATE SEQUENCE schema_test.kseq"
|
555
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){integer :j; primary_key :k, :type=>:integer, :default=>"nextval('schema_test.kseq'::regclass)".lit}
|
556
|
+
POSTGRES_DB.synchronize{|c| POSTGRES_DB.send(:primary_key_sequence_for_table, c, :schema_test__schema_test).should == '"schema_test"."kseq"'}
|
557
|
+
end
|
558
|
+
|
559
|
+
specify "#default_schema= should change the default schema used from public" do
|
560
|
+
POSTGRES_DB.create_table(:schema_test__schema_test){primary_key :i}
|
561
|
+
POSTGRES_DB.default_schema = :schema_test
|
562
|
+
POSTGRES_DB.table_exists?(:schema_test).should == true
|
563
|
+
POSTGRES_DB.tables.should == [:schema_test]
|
564
|
+
POSTGRES_DB.synchronize{|c| POSTGRES_DB.send(:primary_key_for_table, c, :schema_test).should == 'i'}
|
565
|
+
POSTGRES_DB.synchronize{|c| POSTGRES_DB.send(:primary_key_sequence_for_table, c, :schema_test).should == '"schema_test"."schema_test_i_seq"'}
|
566
|
+
end
|
567
|
+
end
|
568
|
+
|
499
569
|
if POSTGRES_DB.server_version >= 80300
|
500
570
|
|
501
571
|
POSTGRES_DB.create_table! :test6 do
|
502
572
|
text :title
|
503
|
-
|
573
|
+
text :body
|
574
|
+
full_text_index [:title, :body]
|
504
575
|
end
|
505
576
|
|
506
577
|
context "PostgreSQL tsearch2" do
|
578
|
+
before do
|
579
|
+
@ds = POSTGRES_DB[:test6]
|
580
|
+
end
|
581
|
+
after do
|
582
|
+
POSTGRES_DB[:test6].delete
|
583
|
+
end
|
507
584
|
|
508
585
|
specify "should search by indexed column" do
|
509
|
-
|
510
|
-
ds
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
586
|
+
record = {:title => "oopsla conference", :body => "test"}
|
587
|
+
@ds << record
|
588
|
+
@ds.full_text_search(:title, "oopsla").all.should include(record)
|
589
|
+
end
|
590
|
+
|
591
|
+
specify "should join multiple coumns with spaces to search by last words in row" do
|
592
|
+
record = {:title => "multiple words", :body => "are easy to search"}
|
593
|
+
@ds << record
|
594
|
+
@ds.full_text_search([:title, :body], "words").all.should include(record)
|
595
|
+
end
|
596
|
+
|
597
|
+
specify "should return rows with a NULL in one column if a match in another column" do
|
598
|
+
record = {:title => "multiple words", :body =>nil}
|
599
|
+
@ds << record
|
600
|
+
@ds.full_text_search([:title, :body], "words").all.should include(record)
|
515
601
|
end
|
516
602
|
end
|
517
603
|
end
|