sequel 3.36.1 → 3.37.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) hide show
  1. data/CHANGELOG +84 -0
  2. data/Rakefile +13 -0
  3. data/bin/sequel +12 -16
  4. data/doc/advanced_associations.rdoc +36 -67
  5. data/doc/association_basics.rdoc +11 -16
  6. data/doc/release_notes/3.37.0.txt +338 -0
  7. data/doc/schema_modification.rdoc +4 -0
  8. data/lib/sequel/adapters/jdbc/h2.rb +1 -1
  9. data/lib/sequel/adapters/jdbc/postgresql.rb +26 -8
  10. data/lib/sequel/adapters/mysql2.rb +4 -3
  11. data/lib/sequel/adapters/odbc/mssql.rb +2 -2
  12. data/lib/sequel/adapters/postgres.rb +4 -60
  13. data/lib/sequel/adapters/shared/mssql.rb +2 -1
  14. data/lib/sequel/adapters/shared/mysql.rb +0 -5
  15. data/lib/sequel/adapters/shared/postgres.rb +68 -2
  16. data/lib/sequel/adapters/shared/sqlite.rb +17 -1
  17. data/lib/sequel/adapters/utils/emulate_offset_with_row_number.rb +12 -1
  18. data/lib/sequel/adapters/utils/pg_types.rb +76 -0
  19. data/lib/sequel/core.rb +13 -0
  20. data/lib/sequel/database/misc.rb +41 -1
  21. data/lib/sequel/database/schema_generator.rb +23 -10
  22. data/lib/sequel/database/schema_methods.rb +26 -4
  23. data/lib/sequel/dataset/graph.rb +2 -1
  24. data/lib/sequel/dataset/query.rb +62 -2
  25. data/lib/sequel/extensions/_pretty_table.rb +7 -3
  26. data/lib/sequel/extensions/arbitrary_servers.rb +5 -4
  27. data/lib/sequel/extensions/blank.rb +4 -0
  28. data/lib/sequel/extensions/columns_introspection.rb +13 -2
  29. data/lib/sequel/extensions/core_extensions.rb +6 -0
  30. data/lib/sequel/extensions/eval_inspect.rb +158 -0
  31. data/lib/sequel/extensions/inflector.rb +4 -0
  32. data/lib/sequel/extensions/looser_typecasting.rb +5 -4
  33. data/lib/sequel/extensions/migration.rb +4 -1
  34. data/lib/sequel/extensions/named_timezones.rb +4 -0
  35. data/lib/sequel/extensions/null_dataset.rb +4 -0
  36. data/lib/sequel/extensions/pagination.rb +4 -0
  37. data/lib/sequel/extensions/pg_array.rb +219 -168
  38. data/lib/sequel/extensions/pg_array_ops.rb +7 -2
  39. data/lib/sequel/extensions/pg_auto_parameterize.rb +10 -4
  40. data/lib/sequel/extensions/pg_hstore.rb +3 -1
  41. data/lib/sequel/extensions/pg_hstore_ops.rb +7 -2
  42. data/lib/sequel/extensions/pg_inet.rb +28 -3
  43. data/lib/sequel/extensions/pg_interval.rb +192 -0
  44. data/lib/sequel/extensions/pg_json.rb +21 -9
  45. data/lib/sequel/extensions/pg_range.rb +487 -0
  46. data/lib/sequel/extensions/pg_range_ops.rb +122 -0
  47. data/lib/sequel/extensions/pg_statement_cache.rb +3 -2
  48. data/lib/sequel/extensions/pretty_table.rb +12 -1
  49. data/lib/sequel/extensions/query.rb +4 -0
  50. data/lib/sequel/extensions/query_literals.rb +6 -6
  51. data/lib/sequel/extensions/schema_dumper.rb +39 -38
  52. data/lib/sequel/extensions/select_remove.rb +4 -0
  53. data/lib/sequel/extensions/server_block.rb +3 -2
  54. data/lib/sequel/extensions/split_array_nil.rb +65 -0
  55. data/lib/sequel/extensions/sql_expr.rb +4 -0
  56. data/lib/sequel/extensions/string_date_time.rb +4 -0
  57. data/lib/sequel/extensions/thread_local_timezones.rb +9 -3
  58. data/lib/sequel/extensions/to_dot.rb +4 -0
  59. data/lib/sequel/model/associations.rb +150 -91
  60. data/lib/sequel/plugins/identity_map.rb +2 -2
  61. data/lib/sequel/plugins/list.rb +1 -0
  62. data/lib/sequel/plugins/many_through_many.rb +33 -32
  63. data/lib/sequel/plugins/nested_attributes.rb +11 -3
  64. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  65. data/lib/sequel/plugins/schema.rb +1 -1
  66. data/lib/sequel/sql.rb +14 -14
  67. data/lib/sequel/version.rb +2 -2
  68. data/spec/adapters/mysql_spec.rb +25 -0
  69. data/spec/adapters/postgres_spec.rb +572 -28
  70. data/spec/adapters/sqlite_spec.rb +16 -1
  71. data/spec/core/database_spec.rb +61 -2
  72. data/spec/core/dataset_spec.rb +92 -0
  73. data/spec/core/expression_filters_spec.rb +12 -0
  74. data/spec/extensions/arbitrary_servers_spec.rb +1 -1
  75. data/spec/extensions/boolean_readers_spec.rb +25 -25
  76. data/spec/extensions/eval_inspect_spec.rb +58 -0
  77. data/spec/extensions/json_serializer_spec.rb +0 -6
  78. data/spec/extensions/list_spec.rb +1 -1
  79. data/spec/extensions/looser_typecasting_spec.rb +7 -7
  80. data/spec/extensions/many_through_many_spec.rb +81 -0
  81. data/spec/extensions/nested_attributes_spec.rb +21 -4
  82. data/spec/extensions/pg_array_ops_spec.rb +1 -11
  83. data/spec/extensions/pg_array_spec.rb +181 -90
  84. data/spec/extensions/pg_auto_parameterize_spec.rb +3 -3
  85. data/spec/extensions/pg_hstore_spec.rb +1 -3
  86. data/spec/extensions/pg_inet_spec.rb +6 -1
  87. data/spec/extensions/pg_interval_spec.rb +73 -0
  88. data/spec/extensions/pg_json_spec.rb +5 -9
  89. data/spec/extensions/pg_range_ops_spec.rb +49 -0
  90. data/spec/extensions/pg_range_spec.rb +372 -0
  91. data/spec/extensions/pg_statement_cache_spec.rb +1 -2
  92. data/spec/extensions/query_literals_spec.rb +1 -2
  93. data/spec/extensions/schema_dumper_spec.rb +48 -89
  94. data/spec/extensions/serialization_spec.rb +1 -5
  95. data/spec/extensions/server_block_spec.rb +2 -2
  96. data/spec/extensions/spec_helper.rb +12 -2
  97. data/spec/extensions/split_array_nil_spec.rb +24 -0
  98. data/spec/integration/associations_test.rb +4 -4
  99. data/spec/integration/database_test.rb +2 -2
  100. data/spec/integration/dataset_test.rb +4 -4
  101. data/spec/integration/eager_loader_test.rb +6 -6
  102. data/spec/integration/plugin_test.rb +2 -2
  103. data/spec/integration/spec_helper.rb +2 -2
  104. data/spec/model/association_reflection_spec.rb +5 -0
  105. data/spec/model/associations_spec.rb +156 -49
  106. data/spec/model/eager_loading_spec.rb +137 -2
  107. data/spec/model/model_spec.rb +10 -10
  108. metadata +15 -2
