sequel 3.38.0 → 3.39.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 (79) hide show
  1. data/CHANGELOG +62 -0
  2. data/README.rdoc +2 -2
  3. data/bin/sequel +12 -2
  4. data/doc/advanced_associations.rdoc +1 -1
  5. data/doc/association_basics.rdoc +13 -0
  6. data/doc/release_notes/3.39.0.txt +237 -0
  7. data/doc/schema_modification.rdoc +4 -4
  8. data/lib/sequel/adapters/jdbc/derby.rb +1 -0
  9. data/lib/sequel/adapters/mock.rb +5 -0
  10. data/lib/sequel/adapters/mysql.rb +8 -1
  11. data/lib/sequel/adapters/mysql2.rb +10 -3
  12. data/lib/sequel/adapters/postgres.rb +72 -8
  13. data/lib/sequel/adapters/shared/db2.rb +1 -0
  14. data/lib/sequel/adapters/shared/mssql.rb +57 -0
  15. data/lib/sequel/adapters/shared/mysql.rb +95 -19
  16. data/lib/sequel/adapters/shared/oracle.rb +14 -0
  17. data/lib/sequel/adapters/shared/postgres.rb +63 -24
  18. data/lib/sequel/adapters/shared/sqlite.rb +6 -9
  19. data/lib/sequel/connection_pool/sharded_threaded.rb +8 -3
  20. data/lib/sequel/connection_pool/threaded.rb +9 -4
  21. data/lib/sequel/database/query.rb +60 -48
  22. data/lib/sequel/database/schema_generator.rb +13 -6
  23. data/lib/sequel/database/schema_methods.rb +65 -12
  24. data/lib/sequel/dataset/actions.rb +22 -4
  25. data/lib/sequel/dataset/features.rb +5 -0
  26. data/lib/sequel/dataset/graph.rb +2 -3
  27. data/lib/sequel/dataset/misc.rb +2 -2
  28. data/lib/sequel/dataset/query.rb +0 -2
  29. data/lib/sequel/dataset/sql.rb +33 -12
  30. data/lib/sequel/extensions/constraint_validations.rb +451 -0
  31. data/lib/sequel/extensions/eval_inspect.rb +17 -2
  32. data/lib/sequel/extensions/pg_array_ops.rb +15 -5
  33. data/lib/sequel/extensions/pg_interval.rb +2 -2
  34. data/lib/sequel/extensions/pg_row_ops.rb +18 -0
  35. data/lib/sequel/extensions/schema_dumper.rb +3 -11
  36. data/lib/sequel/model/associations.rb +3 -2
  37. data/lib/sequel/model/base.rb +57 -13
  38. data/lib/sequel/model/exceptions.rb +20 -2
  39. data/lib/sequel/plugins/constraint_validations.rb +198 -0
  40. data/lib/sequel/plugins/defaults_setter.rb +15 -1
  41. data/lib/sequel/plugins/dirty.rb +2 -2
  42. data/lib/sequel/plugins/identity_map.rb +12 -8
  43. data/lib/sequel/plugins/subclasses.rb +19 -1
  44. data/lib/sequel/plugins/tree.rb +3 -3
  45. data/lib/sequel/plugins/validation_helpers.rb +24 -4
  46. data/lib/sequel/sql.rb +64 -24
  47. data/lib/sequel/timezones.rb +10 -2
  48. data/lib/sequel/version.rb +1 -1
  49. data/spec/adapters/mssql_spec.rb +26 -25
  50. data/spec/adapters/mysql_spec.rb +57 -23
  51. data/spec/adapters/oracle_spec.rb +34 -49
  52. data/spec/adapters/postgres_spec.rb +226 -128
  53. data/spec/adapters/sqlite_spec.rb +50 -49
  54. data/spec/core/connection_pool_spec.rb +22 -0
  55. data/spec/core/database_spec.rb +53 -47
  56. data/spec/core/dataset_spec.rb +36 -32
  57. data/spec/core/expression_filters_spec.rb +14 -2
  58. data/spec/core/mock_adapter_spec.rb +4 -0
  59. data/spec/core/object_graph_spec.rb +0 -13
  60. data/spec/core/schema_spec.rb +64 -5
  61. data/spec/core_extensions_spec.rb +1 -0
  62. data/spec/extensions/constraint_validations_plugin_spec.rb +196 -0
  63. data/spec/extensions/constraint_validations_spec.rb +316 -0
  64. data/spec/extensions/defaults_setter_spec.rb +24 -0
  65. data/spec/extensions/eval_inspect_spec.rb +9 -0
  66. data/spec/extensions/identity_map_spec.rb +11 -2
  67. data/spec/extensions/pg_array_ops_spec.rb +9 -0
  68. data/spec/extensions/pg_row_ops_spec.rb +11 -1
  69. data/spec/extensions/pg_row_plugin_spec.rb +4 -0
  70. data/spec/extensions/schema_dumper_spec.rb +8 -5
  71. data/spec/extensions/subclasses_spec.rb +14 -0
  72. data/spec/extensions/validation_helpers_spec.rb +15 -2
  73. data/spec/integration/dataset_test.rb +75 -1
  74. data/spec/integration/plugin_test.rb +146 -0
  75. data/spec/integration/schema_test.rb +34 -0
  76. data/spec/model/dataset_methods_spec.rb +38 -0
  77. data/spec/model/hooks_spec.rb +6 -0
  78. data/spec/model/validations_spec.rb +27 -2
  79. metadata +8 -2
