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