@@ -0,0 +1,122 @@
1
+ # The pg_range_ops extension adds support to Sequel's DSL to make
2
+ # it easier to call PostgreSQL range functions and operators.
3
+ #
4
+ # To load the extension:
5
+ #
6
+ # Sequel.extension :pg_range_ops
7
+ #
8
+ # The most common usage is taking an object that represents an SQL
9
+ # identifier (such as a :symbol), and calling #pg_range on it:
10
+ #
11
+ # r = :range.pg_range
12
+ #
13
+ # This creates a Sequel::Postgres::RangeOp object that can be used
14
+ # for easier querying:
15
+ #
16
+ # r.contains(:other) # range @> other
17
+ # r.contained_by(:other) # range <@ other
18
+ # r.overlaps(:other) # range && other
19
+ # r.left_of(:other) # range << other
20
+ # r.right_of(:other) # range >> other
21
+ # r.starts_before(:other) # range &< other
22
+ # r.ends_after(:other) # range &> other
23
+ # r.adjacent_to(:other) # range -|- other
24
+ #
25
+ # r.lower # lower(range)
26
+ # r.upper # upper(range)
27
+ # r.isempty # isempty(range)
28
+ # r.lower_inc # lower_inc(range)
29
+ # r.upper_inc # upper_inc(range)
30
+ # r.lower_inf # lower_inf(range)
31
+ # r.upper_inf # upper_inf(range)
32
+ #
33
+ # See the PostgreSQL range function and operator documentation for more
34
+ # details on what these functions and operators do.
35
+ #
36
+ # If you are also using the pg_range extension, you should load it before
37
+ # loading this extension. Doing so will allow you to use PGArray#op to get
38
+ # an RangeOp, allowing you to perform range operations on range literals.
39
+
40
+ module Sequel
41
+ module Postgres
42
+ # The RangeOp class is a simple container for a single object that
43
+ # defines methods that yield Sequel expression objects representing
44
+ # PostgreSQL range operators and functions.
45
+ #
46
+ # Most methods in this class are defined via metaprogramming, see
47
+ # the pg_range_ops extension documentation for details on the API.
48
+ class RangeOp < Sequel::SQL::Wrapper
49
+ OPERATORS = {
50
+ :contains => ["(".freeze, " @> ".freeze, ")".freeze].freeze,
51
+ :contained_by => ["(".freeze, " <@ ".freeze, ")".freeze].freeze,
52
+ :left_of => ["(".freeze, " << ".freeze, ")".freeze].freeze,
53
+ :right_of => ["(".freeze, " >> ".freeze, ")".freeze].freeze,
54
+ :starts_before => ["(".freeze, " &< ".freeze, ")".freeze].freeze,
55
+ :ends_after => ["(".freeze, " &> ".freeze, ")".freeze].freeze,
56
+ :adjacent_to => ["(".freeze, " -|- ".freeze, ")".freeze].freeze,
57
+ :overlaps => ["(".freeze, " && ".freeze, ")".freeze].freeze,
58
+ }
59
+ FUNCTIONS = %w'lower upper isempty lower_inc upper_inc lower_inf upper_inf'
60
+
61
+ FUNCTIONS.each do |f|
62
+ class_eval("def #{f}; function(:#{f}) end", __FILE__, __LINE__)
63
+ end
64
+ OPERATORS.keys.each do |f|
65
+ class_eval("def #{f}(v); operator(:#{f}, v) end", __FILE__, __LINE__)
66
+ end
67
+
68
+ # These operators are already supported by the wrapper, but for ranges they
69
+ # return ranges, so wrap the results in another RangeOp.
70
+ %w'+ * -'.each do |f|
71
+ class_eval("def #{f}(v); RangeOp.new(super) end", __FILE__, __LINE__)
72
+ end
73
+
74
+ # Return the receiver.
75
+ def pg_range
76
+ self
77
+ end
78
+
79
+ private
80
+
81
+ # Create a boolen expression for the given type and argument.
82
+ def operator(type, other)
83
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(OPERATORS[type], [value, other]))
84
+ end
85
+
86
+ # Return a function called with the receiver.
87
+ def function(name)
88
+ Sequel::SQL::Function.new(name, self)
89
+ end
90
+ end
91
+
92
+ module RangeOpMethods
93
+ # Wrap the receiver in an RangeOp so you can easily use the PostgreSQL
94
+ # range functions and operators with it.
95
+ def pg_range
96
+ RangeOp.new(self)
97
+ end
98
+ end
99
+
100
+ if defined?(PGRange)
101
+ class PGRange
102
+ # Wrap the PGRange instance in an RangeOp, allowing you to easily use
103
+ # the PostgreSQL range functions and operators with literal ranges.
104
+ def op
105
+ RangeOp.new(self)
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ class SQL::GenericExpression
112
+ include Sequel::Postgres::RangeOpMethods
113
+ end
114
+
115
+ class LiteralString
116
+ include Sequel::Postgres::RangeOpMethods
117
+ end
118
+ end
119
+
120
+ class Symbol
121
+ include Sequel::Postgres::RangeOpMethods
122
+ end
@@ -3,8 +3,7 @@
3
3
  # executed repeatedly. When combined with the pg_auto_parameterize