@@ -21,6 +21,11 @@ module Sequel
21
21
  # The default options for join table columns.
22
22
  DEFAULT_JOIN_TABLE_COLUMN_OPTIONS = {:null=>false}
23
23
 
24
+ # The alter table operations that are combinable.
25
+ COMBINABLE_ALTER_TABLE_OPS = [:add_column, :drop_column, :rename_column,
26
+ :set_column_type, :set_column_default, :set_column_null,
27
+ :add_constraint, :drop_constraint]
28
+
24
29
  # Adds a column to the specified table. This method expects a column name,
25
30
  # a datatype and optionally a hash with additional constraints and options:
26
31
  #
@@ -70,7 +75,7 @@ module Sequel
70
75
  def alter_table(name, generator=nil, &block)
71
76
  generator ||= alter_table_generator(&block)
72
77
  remove_cached_schema(name)
73
- apply_alter_table(name, generator.operations)
78
+ apply_alter_table_generator(name, generator)
74
79
  nil
75
80
  end
76
81
 
@@ -333,17 +338,21 @@ module Sequel
333
338
 
334
339
  # Apply the changes in the given alter table ops to the table given by name.
335
340
  def apply_alter_table(name, ops)
336
- alter_table_sql_list(name, ops).flatten.each{|sql| execute_ddl(sql)}
341
+ alter_table_sql_list(name, ops).each{|sql| execute_ddl(sql)}
337
342
  end
338
343
 
344
+ # Apply the operations in the given generator to the table given by name.
345
+ def apply_alter_table_generator(name, generator)
346
+ apply_alter_table(name, generator.operations)
347
+ end
348
+
339
349
  # The class used for alter_table generators.
340
350
  def alter_table_generator_class
341
351
  Schema::AlterTableGenerator
342
352
  end
343
353
 
344
- # The SQL to execute to modify the DDL for the given table name. op
345
- # should be one of the operations returned by the AlterTableGenerator.
346
- def alter_table_sql(table, op)
354
+ # SQL fragment for given alter table operation.
355
+ def alter_table_op_sql(table, op)
347
356
  quoted_name = quote_identifier(op[:name]) if op[:name]
348
357
  alter_table_op = case op[:op]
349
358
  when :add_column
@@ -358,24 +367,56 @@ module Sequel
358
367
  "ALTER COLUMN #{quoted_name} SET DEFAULT #{literal(op[:default])}"
359
368
  when :set_column_null
360
369
  "ALTER COLUMN #{quoted_name} #{op[:null] ? 'DROP' : 'SET'} NOT NULL"
361
- when :add_index
362
- return index_definition_sql(table, op)
363
- when :drop_index
364
- return drop_index_sql(table, op)
365
370
  when :add_constraint
366
371
  "ADD #{constraint_definition_sql(op)}"
367
372
  when :drop_constraint
368
373
  "DROP CONSTRAINT #{quoted_name}#{' CASCADE' if op[:cascade]}"
369
374
  else
370
- raise Error, "Unsupported ALTER TABLE operation"
375
+ raise Error, "Unsupported ALTER TABLE operation: #{op[:op]}"
376
+ end
377
+ end
378
+
379
+ # The SQL to execute to modify the DDL for the given table name. op
380
+ # should be one of the operations returned by the AlterTableGenerator.
381
+ def alter_table_sql(table, op)
382
+ case op[:op]
383
+ when :add_index
384
+ index_definition_sql(table, op)
385
+ when :drop_index
386
+ drop_index_sql(table, op)
387
+ else
388
+ "ALTER TABLE #{quote_schema_table(table)} #{alter_table_op_sql(table, op)}"
371
389
  end
372
- "ALTER TABLE #{quote_schema_table(table)} #{alter_table_op}"
373
390
  end
374
391
 
375
392
  # Array of SQL DDL modification statements for the given table,
376
393
  # corresponding to the DDL changes specified by the operations.
377
394
  def alter_table_sql_list(table, operations)
378
- operations.map{|op| alter_table_sql(table, op)}
395
+ if supports_combining_alter_table_ops?
396
+ grouped_ops = []
397
+ last_combinable = false
398
+ operations.each do |op|
399
+ if combinable_alter_table_op?(op)
400
+ if sql = alter_table_op_sql(table, op)
401
+ grouped_ops << [] unless last_combinable
402
+ grouped_ops.last << sql
403
+ last_combinable = true
404
+ end
405
+ elsif sql = alter_table_sql(table, op)
406
+ grouped_ops << sql
407
+ last_combinable = false
408
+ end
409
+ end
410
+ grouped_ops.map do |gop|
411
+ if gop.is_a?(Array)
412
+ "ALTER TABLE #{quote_schema_table(table)} #{gop.join(', ')}"
413
+ else
414
+ gop
415
+ end
416
+ end
417
+ else
418
+ operations.map{|op| alter_table_sql(table, op)}.flatten.compact
419
+ end
379
420
  end
