sequel 3.38.0 → 3.39.0

Sign up to get free protection for your applications and to get access to all the features.
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