4
4
  # extension, it can take Sequel code such as:
5
5
  #
6
- # DB.extend Sequel::Postgres::AutoParameterize::DatabaseMethods
7
- # DB.extend Sequel::Postgres::StatementCache::DatabaseMethods
6
+ # DB.extension :pg_auto_parameterize, :pg_statement_cache
8
7
  # DB[:table].filter(:a=>1)
9
8
  # DB[:table].filter(:a=>2)
10
9
  # DB[:table].filter(:a=>3)
@@ -313,4 +312,6 @@ module Sequel
313
312
  end
314
313
  end
315
314
  end
315
+
316
+ Database.register_extension(:pg_statement_cache, Postgres::StatementCache::DatabaseMethods)
316
317
  end
@@ -1,6 +1,17 @@
1
1
  # The pretty_table extension adds Sequel::Dataset#print and the
2
2
  # Sequel::PrettyTable class for creating nice-looking plain-text
3
- # tables.
3
+ # tables. Example:
4
+ #
5
+ # +--+-------+
6
+ # |id|name |
7
+ # |--+-------|
8
+ # |1 |fasdfas|
9
+ # |2 |test |
10
+ # +--+-------+
11
+ #
12
+ # To load the extension:
13
+ #
14
+ # Sequel.extension :pretty_table
4
15
 