380
421
 
381
422
  # The SQL string specify the autoincrement property, generally used by
@@ -458,6 +499,12 @@ module Sequel
458
499
  "FOREIGN KEY #{literal(constraint[:columns])}#{column_references_sql(constraint)}"
459
500
  end
460
501
 
502
+ # Whether the given alter table operation is combinable.
503
+ def combinable_alter_table_op?(op)
504
+ # Use a dynamic lookup for easier overriding in adapters
505
+ COMBINABLE_ALTER_TABLE_OPS.include?(op[:op])
506
+ end
507
+
461
508
  # SQL DDL fragment specifying a constraint on a table.
462
509
  def constraint_definition_sql(constraint)
463
510
  sql = constraint[:name] ? "CONSTRAINT #{quote_identifier(constraint[:name])} " : ""
@@ -651,6 +698,12 @@ module Sequel
651
698
  @schema_utility_dataset ||= dataset
652
699
  end
653
700
 
701
+ # Whether the database supports combining multiple alter table
702
+ # operations into a single query, false by default.
703
+ def supports_combining_alter_table_ops?
704
+ false
705
+ end
706
+
654
707
  # SQL DDL fragment for temporary table
655
708
  def temporary_table_sql
656
709
  self.class.const_get(:TEMPORARY)
@@ -93,12 +93,30 @@ module Sequel
93
93
  columns
94
94
  end
95
95
 
96
- # Returns the number of records in the dataset.
96
+ # Returns the number of records in the dataset. If an argument is provided,
97
+ # it is used as the argument to count. If a block is provided, it is
98
+ # treated as a virtual row, and the result is used as the argument to
99
+ # count.
97
100
  #
98
101
  # DB[:table].count # SELECT COUNT(*) AS count FROM table LIMIT 1
99
102
  # # => 3
100
- def count
101
- aggregate_dataset.get{COUNT(:*){}.as(count)}.to_i
103
+ # DB[:table].count(:column) # SELECT COUNT(column) AS count FROM table LIMIT 1
104
+ # # => 2
105
+ # DB[:table].count{foo(column)} # SELECT COUNT(foo(column)) AS count FROM table LIMIT 1
106
+ # # => 1
107
+ def count(arg=(no_arg=true), &block)
108
+ if no_arg
109
+ if block
110
+ arg = Sequel.virtual_row(&block)
111
+ aggregate_dataset.get{COUNT(arg).as(count)}
112
+ else
113
+ aggregate_dataset.get{COUNT(:*){}.as(count)}.to_i
114
+ end
115
+ elsif block
116
+ raise Error, 'cannot provide both argument and block to Dataset#count'
117
+ else
118
+ aggregate_dataset.get{COUNT(arg).as(count)}
119
+ end
102
120
  end
103
121
 
104
122
  # Deletes the records in the dataset. The returned value should be
@@ -311,7 +329,7 @@ module Sequel
311
329
  # # INSERT INTO table (x) VALUES (1)
312
330
  # # INSERT INTO table (x) VALUES (2)
313
331
  #
314
- # DB[:table].insert_multiple([{:x=>1}, {:x=>2}]){|row| row[:y] = row[:x] * 2}
332
+ # DB[:table].insert_multiple([{:x=>1}, {:x=>2}]){|row| row[:y] = row[:x] * 2; row }
315
333
  # # => [6, 7]
316
334
  # # INSERT INTO table (x, y) VALUES (1, 2)
317
335
  # # INSERT INTO table (x, y) VALUES (2, 4)
@@ -114,6 +114,11 @@ module Sequel
114
114
  supports_distinct_on?
115
115
  end
116
116
 
117
+ # Whether the dataset supports pattern matching by regular expressions.
118
+ def supports_regexp?
119
+ false
120
+ end
121
+
117
122
  # Whether the RETURNING clause is supported for the given type of query.
118
123
  # +type+ can be :insert, :update, or :delete.
119
124
  def supports_returning?(type)
@@ -73,9 +73,8 @@ module Sequel
73
73
  # alias the table. You will get an error if the the alias (or table) name is
74
74
  # used more than once.
75
75
  def graph(dataset, join_conditions = nil, options = {}, &block)
76
- # Allow the use of a model, dataset, or symbol as the first argument
76
+ # Allow the use of a dataset or symbol as the first argument
77
77
  # Find the table name/dataset based on the argument
78
- dataset = dataset.dataset if dataset.respond_to?(:dataset)
79
78
  table_alias = options[:table_alias]
80
79
  case dataset
81
80
  when Symbol
