sequel 2.6.0 → 2.7.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 +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
|