5
16
  module Sequel
6
17
  extension :_pretty_table
@@ -1,6 +1,10 @@
1
1
  # The query extension adds Sequel::Dataset#query which allows
2
2
  # a different way to construct queries instead of the usual
3
3
  # method chaining.
4
+ #
5
+ # To load the extension, do:
6
+ #
7
+ # Sequel.extension :query
4
8
 
5
9
  module Sequel
6
10
  class Database
@@ -21,17 +21,15 @@
21
21
  # desirable in this area, but for backwards compatibility, the
22
22
  # defaults won't be changed until the next major release.
23
23
  #
24
- # Loading this extension does nothing by default except make the
25
- # Sequel::QueryLiterals module available. You can extend specific
26
- # datasets with this module:
24
+ # You can load this extension into specific datasets:
27
25
  #
28
26
  # ds = DB[:table]
29
- # ds.extend(Sequel::QueryLiterals)
27
+ # ds.extension(:query_literals)
30
28
  #
31
- # Order you can extend all of a database's datasets with it, which
29
+ # Or you can load it into all of a database's datasets, which
32
30
  # is probably the desired behavior if you are using this extension:
33
31
  #
34
- # DB.extend_datasets(Sequel::QueryLiterals)
32
+ # DB.extension(:query_literals)
35
33
 
36
34
  module Sequel
37
35
  # The QueryLiterals module can be used to make select, group, and
@@ -76,4 +74,6 @@ module Sequel
76
74
  end
77
75
  end
78
76
  end
77
+
78
+ Dataset.register_extension(:query_literals, QueryLiterals)
79
79
  end
@@ -3,6 +3,10 @@
3
3
  # database (which can be the same type or a different type than