@@ -91,7 +90,7 @@ module Sequel
91
90
  table_alias ||= dataset_alias((@opts[:num_dataset_sources] || 0)+1)
92
91
  end
93
92
  else
94
- raise Error, "The dataset argument should be a symbol, dataset, or model"
93
+ raise Error, "The dataset argument should be a symbol or dataset"
95
94
  end
96
95
 
97
96
  # Raise Sequel::Error with explanation that the table alias has been used
@@ -31,9 +31,9 @@ module Sequel
31
31
  end
32
32
 
33
33
  # Define a hash value such that datasets with the same DB, opts, and SQL
34
- # will be consider equal.
34
+ # will be considered equal.
35
35
  def ==(o)
36
- o.is_a?(self.class) && db == o.db && opts == o.opts && sql == o.sql
36
+ o.is_a?(self.class) && db == o.db && opts == o.opts && sql == o.sql
37
37
  end
38
38
 
39
39
  # Alias for ==
@@ -459,7 +459,6 @@ module Sequel
459
459
  # * type - The type of join to do (e.g. :inner)
460
460
  # * table - Depends on type:
461
461
  # * Dataset - a subselect is performed with an alias of tN for some value of N
462
- # * Model (or anything responding to :table_name) - table.table_name
463
462
  # * String, Symbol: table
464
463
  # * expr - specifies conditions, depends on type:
465
464
  # * Hash, Array of two element arrays - Assumes key (1st arg) is column of joined table (unless already
@@ -536,7 +535,6 @@ module Sequel
536
535
  end
537
536
  table_name = table_alias
538
537
  else
539
- table = table.table_name if table.respond_to?(:table_name)
540
538
  table, implicit_table_alias = split_alias(table)
541
539
  table_alias ||= implicit_table_alias
542
540
  table_name = table_alias || table
@@ -49,10 +49,6 @@ module Sequel
49
49
  end
50
50
  when Dataset, Array, LiteralString
51
51
  values = vals
52
- else
53
- if vals.respond_to?(:values) && (v = vals.values).is_a?(Hash)
54
- return insert_sql(v)
55
- end
56
52
  end
57
53
  when 2
58
54
  if (v0 = values.at(0)).is_a?(Array) && ((v1 = values.at(1)).is_a?(Array) || v1.is_a?(Dataset) || v1.is_a?(LiteralString))
@@ -182,6 +178,9 @@ module Sequel
182
178
  clauses.map{|clause| :"#{type}_#{clause}_sql"}.freeze
183
179
  end
184
180
 
181
+ # Map of emulated function names to native function names.
182
+ EMULATED_FUNCTION_MAP = {}
183
+
185
184
  WILDCARD = LiteralString.new('*').freeze
186
185
  ALL = ' ALL'.freeze
187
186
  AND_SEPARATOR = " AND ".freeze
@@ -272,6 +271,7 @@ module Sequel
272
271
  TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
273
272
  STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
274
273
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
274
+ REGEXP_OPERATORS = ::Sequel::SQL::ComplexExpression::REGEXP_OPERATORS
275
275
  UNDERSCORE = '_'.freeze
276
276
  UPDATE = 'UPDATE'.freeze
277
277
  UPDATE_CLAUSE_METHODS = clause_methods(:update, %w'update table set where')
@@ -456,6 +456,9 @@ module Sequel
456
456
  sql << PAREN_CLOSE
457
457
  end
458
458
  when *TWO_ARITY_OPERATORS
459
+ if REGEXP_OPERATORS.include?(op) && !supports_regexp?
460
+ raise InvalidOperation, "Pattern matching via regular expressions is not supported on #{db.database_type}"
461
+ end
459
462
  sql << PAREN_OPEN
460
463
  literal_append(sql, args.at(0))
461
464
  sql << SPACE << op.to_s << SPACE
@@ -493,15 +496,18 @@ module Sequel
493
496
  sql << constant.to_s
494
497
  end
495
498
 
496
- # SQL fragment specifying an SQL function call
499
+ # SQL fragment specifying an emulated SQL function call.
500
+ # By default, assumes just the function name may need to
501
+ # be emulated, adapters should set an EMULATED_FUNCTION_MAP
502
+ # hash mapping emulated functions to native functions in
503
+ # their dataset class to setup the emulation.
504
+ def emulated_function_sql_append(sql, f)
505
+ _function_sql_append(sql, native_function_name(f.f), f.args)
506
+ end
507
+
508
+ # SQL fragment specifying an SQL function call without emulation.
497
509
  def function_sql_append(sql, f)
498
- sql << f.f.to_s
499
- args = f.args
500
- if args.empty?
501
- sql << FUNCTION_EMPTY
502
- else
503
- literal_append(sql, args)
504
- end
510
+ _function_sql_append(sql, f.f, f.args)
505
511
  end
506
512
 
507
513
  # SQL fragment specifying a JOIN clause without ON or USING.
@@ -718,6 +724,16 @@ module Sequel
718
724
 
719
725
  private
