sequel 2.6.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/CHANGELOG +64 -0
  2. data/Rakefile +1 -1
  3. data/lib/sequel_core/adapters/jdbc.rb +6 -2
  4. data/lib/sequel_core/adapters/jdbc/oracle.rb +23 -0
  5. data/lib/sequel_core/adapters/oracle.rb +4 -77
  6. data/lib/sequel_core/adapters/postgres.rb +39 -26
  7. data/lib/sequel_core/adapters/shared/mssql.rb +0 -1
  8. data/lib/sequel_core/adapters/shared/mysql.rb +1 -1
  9. data/lib/sequel_core/adapters/shared/oracle.rb +82 -0
  10. data/lib/sequel_core/adapters/shared/postgres.rb +65 -46
  11. data/lib/sequel_core/core_ext.rb +10 -0
  12. data/lib/sequel_core/core_sql.rb +7 -0
  13. data/lib/sequel_core/database.rb +22 -0
  14. data/lib/sequel_core/database/schema.rb +1 -1
  15. data/lib/sequel_core/dataset.rb +29 -11
  16. data/lib/sequel_core/dataset/sql.rb +27 -7
  17. data/lib/sequel_core/migration.rb +20 -2
  18. data/lib/sequel_core/object_graph.rb +24 -10
  19. data/lib/sequel_core/schema/generator.rb +22 -9
  20. data/lib/sequel_core/schema/sql.rb +13 -9
  21. data/lib/sequel_core/sql.rb +27 -2
  22. data/lib/sequel_model/association_reflection.rb +251 -141
  23. data/lib/sequel_model/associations.rb +114 -61
  24. data/lib/sequel_model/base.rb +25 -21
  25. data/lib/sequel_model/eager_loading.rb +17 -40
  26. data/lib/sequel_model/hooks.rb +25 -24
  27. data/lib/sequel_model/record.rb +29 -51
  28. data/lib/sequel_model/schema.rb +1 -1
  29. data/lib/sequel_model/validations.rb +13 -3
  30. data/spec/adapters/postgres_spec.rb +104 -18
  31. data/spec/adapters/spec_helper.rb +4 -1
  32. data/spec/integration/eager_loader_test.rb +5 -4
  33. data/spec/integration/spec_helper.rb +4 -1
  34. data/spec/sequel_core/connection_pool_spec.rb +24 -24
  35. data/spec/sequel_core/core_sql_spec.rb +12 -0
  36. data/spec/sequel_core/dataset_spec.rb +77 -2
  37. data/spec/sequel_core/expression_filters_spec.rb +6 -0
  38. data/spec/sequel_core/object_graph_spec.rb +40 -2
  39. data/spec/sequel_core/schema_spec.rb +13 -0
  40. data/spec/sequel_model/association_reflection_spec.rb +8 -8
  41. data/spec/sequel_model/associations_spec.rb +164 -3
  42. data/spec/sequel_model/caching_spec.rb +2 -1
  43. data/spec/sequel_model/eager_loading_spec.rb +107 -3
  44. data/spec/sequel_model/hooks_spec.rb +38 -22
  45. data/spec/sequel_model/model_spec.rb +11 -35
  46. data/spec/sequel_model/plugins_spec.rb +4 -2
  47. data/spec/sequel_model/record_spec.rb +8 -5
  48. data/spec/sequel_model/validations_spec.rb +25 -0
  49. data/spec/spec_config.rb +4 -3
  50. 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
- join_type = r[:graph_join_type]
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] = assoc_type
156
- ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
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.reject{|k,v| v == :many_to_one}.length > 1
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] == :many_to_one ? nil : [] unless current.associations.include?(assoc_name)
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 assoc_type = type_map[ta]
301
- when :many_to_one
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 assoc_type == :one_to_many and reciprocal = reciprocal_map[ta]
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] == :many_to_one
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 do |reflection|
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 |r|
379
- keys.each do |key|
380
- ((key_hash[key][r[key]] ||= []) << r) if r[key]
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{|r| r[:eager_loader].call(key_hash, a, eager_assoc[r[:name]])}
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,
@@ -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 the model class or any of its ancestors have defined
12
- # hooks for the given hook key. Notice that this method cannot detect
13
- # hooks defined using overridden methods.
14
- def self.has_hooks?(key)
15
- has = hooks[key] && !hooks[key].empty?
16
- has || ((self != Model) && superclass.has_hooks?(key))
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
- # Returns all hook methods for the given type of hook for this
38
- # model class and its ancestors.
39
- def self.all_hooks(hook) # :nodoc:
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
- # Runs all hooks of type hook on the given object.
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
- private_class_method :add_hook, :all_hooks, :hooks, :run_hooks
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
- instance_eval("def #{hook}(method = nil, &block); add_hook(:#{hook}, method, &block) end")
58
- define_method(hook){model.send(:run_hooks, hook, self)}
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
@@ -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 = nil, from_db = false, &block)
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
- case opts[:type]
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
- when :one_to_many
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.single_associated_object?
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
- case opts[:type]
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
- when :one_to_many
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 = @raise_on_save_failure
463
- raise_error = true if reflection[:type] == :many_to_one
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 = @raise_on_save_failure if raise_error.nil?
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
- strict_param_setting = @strict_param_setting
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 strict_param_setting
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 @typecast_on_assignment && @db_schema && (col_schema = @db_schema[column])
539
- value = nil if value == '' and @typecast_empty_string_to_nil and col_schema[:type] and ![:string, :blob].include?(col_schema[:type])
540
- raise(Error::InvalidValue, "nil/NULL is not allowed for the #{column} column") if @raise_on_typecast_failure && value.nil? && (col_schema[:allow_null] == false)
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 @raise_on_typecast_failure
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)
@@ -3,7 +3,7 @@ module Sequel
3
3
  # Creates table, using the column information from set_schema.
4
4
  def self.create_table
5
5
  db.create_table(table_name, @schema)
6
- @db_schema = get_db_schema(true) unless @@lazy_load_schema
6
+ @db_schema = get_db_schema(true)
7
7
  columns
8
8
  end
9
9
 
@@ -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. You should also add a unique index in the
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
- full_text_index [:title]
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
- # tsearch is by default included from PostgreSQL 8.3
510
- ds = POSTGRES_DB[:test6]
511
- record = {:title => "oopsla conference"}
512
- ds << record
513
- actual = ds.full_text_search(:title, "oopsla")
514
- actual.should include(record)
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