4
4
  # the current database). The main interface is through
5
5
  # Sequel::Database#dump_schema_migration.
6
+ #
7
+ # To load the extension:
8
+ #
9
+ # Sequel.extension :schema_dumper
6
10
 
7
11
  module Sequel
8
12
  class Database
@@ -17,7 +21,7 @@ module Sequel
17
21
  ts = tables(options)
18
22
  <<END_MIG
19
23
  Sequel.migration do
20
- up do
24
+ change do
21
25
  #{ts.sort_by{|t| t.to_s}.map{|t| dump_table_foreign_keys(t)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
22
26
  end
23
27
  end
@@ -26,38 +30,35 @@ END_MIG
26
30
 
27
31
  # Dump indexes for all tables as a migration. This complements
28
32
  # the :indexes=>false option to dump_schema_migration. Options:
29
- # * :same_db - Create a dump for the same database type, so
30
- # don't ignore errors if the index statements fail.
31
- # * :index_names - If set to false, don't record names of indexes. If
32
- # set to :namespace, prepend the table name to the index name if the
33
- # database does not use a global index namespace.
33
+ # :same_db :: Create a dump for the same database type, so
34
+ # don't ignore errors if the index statements fail.
35
+ # :index_names :: If set to false, don't record names of indexes. If
36
+ # set to :namespace, prepend the table name to the index name if the
37
+ # database does not use a global index namespace.
34
38
  def dump_indexes_migration(options={})
35
39
  ts = tables(options)
36
40
  <<END_MIG
37
41
  Sequel.migration do
38
- up do
42
+ change do
39
43
  #{ts.sort_by{|t| t.to_s}.map{|t| dump_table_indexes(t, :add_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
40
44
  end
41
-
42
- down do
43
- #{ts.sort_by{|t| t.to_s}.reverse.map{|t| dump_table_indexes(t, :drop_index, options)}.reject{|x| x == ''}.join("\n\n").gsub(/^/o, ' ')}
44
- end
45
45
  end
46
46
  END_MIG
47
47
  end
48
48
 
49
49
  # Return a string that contains a Sequel::Migration subclass that when
50
50
  # run would recreate the database structure. Options:
51
- # * :same_db - Don't attempt to translate database types to ruby types.
52
- # If this isn't set to true, all database types will be translated to
53
- # ruby types, but there is no guarantee that the migration generated
54
- # will yield the same type. Without this set, types that aren't
55
- # recognized will be translated to a string-like type.
56
- # * :foreign_keys - If set to false, don't dump foreign_keys
57
- # * :indexes - If set to false, don't dump indexes (they can be added
58
- # later via dump_index_migration).
59
- # * :index_names - If set to false, don't record names of indexes. If
60
- # set to :namespace, prepend the table name to the index name.
51
+ # :same_db :: Don't attempt to translate database types to ruby types.
52
+ # If this isn't set to true, all database types will be translated to
53
+ # ruby types, but there is no guarantee that the migration generated
54
+ # will yield the same type. Without this set, types that aren't
55
+ # recognized will be translated to a string-like type.
56
+ # :foreign_keys :: If set to false, don't dump foreign_keys (they can be
57
+ # added later via #dump_foreign_key_migration)
58
+ # :indexes :: If set to false, don't dump indexes (they can be added
59
+ # later via #dump_index_migration).
60
+ # :index_names :: If set to false, don't record names of indexes. If
61
+ # set to :namespace, prepend the table name to the index name.
61
62
  def dump_schema_migration(options={})
62
63
  options = options.dup
63
64
  if options[:indexes] == false && !options.has_key?(:foreign_keys)
@@ -78,13 +79,9 @@ END_MIG
78
79
 
79
80
  <<END_MIG
80
81
  Sequel.migration do
81
- up do
82
+ change do
82
83
  #{ts.map{|t| dump_table_schema(t, options)}.join("\n\n").gsub(/^/o, ' ')}#{"\n \n" if skipped_fks}#{skipped_fks}