720
726
 
727
+ # Backbone of function_sql_append and emulated_function_sql_append.
728
+ def _function_sql_append(sql, name, args)
729
+ sql << name.to_s
730
+ if args.empty?
731
+ sql << FUNCTION_EMPTY
732
+ else
733
+ literal_append(sql, args)
734
+ end
735
+ end
736
+
721
737
  # Formats the truncate statement. Assumes the table given has already been
722
738
  # literalized.
723
739
  def _truncate_sql(table)
@@ -1127,6 +1143,11 @@ module Sequel
1127
1143
  BOOL_TRUE
1128
1144
  end
1129
1145
 
1146
+ # Get the native function name given the emulated function name.
1147
+ def native_function_name(emulated_function)
1148
+ self.class.const_get(:EMULATED_FUNCTION_MAP).fetch(emulated_function, emulated_function)
1149
+ end
1150
+
1130
1151
  # Returns a qualified column name (including a table name) if the column
1131
1152
  # name isn't already qualified.
1132
1153
  def qualified_column_name(column, table)
@@ -0,0 +1,451 @@
1
+ # The constraint_validations extension is designed to easily create database
2
+ # constraints inside create_table and alter_table blocks. It also adds
3
+ # relevant metadata about the constraints to a separate table, which the
4
+ # constraint_validations model plugin uses to setup automatic validations.
5
+ #
6
+ # To use this extension, you first need to load it into the database:
7
+ #
8
+ # DB.extension(:constraint_validations)
9
+ #
10
+ # Note that you should only need to do this when modifying the constraint
11
+ # validations (i.e. when migrating). You should probably not load this
12
+ # extension in general application code.
13
+ #
14
+ # You also need to make sure to add the metadata table for the automatic
15
+ # validations. By default, this table is called sequel_constraint_validations.
16
+ #
17
+ # DB.create_constraint_validations_table
18
+ #
19
+ # This table should only be created once. For new applications, you
20
+ # generally want to create it first, before creating any other application
21
+ # tables.
22
+ #
23
+ # Because migrations instance_eval the up and down blocks on a database,
24
+ # using this extension in a migration can be done via:
25
+ #
26
+ # Sequel.migration do
27
+ # up do
28
+ # extension(:constraint_validations)
29
+ # # ...
30
+ # end
31
+ # down do
32
+ # extension(:constraint_validations)
33
+ # # ...
34
+ # end
35
+ # end
36
+ #
37
+ # However, note that you cannot use change migrations with this extension,
38
+ # you need to use separate up/down migrations.
39
+ #
40
+ # The API for creating the constraints with automatic validations is
41
+ # similar to the validation_helpers model plugin API. However,
42
+ # instead of having separate validates_* methods, it just adds a validate
43
+ # method that accepts a block to the schema generators. Like the
44
+ # create_table and alter_table blocks, this block is instance_evaled and
45
+ # offers its own DSL. Example:
46
+ #
47
+ # DB.create_table(:table) do
48
+ # Integer :id
49
+ # String :name
50
+ #
51
+ # validate do
52
+ # presence :id
53
+ # min_length 5, :name
54
+ # end
55
+ # end
56
+ #
57
+ # instance_eval is used in this case because create_table and alter_table
58
+ # already use instance_eval, so losing access to the surrounding receiver
59
+ # is not an issue.
60
+ #
61
+ # Here's a breakdown of the constraints created for each constraint validation
62
+ # method:
63
+ #
64
+ # All constraints except unique unless :allow_nil is true :: CHECK column IS NOT NULL
65
+ # presence (String column) :: CHECK trim(column) != ''
66
+ # exact_length 5 :: CHECK char_length(column) = 5
67
+ # min_length 5 :: CHECK char_length(column) >= 5
68
+ # max_length 5 :: CHECK char_length(column) <= 5
69
+ # length_range 3..5 :: CHECK char_length(column) >= 3 AND char_length(column) <= 5
70
+ # length_range 3...5 :: CHECK char_length(column) >= 3 AND char_length(column) < 5
71
+ # format /foo\\d+/ :: CHECK column ~ 'foo\\d+'
72
+ # format /foo\\d+/i :: CHECK column ~* 'foo\\d+'
73
+ # like 'foo%' :: CHECK column LIKE 'foo%'
74
+ # ilike 'foo%' :: CHECK column ILIKE 'foo%'
75
+ # includes ['a', 'b'] :: CHECK column IN ('a', 'b')
76
+ # includes [1, 2] :: CHECK column IN (1, 2)
77
+ # includes 3..5 :: CHECK column >= 3 AND column <= 5
78
+ # includes 3...5 :: CHECK column >= 3 AND column < 5
79
+ # unique :: UNIQUE (column)
80
+ #
81
+ # There are some additional API differences:
82
+ #
83
+ # * Only the :message and :allow_nil options are respected. The :allow_blank
84
+ # and :allow_missing options are not respected.
85
+ # * A new option, :name, is respected, for providing the name of the constraint. It is highly
86
+ # recommended that you provide a name for all constraint validations, as
87
+ # otherwise, it is difficult to drop the constraints later.
88
+ # * The includes validation only supports an array of strings, and array of
89
+ # integers, and a range of integers.
90
+ # * There are like and ilike validations, which are similar to the format
91
+ # validation but use a case sensitive or case insensitive LIKE pattern. LIKE
92
+ # patters are very simple, so many regexp patterns cannot be expressed by
93
+ # them, but only a couple databases (PostgreSQL and MySQL) support regexp
94
+ # patterns.
95
+ # * When using the unique validation, column names cannot have embedded commas.
96
+ # For similar reasons, when using an includes validation with an array of
97
+ # strings, none of the strings in the array can have embedded commas.
98
+ # * The unique validation does not support an arbitrary number of columns.
99
+ # For a single column, just the symbol should be used, and for an array
100
+ # of columns, an array of symbols should be used. There is no support
101
+ # for creating two separate unique validations for separate columns in
102
+ # a single call.
103
+ # * A drop method can be called with a constraint name in a alter_table
104
+ # validate block to drop an existing constraint and the related
105
+ # validation metadata.
106
+ # * While it is allowed to create a presence constraint with :allow_nil
107
+ # set to true, doing so does not create a constraint unless the column
108
+ # has String type.
109
+ #
110
+ # Note that this extension has the following issues on certain databases:
111
+ #
112
+ # * MySQL does not support check constraints (they are parsed but ignored),
113
+ # so using this extension does not actually set up constraints on MySQL,
114
+ # except for the unique constraint. It can still be used on MySQL to
115
+ # add the validation metadata so that the plugin can setup automatic
116
+ # validations.
117
+ # * On SQLite, adding constraints to a table is not supported, so it must
118
+ # be emulated by dropping the table and recreating it with the constraints.
119
+ # If you want to use this plugin on SQLite with an alter_table block,
120
+ # you should drop all constraint validation metadata using
121
+ # <tt>drop_constraint_validations_for(:table=>'table')</tt>, and then
122
+ # readd all constraints you want to use inside the alter table block,
123
+ # making no other changes inside the alter_table block.
124
+
125
+ module Sequel
126
+ module ConstraintValidations
127
+ # The default table name used for the validation metadata.
128
+ DEFAULT_CONSTRAINT_VALIDATIONS_TABLE = :sequel_constraint_validations
129
+
130
+ # Set the default validation metadata table name if it has not already
131
+ # been set.
132
+ def self.extended(db)
133
+ db.constraint_validations_table ||= DEFAULT_CONSTRAINT_VALIDATIONS_TABLE
134
+ end
135
+
136
+ # This is the DSL class used for the validate block inside create_table and
137
+ # alter_table.
138
+ class Generator
139
+ # Store the schema generator that encloses this validates block.
140
+ def initialize(generator)
141
+ @generator = generator
142
+ end
143
+
144
+ # Create constraint validation methods that don't take an argument
145
+ %w'presence unique'.each do |v|
146
+ class_eval(<<-END, __FILE__, __LINE__+1)
147
+ def #{v}(columns, opts={})
148
+ @generator.validation({:type=>:#{v}, :columns=>Array(columns)}.merge(opts))
149
+ end
150
+ END
151
+ end
152
+
153
+ # Create constraint validation methods that take an argument
154
+ %w'exact_length min_length max_length length_range format like ilike includes'.each do |v|
155
+ class_eval(<<-END, __FILE__, __LINE__+1)
156
+ def #{v}(arg, columns, opts={})
157
+ @generator.validation({:type=>:#{v}, :columns=>Array(columns), :arg=>arg}.merge(opts))
158
+ end
159
+ END
160
+ end
161
+
162
+ # Given the name of a constraint, drop that constraint from the database,
163
+ # and remove the related validation metadata.
164
+ def drop(constraint)
165
+ @generator.validation({:type=>:drop, :name=>constraint})
166
+ end
167
+
168
+ # Alias of instance_eval for a nicer API.
169
+ def process(&block)
170
+ instance_eval(&block)
171
+ end
172
+ end
173
+
174
+ # Additional methods for the create_table generator to support constraint validations.
175
+ module CreateTableGeneratorMethods
176
+ # An array of stored validation metadata, used later by the database to create
177
+ # constraints.
178
+ attr_reader :validations
179
+
180
+ # Add a validation metadata hash to the stored array.
181
+ def validation(opts)
182
+ @validations << opts
183
+ end
184
+
185
+ # Call into the validate DSL for creating constraint validations.
186
+ def validate(&block)
187
+ Generator.new(self).process(&block)
188
+ end
189
+ end
190
+
191
+ # Additional methods for the alter_table generator to support constraint validations,
192
+ # used to give it a more similar API to the create_table generator.
193
+ module AlterTableGeneratorMethods
194
+ include CreateTableGeneratorMethods
195
+
196
+ # Alias of add_constraint for similarity to create_table generator.
197
+ def constraint(*args)
198
+ add_constraint(*args)
199
+ end
200
+
201
+ # Alias of add_unique_constraint for similarity to create_table generator.
202
+ def unique(*args)
203
+ add_unique_constraint(*args)
204
+ end
205
+ end
206
+
207
+ # The name of the table storing the validation metadata. If modifying this
208
+ # from the default, this should be changed directly after loading the
209
+ # extension into the database
210
+ attr_accessor :constraint_validations_table
211
+
212
+ # Create the table storing the validation metadata for all of the
213
+ # constraints created by this extension.
214
+ def create_constraint_validations_table
215
+ create_table(constraint_validations_table) do
216
+ String :table, :null=>false
217
+ String :constraint_name
218
+ String :validation_type, :null=>false
219
+ String :column, :null=>false
220
+ String :argument
221
+ String :message
222
+ TrueClass :allow_nil
223
+ end
224
+ end
225
+
226
+ # Drop the constraint validations table.
227
+ def drop_constraint_validations_table
228
+ drop_table(constraint_validations_table)
229
+ end
230
+
231
+ # Delete validation metadata for specific constraints. At least
232
+ # one of the following options should be specified:
233
+ #
234
+ # :table :: The table containing the constraint
235
+ # :column :: The column affected by the constraint
236
+ # :constraint :: The name of the related constraint
237
+ #
238
+ # The main reason for this method is when dropping tables
239
+ # or columns. If you have previously defined a constraint
240
+ # validation on the table or column, you should delete the
241
+ # related metadata when dropping the table or column.
242
+ # For a table, this isn't a big issue, as it will just result
243
+ # in some wasted space, but for columns, if you don't drop
244
+ # the related metadata, it could make it impossible to save
245
+ # rows, since a validation for a nonexistent column will be
246
+ # created.
247
+ def drop_constraint_validations_for(opts={})
248
+ ds = from(constraint_validations_table)
249
+ if table = opts[:table]
250
+ ds = ds.where(:table=>constraint_validations_literal_table(table))
251
+ end
252
+ if column = opts[:column]
253
+ ds = ds.where(:column=>column.to_s)
254
+ end
255
+ if constraint = opts[:constraint]
256
+ ds = ds.where(:constraint_name=>constraint.to_s)
257
+ end
258
+ unless table || column || constraint
259
+ raise Error, "must specify :table, :column, or :constraint when dropping constraint validations"
260
+ end
261
+ ds.delete
262
+ end
263
+
264
+ private
265
+
266
+ # Modify the default create_table generator to include
267
+ # the constraint validation methods.
268
+ def alter_table_generator(&block)
269
+ super do
270
+ extend AlterTableGeneratorMethods
271
+ @validations = []
272
+ instance_eval(&block) if block
273
+ end
274
+ end
275
+
276
+ # After running all of the table alteration statements,
277
+ # if there were any constraint validations, run table alteration
278
+ # statements to create related constraints. This is purposely
279
+ # run after the other statements, as the presence validation
280
+ # in alter table requires introspecting the modified model
281
+ # schema.
282
+ def apply_alter_table_generator(name, generator)
283
+ super
284
+ unless generator.validations.empty?
285
+ gen = alter_table_generator
286
+ process_generator_validations(name, gen, generator.validations)
287
+ apply_alter_table(name, gen.operations)
288
+ end
289
+ end
290
+
291
+ # The value of a blank string. An empty string by default, but nil
292
+ # on Oracle as Oracle treats the empty string as NULL.
293
+ def blank_string_value
294
+ if database_type == :oracle
295
+ nil
296
+ else
297
+ ''
298
+ end
299
+ end
300
+
301
+ # Return an unquoted literal form of the table name.
302
+ # This allows the code to handle schema qualified tables,
303
+ # without quoting all table names.
304
+ def constraint_validations_literal_table(table)
305
+ ds = dataset
306
+ ds.quote_identifiers = false
307
+ ds.literal(table)
308
+ end
309
+
310
+ # Before creating the table, add constraints for all of the
311
+ # generators validations to the generator.
312
+ def create_table_from_generator(name, generator, options)
313
+ unless generator.validations.empty?
314
+ process_generator_validations(name, generator, generator.validations)
315
+ end
316
+ super
317
+ end
318
+
319
+ # Modify the default create_table generator to include
320
+ # the constraint validation methods.
321
+ def create_table_generator(&block)
322
+ super do
323
+ extend CreateTableGeneratorMethods
324
+ @validations = []
325
+ instance_eval(&block) if block
326
+ end
327
+ end
328
+
329
+ # For the given table, generator, and validations, add constraints
330
+ # to the generator for each of the validations, as well as adding
331
+ # validation metadata to the constraint validations table.
332
+ def process_generator_validations(table, generator, validations)
333
+ drop_rows = []
334
+ rows = validations.map do |val|
335
+ columns, arg, constraint, validation_type, message, allow_nil = val.values_at(:columns, :arg, :name, :type, :message, :allow_nil)
336
+
337
+ case validation_type
338
+ when :presence
339
+ string_check = columns.select{|c| generator_string_column?(generator, table, c)}.map{|c| [Sequel.trim(c), blank_string_value]}
340
+ generator_add_constraint_from_validation(generator, val, (Sequel.negate(string_check) unless string_check.empty?))
341
+ when :exact_length
342
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {Sequel.char_length(c) => arg}}))
343
+ when :min_length
344
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) >= arg}))
345
+ when :max_length
346
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.char_length(c) <= arg}))
347
+ when :length_range
348
+ op = arg.exclude_end? ? :< : :<=
349
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| (Sequel.char_length(c) >= arg.begin) & Sequel.char_length(c).send(op, arg.end)}))
350
+ arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}"
351
+ when :format
352
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {c => arg}}))
353
+ if arg.casefold?
354
+ validation_type = :iformat
355
+ end
356
+ arg = arg.source
357
+ when :includes
358
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| {c => arg}}))
359
+ if arg.is_a?(Range)
360
+ if (b = arg.begin).is_a?(Integer) && (e = arg.end).is_a?(Integer)
361
+ validation_type = :includes_int_range
362
+ arg = "#{arg.begin}..#{'.' if arg.exclude_end?}#{arg.end}"
363
+ else
364
+ raise Error, "validates includes with a range only supports integers currently, cannot handle: #{arg.inspect}"
365
+ end
366
+ elsif arg.is_a?(Array)
367
+ if arg.all?{|x| x.is_a?(Integer)}
368
+ validation_type = :includes_int_array
369
+ elsif arg.all?{|x| x.is_a?(String)}
370
+ validation_type = :includes_str_array
371
+ else
372
+ raise Error, "validates includes with an array only supports strings and integers currently, cannot handle: #{arg.inspect}"
373
+ end
374
+ arg = arg.join(',')
375
+ else
376
+ raise Error, "validates includes only supports arrays and ranges currently, cannot handle: #{arg.inspect}"
377
+ end
378
+ when :like, :ilike
379
+ generator_add_constraint_from_validation(generator, val, Sequel.&(*columns.map{|c| Sequel.send(validation_type, c, arg)}))
380
+ when :unique
381
+ generator.unique(columns, :name=>constraint)
382
+ columns = [columns.join(',')]
383
+ when :drop
384
+ if generator.is_a?(Sequel::Schema::AlterTableGenerator)
385
+ unless constraint
386
+ raise Error, 'cannot drop a constraint validation without a constraint name'
387
+ end
388
+ generator.drop_constraint(constraint)
389
+ drop_rows << [constraint_validations_literal_table(table), constraint.to_s]
390
+ columns = []
391
+ else
392
+ raise Error, 'cannot drop a constraint validation in a create_table generator'
393
+ end
394
+ else
395
+ raise Error, "invalid or missing validation type: #{val.inspect}"
396
+ end
397
+
398
+ columns.map do |column|
399
+ {:table=>constraint_validations_literal_table(table), :constraint_name=>(constraint.to_s if constraint), :validation_type=>validation_type.to_s, :column=>column.to_s, :argument=>(arg.to_s if arg), :message=>(message.to_s if message), :allow_nil=>allow_nil}
400
+ end
401
+ end
402
+
403
+ ds = from(:sequel_constraint_validations)
404
+ ds.multi_insert(rows.flatten)
405
+ unless drop_rows.empty?
406
+ ds.where([:table, :constraint_name]=>drop_rows).delete
407
+ end
408
+ end
409
+
410
+ # Add the constraint to the generator, including a NOT NULL constraint
411
+ # for all columns unless the :allow_nil option is given.
412
+ def generator_add_constraint_from_validation(generator, val, cons)
413
+ unless val[:allow_nil]
414
+ nil_cons = Sequel.negate(val[:columns].map{|c| [c, nil]})
415
+ cons = cons ? Sequel.&(nil_cons, cons) : nil_cons
416
+ end
417
+
418
+ if cons
419
+ generator.constraint(val[:name], cons)
420
+ end
421
+ end
422
+
423
+
424
+ # Introspect the generator to determine if column
425
+ # created is a string or not.
426
+ def generator_string_column?(generator, table, c)
427
+ if generator.is_a?(Sequel::Schema::AlterTableGenerator)
428
+ # This is the alter table case, which runs after the
429
+ # table has been altered, so just check the database
430
+ # schema for the column.
431
+ schema(table).each do |col, sch|
432
+ if col == c
433
+ return sch[:type] == :string
434
+ end
435
+ end
436
+ false
437
+ else
438
+ # This is the create table case, check the metadata
439
+ # for the column to be created to see if it is a string.
440
+ generator.columns.each do |col|
441
+ if col[:name] == c
442
+ return [String, :text, :varchar].include?(col[:type])
443
+ end
444
+ end
445
+ false
446
+ end
447
+ end
448
+ end
449
+
450
+ Database.register_extension(:constraint_validations, ConstraintValidations)
451
+ end