83
84
  end
84
-
85
- down do
86
- drop_table(#{ts.reverse.inspect[1...-1]})
87
- end
88
85
  end
89
86
  END_MIG
90
87
  end
@@ -113,16 +110,15 @@ END_MIG
113
110
  end
114
111
  end
115
112
 
116
- # Convert the given name and parsed database schema into an array with a method
117
- # name and arguments to it to pass to a Schema::Generator to recreate the column.
118
- def column_schema_to_generator_opts(name, schema, options)
113
+ # Recreate the column in the passed Schema::Generator from the given name and parsed database schema.
114
+ def recreate_column(name, schema, gen, options)
119
115
  if options[:single_pk] && schema_autoincrementing_primary_key?(schema)
120
116
  type_hash = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
121
117
  [:table, :key, :on_delete, :on_update, :deferrable].each{|f| type_hash[f] = schema[f] if schema[f]}
122
118
  if type_hash == {:type=>Integer} || type_hash == {:type=>"integer"}
123
- [:primary_key, name]
119
+ gen.primary_key(name)
124
120
  else
125
- [:primary_key, name, type_hash]
121
+ gen.primary_key(name, type_hash)
126
122
  end
127
123
  else
128
124
  col_opts = options[:same_db] ? {:type=>schema[:db_type]} : column_schema_to_ruby_type(schema)
@@ -137,9 +133,14 @@ END_MIG
137
133
  col_opts[:null] = false if schema[:allow_null] == false
138
134
  if table = schema[:table]
139
135
  [:key, :on_delete, :on_update, :deferrable].each{|f| col_opts[f] = schema[f] if schema[f]}
140
- [:foreign_key, name, table, col_opts]
136
+ col_opts[:type] = type unless type == Integer || type == 'integer'
137
+ gen.foreign_key(name, table, col_opts)
141
138
  else
142
- [:column, name, type, col_opts]
139
+ gen.column(name, type, col_opts)
140
+ if (type == Integer || type == Bignum) && schema[:db_type] =~ / unsigned\z/io
141
+ Sequel.extension :eval_inspect
142
+ gen.check(Sequel::SQL::Identifier.new(name) >= 0)
143
+ end
143
144
  end
144
145
  end
145
146
  end
@@ -197,7 +198,7 @@ END_MIG
197
198
  # string that would add the foreign keys if run in a migration.
198
199
  def dump_add_fk_constraints(table, fks)
199
200
  sfks = "alter_table(#{table.inspect}) do\n"
200
- sfks << Schema::Generator.new(self) do
201
+ sfks << create_table_generator do
201
202
  fks.sort_by{|fk| fk[:columns].map{|c| c.to_s}}.each do |fk|
202
203
  foreign_key fk[:columns], fk
203
204
  end
@@ -229,7 +230,7 @@ END_MIG
229
230
  s = schema(table).dup
230
231
  pks = s.find_all{|x| x.last[:primary_key] == true}.map{|x| x.first}
231
232
  options = options.merge(:single_pk=>true) if pks.length == 1
232
- m = method(:column_schema_to_generator_opts)
233
+ m = method(:recreate_column)
233
234
  im = method(:index_to_generator_opts)
234
235
 
235
236
  if options[:indexes] != false
@@ -269,8 +270,8 @@ END_MIG
269
270
  end
270
271
  end
271
272
 
272
- Schema::Generator.new(self) do
273
- s.each{|name, info| send(*m.call(name, info, options))}
273
+ create_table_generator do
274
+ s.each{|name, info| m.call(name, info, self, options)}
274
275
  primary_key(pks) if !@primary_key && pks.length > 0
275
276
  indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))} if indexes
276
277
  composite_fks.each{|fk| send(:foreign_key, fk[:columns], fk)} if composite_fks
@@ -286,7 +287,7 @@ END_MIG
286
287
  return ''
287
288
  end
288
289
  im = method(:index_to_generator_opts)
289
- gen = Schema::Generator.new(self) do
290
+ gen = create_table_generator do
290
291
  indexes.each{|iname, iopts| send(:index, iopts[:columns], im.call(table, iname, iopts, options))}
291
292
  end
292
293
  gen.dump_indexes(meth=>table, :ignore_errors=>!options[:same_db])
@@ -1,6 +1,10 @@
1
1
  # The select_remove extension adds Sequel::Dataset#select_remove for removing existing selected
2
2
  # columns from a dataset. It's not part of Sequel core as it is rarely needed and has
3
3
  # some corner cases where it can't work correctly.
4
+ #
5
+ # To load the extension:
6
+ #
7
+ # Sequel.extension :select_remove
4
8
 
5
9
  module Sequel
6
10
  class Dataset
@@ -4,8 +4,7 @@
4
4
  #
5
5
  # First, you need to enable it on the database object:
6
6
  #
7
- # Sequel.extension :server_block
8
- # DB.extend Sequel::ServerBlock
7
+ # DB.extension :server_block
9
8
  #
10
9
  # Then you can call with_server:
11
10
  #
@@ -136,4 +135,6 @@ module Sequel
136
135
  end
137
136
  end
138
137
  end
138
+
139
+ Database.register_extension(:server_block, ServerBlock)
139
140
  end
@@ -0,0 +1,65 @@
1
+ # The split_array_nil extension overrides Sequel's default handling of
2
+ # IN/NOT IN with arrays of values to do specific nil checking. For example,
3
+ #
4
+ # ds = DB[:table].where(:column=>[1, nil])
5
+ #
6
+ # By default, that produces the following SQL:
7
+ #
8
+ # SELECT * FROM table WHERE (column IN (1, NULL))
9
+ #
10
+ # However, because NULL = NULL is not true in SQL (it is NULL), this
11
+ # will not return rows in the table where the column is NULL. This
12
+ # extension allows for an alternative behavior more similar to ruby,
13
+ # which will return rows in the table where the column is NULL, using
14
+ # a query like:
15
+ #
16
+ # SELECT * FROM table WHERE ((column IN (1)) OR (column IS NULL)))
17
+ #
18
+ # Similarly, for NOT IN queries:
19
+ #
20
+ # ds = DB[:table].exclude(:column=>[1, nil])
21
+ # # Default:
22
+ # # SELECT * FROM table WHERE (column NOT IN (1, NULL))
23
+ # # with split_array_nils extension:
24
+ # # SELECT * FROM table WHERE ((column NOT IN (1)) AND (column IS NOT NULL)))
25
+ #
26
+ # To use this extension with a single dataset:
27
+ #
28
+ # ds = ds.extension(:split_array_nil)
29
+ #
30
+ # To use this extension for all of a database's datasets:
31
+ #
32
+ # DB.extension(:split_array_nil)
33
+
34
+ module Sequel
35
+ class Dataset
36
+ module SplitArrayNil
37
+ # Over the IN/NOT IN handling with an array of values where one of the
38
+ # values in the array is nil, by removing nils from the array of values,
39
+ # and using a separate OR IS NULL clause for IN or AND IS NOT NULL clause
40
+ # for NOT IN.
41
+ def complex_expression_sql_append(sql, op, args)
42
+ case op
43
+ when :IN, :"NOT IN"
44
+ vals = args.at(1)
45
+ if vals.is_a?(Array) && vals.any?{|v| v.nil?}
46
+ cols = args.at(0)
47
+ vals = vals.compact
48
+ c = Sequel::SQL::BooleanExpression
49
+ if op == :IN
50
+ literal_append(sql, c.new(:OR, c.new(:IN, cols, vals), c.new(:IS, cols, nil)))
51
+ else
52
+ literal_append(sql, c.new(:AND, c.new(:"NOT IN", cols, vals), c.new(:"IS NOT", cols, nil)))
53
+ end
54
+ else
55
+ super
56
+ end
57
+ else
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ Dataset.register_extension(:split_array_nil, Dataset::SplitArrayNil)
65
+ end