sequel 4.1.1 → 4.2.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/opening_databases.rdoc +4 -0
  4. data/doc/release_notes/4.2.0.txt +129 -0
  5. data/lib/sequel/adapters/jdbc/hsqldb.rb +5 -0
  6. data/lib/sequel/adapters/mysql2.rb +2 -1
  7. data/lib/sequel/adapters/postgres.rb +8 -4
  8. data/lib/sequel/adapters/shared/db2.rb +5 -0
  9. data/lib/sequel/adapters/shared/mssql.rb +15 -4
  10. data/lib/sequel/adapters/shared/mysql.rb +1 -0
  11. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  12. data/lib/sequel/adapters/shared/postgres.rb +10 -0
  13. data/lib/sequel/adapters/shared/sqlite.rb +5 -0
  14. data/lib/sequel/database/features.rb +6 -1
  15. data/lib/sequel/database/schema_methods.rb +3 -7
  16. data/lib/sequel/dataset/actions.rb +3 -4
  17. data/lib/sequel/dataset/features.rb +5 -0
  18. data/lib/sequel/dataset/misc.rb +28 -3
  19. data/lib/sequel/dataset/mutation.rb +37 -11
  20. data/lib/sequel/dataset/prepared_statements.rb +1 -3
  21. data/lib/sequel/dataset/query.rb +12 -3
  22. data/lib/sequel/dataset/sql.rb +12 -6
  23. data/lib/sequel/deprecated.rb +1 -1
  24. data/lib/sequel/extensions/columns_introspection.rb +1 -1
  25. data/lib/sequel/extensions/core_extensions.rb +0 -2
  26. data/lib/sequel/extensions/empty_array_ignore_nulls.rb +1 -1
  27. data/lib/sequel/extensions/filter_having.rb +1 -1
  28. data/lib/sequel/extensions/from_block.rb +31 -0
  29. data/lib/sequel/extensions/graph_each.rb +1 -1
  30. data/lib/sequel/extensions/hash_aliases.rb +1 -1
  31. data/lib/sequel/extensions/mssql_emulate_lateral_with_apply.rb +78 -0
  32. data/lib/sequel/extensions/pagination.rb +1 -1
  33. data/lib/sequel/extensions/pg_loose_count.rb +32 -0
  34. data/lib/sequel/extensions/pg_static_cache_updater.rb +133 -0
  35. data/lib/sequel/extensions/pretty_table.rb +1 -1
  36. data/lib/sequel/extensions/query.rb +3 -1
  37. data/lib/sequel/extensions/query_literals.rb +1 -1
  38. data/lib/sequel/extensions/select_remove.rb +1 -1
  39. data/lib/sequel/extensions/sequel_3_dataset_methods.rb +1 -1
  40. data/lib/sequel/extensions/set_overrides.rb +1 -1
  41. data/lib/sequel/model.rb +1 -1
  42. data/lib/sequel/model/base.rb +20 -6
  43. data/lib/sequel/model/exceptions.rb +1 -1
  44. data/lib/sequel/plugins/composition.rb +9 -0
  45. data/lib/sequel/plugins/dirty.rb +19 -8
  46. data/lib/sequel/plugins/instance_filters.rb +9 -0
  47. data/lib/sequel/plugins/serialization.rb +9 -0
  48. data/lib/sequel/plugins/serialization_modification_detection.rb +9 -0
  49. data/lib/sequel/plugins/static_cache.rb +96 -28
  50. data/lib/sequel/version.rb +2 -2
  51. data/spec/adapters/mssql_spec.rb +1 -1
  52. data/spec/adapters/postgres_spec.rb +70 -0
  53. data/spec/core/dataset_spec.rb +58 -1
  54. data/spec/core/deprecated_spec.rb +1 -1
  55. data/spec/core/schema_spec.rb +18 -0
  56. data/spec/extensions/composition_spec.rb +7 -0
  57. data/spec/extensions/dirty_spec.rb +9 -0
  58. data/spec/extensions/from_block_spec.rb +21 -0
  59. data/spec/extensions/instance_filters_spec.rb +6 -0
  60. data/spec/extensions/pg_loose_count_spec.rb +17 -0
  61. data/spec/extensions/pg_static_cache_updater_spec.rb +80 -0
  62. data/spec/extensions/query_spec.rb +8 -0
  63. data/spec/extensions/serialization_modification_detection_spec.rb +9 -0
  64. data/spec/extensions/serialization_spec.rb +7 -0
  65. data/spec/extensions/set_overrides_spec.rb +12 -0
  66. data/spec/extensions/static_cache_spec.rb +314 -154
  67. data/spec/integration/dataset_test.rb +12 -2
  68. data/spec/integration/schema_test.rb +13 -0
  69. data/spec/model/record_spec.rb +74 -0
  70. metadata +13 -3
@@ -40,6 +40,13 @@ module Sequel
40
40
  def eql?(o)
41
41
  self == o
42
42
  end
43
+
44
+ # Similar to #clone, but returns an unfrozen clone if the receiver is frozen.
45
+ def dup
46
+ o = clone
47
+ o.opts.delete(:frozen)
48
+ o
49
+ end
43
50
 
44
51
  # Yield a dataset for each server in the connection pool that is tied to that server.
45
52
  # Intended for use in sharded environments where all servers need to be modified
@@ -58,6 +65,17 @@ module Sequel
58
65
  def escape_like(string)
59
66
  string.gsub(/[\\%_]/){|m| "\\#{m}"}
60
67
  end
68
+
69
+ # Sets the frozen flag on the dataset, so you can't modify it. Returns the receiver.
70
+ def freeze
71
+ @opts[:frozen] = true
72
+ self
73
+ end
74
+
75
+ # Whether the object is frozen.
76
+ def frozen?
77
+ @opts[:frozen]
78
+ end
61
79
 
62
80
  # Alias of +first_source_alias+
63
81
  def first_source
@@ -142,9 +160,7 @@ module Sequel
142
160
  # Returns a string representation of the dataset including the class name
143
161
  # and the corresponding SQL select statement.
144
162
  def inspect
145
- c = self.class
146
- c = c.superclass while c.name.nil? || c.name == ''
147
- "#<#{c.name}: #{sql.inspect}>"
163
+ "#<#{visible_class_name}: #{sql.inspect}>"
148
164
  end
149
165
 
150
166
  # The alias to use for the row_number column, used when emulating OFFSET
@@ -207,5 +223,14 @@ module Sequel
207
223
  table_alias
208
224
  end
209
225
  end
226
+
227
+ private
228
+
229
+ # Return the class name for this dataset, but skip anonymous classes
230
+ def visible_class_name
231
+ c = self.class
232
+ c = c.superclass while c.name.nil? || c.name == ''
233
+ c.name
234
+ end
210
235
  end
211
236
  end
@@ -6,7 +6,7 @@ module Sequel
6
6
  # ---------------------
7
7
 
8
8
  # All methods that should have a ! method added that modifies the receiver.
9
- MUTATION_METHODS = QUERY_METHODS - [:paginate, :naked, :from_self]
9
+ MUTATION_METHODS = QUERY_METHODS - [:naked, :from_self]
10
10
 
11
11
  # Setup mutation (e.g. filter!) methods. These operate the same as the
12
12
  # non-! methods, but replace the options of the current dataset with the
@@ -26,18 +26,9 @@ module Sequel
26
26
  # Add the mutation methods via metaprogramming
27
27
  def_mutation_method(*MUTATION_METHODS)
28
28
 
29
- # Set the method to call on identifiers going into the database for this dataset
30
- attr_writer :identifier_input_method
31
-
32
- # Set the method to call on identifiers coming the database for this dataset
33
- attr_writer :identifier_output_method
34
-
35
- # Whether to quote identifiers for this dataset
36
- attr_writer :quote_identifiers
37
-
38
29
  # The row_proc for this database, should be any object that responds to +call+ with
39
30
  # a single hash argument and returns the object you want #each to return.
40
- attr_accessor :row_proc
31
+ attr_reader :row_proc
41
32
 
42
33
  # Load an extension into the receiver. In addition to requiring the extension file, this
43
34
  # also modifies the dataset to work with the extension (usually extending it with a
@@ -45,6 +36,7 @@ module Sequel
45
36
  # extension does not have specific support for Database objects, an Error will be raised.
46
37
  # Returns self.
47
38
  def extension!(*exts)
39
+ raise_if_frozen!
48
40
  Sequel.extension(*exts)
49
41
  exts.each do |ext|
50
42
  if pr = Sequel.synchronize{EXTENSIONS[ext]}
@@ -58,24 +50,58 @@ module Sequel
58
50
 
59
51
  # Avoid self-referential dataset by cloning.
60
52
  def from_self!(*args, &block)
53
+ raise_if_frozen!
61
54
  @opts = clone.from_self(*args, &block).opts
62
55
  self
63
56
  end
64
57
 
58
+ # Set the method to call on identifiers going into the database for this dataset
59
+ def identifier_input_method=(v)
60
+ raise_if_frozen!
61
+ @identifier_input_method = v
62
+ end
63
+
64
+ # Set the method to call on identifiers coming the database for this dataset
65
+ def identifier_output_method=(v)
66
+ raise_if_frozen!
67
+ @identifier_output_method = v
68
+ end
69
+
65
70
  # Remove the row_proc from the current dataset.
66
71
  def naked!
72
+ raise_if_frozen!
67
73
  self.row_proc = nil
68
74
  self
69
75
  end
70
76
 
77
+ # Set whether to quote identifiers for this dataset
78
+ def quote_identifiers=(v)
79
+ raise_if_frozen!
80
+ @quote_identifiers = v
81
+ end
82
+
83
+ # Override the row_proc for this dataset
84
+ def row_proc=(v)
85
+ raise_if_frozen!
86
+ @row_proc = v
87
+ end
88
+
71
89
  private
72
90
 
73
91
  # Modify the receiver with the results of sending the meth, args, and block
74
92
  # to the receiver and merging the options of the resulting dataset into
75
93
  # the receiver's options.
76
94
  def mutation_method(meth, *args, &block)
95
+ raise_if_frozen!
77
96
  @opts = send(meth, *args, &block).opts
78
97
  self
79
98
  end
99
+
100
+ # Raise a RuntimeError if the receiver is frozen
101
+ def raise_if_frozen!
102
+ if frozen?
103
+ raise RuntimeError, "can't modify frozen #{visible_class_name}"
104
+ end
105
+ end
80
106
  end
81
107
  end
@@ -118,9 +118,7 @@ module Sequel
118
118
  # with the prepared SQL it represents (which in general won't have
119
119
  # substituted variables).
120
120
  def inspect
121
- c = self.class
122
- c = c.superclass while c.name.nil? || c.name == ''
123
- "<#{c.name}/PreparedStatement #{prepared_sql.inspect}>"
121
+ "<#{visible_class_name}/PreparedStatement #{prepared_sql.inspect}>"
124
122
  end
125
123
 
126
124
  protected
@@ -36,12 +36,11 @@ module Sequel
36
36
  QUERY_METHODS = (<<-METHS).split.map{|x| x.to_sym} + JOIN_METHODS
37
37
  add_graph_aliases and distinct except exclude exclude_having exclude_where
38
38
  filter for_update from from_self graph grep group group_and_count group_by having intersect invert
39
- limit lock_style naked or order order_append order_by order_more order_prepend paginate qualify query
39
+ limit lock_style naked or order order_append order_by order_more order_prepend qualify
40
40
  reverse reverse_order select select_all select_append select_group select_more server
41
- set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
41
+ set_graph_aliases unfiltered ungraphed ungrouped union
42
42
  unlimited unordered where with with_recursive with_sql
43
43
  METHS
44
- # REMOVE40: query paginate set_defaults set_overrides
45
44
 
46
45
  # Register an extension callback for Dataset objects. ext should be the
47
46
  # extension name symbol, and mod should either be a Module that the
@@ -501,6 +500,16 @@ module Sequel
501
500
  class_eval("def #{jtype}_join(table); raise(Sequel::Error, '#{jtype}_join does not accept join table blocks') if block_given?; join_table(:#{jtype}, table) end", __FILE__, __LINE__)
502
501
  end
503
502
 
503
+ # Marks this dataset as a lateral dataset. If used in another dataset's FROM
504
+ # or JOIN clauses, it will surround the subquery with LATERAL to enable it
505
+ # to deal with previous tables in the query:
506
+ #
507
+ # DB.from(:a, DB[:b].where(:a__c=>:b__d).lateral)
508
+ # # SELECT * FROM a, LATERAL (SELECT * FROM b WHERE (a.c = b.d))
509
+ def lateral
510
+ clone(:lateral=>true)
511
+ end
512
+
504
513
  # If given an integer, the dataset will contain only the first l results.
505
514
  # If given a range, it will contain only those at offsets within that
506
515
  # range. If a second argument is given, it is used as an offset. To use
@@ -242,6 +242,7 @@ module Sequel
242
242
  INTO = " INTO ".freeze
243
243
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
244
244
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
245
+ LATERAL = 'LATERAL '.freeze
245
246
  LIKE_OPERATORS = ::Sequel::SQL::ComplexExpression::LIKE_OPERATORS
246
247
  LIMIT = " LIMIT ".freeze
247
248
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
@@ -533,12 +534,16 @@ module Sequel
533
534
  str = pls.str
534
535
  sql << PAREN_OPEN if pls.parens
535
536
  if args.is_a?(Hash)
536
- re = /:(#{args.keys.map{|k| Regexp.escape(k.to_s)}.join('|')})\b/
537
- loop do
538
- previous, q, str = str.partition(re)
539
- sql << previous
540
- literal_append(sql, args[($1||q[1..-1].to_s).to_sym]) unless q.empty?
541
- break if str.empty?
537
+ if args.empty?
538
+ sql << str
539
+ else
540
+ re = /:(#{args.keys.map{|k| Regexp.escape(k.to_s)}.join('|')})\b/
541
+ loop do
542
+ previous, q, str = str.partition(re)
543
+ sql << previous
544
+ literal_append(sql, args[($1||q[1..-1].to_s).to_sym]) unless q.empty?
545
+ break if str.empty?
546
+ end
542
547
  end
543
548
  elsif str.is_a?(Array)
544
549
  len = args.length
@@ -1043,6 +1048,7 @@ module Sequel
1043
1048
 
1044
1049
  # SQL fragment for Dataset. Does a subselect inside parantheses.
1045
1050
  def literal_dataset_append(sql, v)
1051
+ sql << LATERAL if v.opts[:lateral]
1046
1052
  sql << PAREN_OPEN
1047
1053
  subselect_sql_append(sql, v)
1048
1054
  sql << PAREN_CLOSE
@@ -37,7 +37,7 @@ module Sequel
37
37
  # Print the message and possibly backtrace to the output.
38
38
  def self.deprecate(method, instead=nil)
39
39
  return unless output
40
- message = instead ? "#{method} is deprecated and will be removed in Sequel 4.0. #{instead}." : method
40
+ message = instead ? "#{method} is deprecated and will be removed in a future version of Sequel. #{instead}." : method
41
41
  message = "#{prefix}#{message}" if prefix
42
42
  output.puts(message)
43
43
  case b = backtrace_filter
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # To attempt to introspect columns for a single dataset:
11
11
  #
12
- # ds.extension(:columns_introspection)
12
+ # ds = ds.extension(:columns_introspection)
13
13
  #
14
14
  # To attempt to introspect columns for all datasets on a single database:
15
15
  #
@@ -48,8 +48,6 @@ class Array
48
48
  def sql_value_list
49
49
  ::Sequel::SQL::ValueList.new(self)
50
50
  end
51
-
52
- # Deprecated alias for sql_value_list
53
51
  alias sql_array sql_value_list
54
52
 
55
53
  # Return a <tt>Sequel::SQL::BooleanExpression</tt> created from this array, matching all of the
@@ -13,7 +13,7 @@
13
13
  # You can load this extension into specific datasets:
14
14
  #
15
15
  # ds = DB[:table]
16
- # ds.extension(:empty_array_ignore_nulls)
16
+ # ds = ds.extension(:empty_array_ignore_nulls)
17
17
  #
18
18
  # Or you can load it into all of a database's datasets, which
19
19
  # is probably the desired behavior if you are using this extension:
@@ -7,7 +7,7 @@
7
7
  # You can load this extension into specific datasets:
8
8
  #
9
9
  # ds = DB[:table]
10
- # ds.extension(:filter_having)
10
+ # ds = ds.extension(:filter_having)
11
11
  #
12
12
  # Or you can load it into all of a database's datasets, which
13
13
  # is probably the desired behavior if you are using this extension:
@@ -0,0 +1,31 @@
1
+ # The from_block extension changes Database#from so that blocks given
2
+ # to it are treated as virtual rows applying to the FROM clause,
3
+ # instead of virtual rows applying to the WHERE clause. This will
4
+ # probably be made the default in the next major version of Sequel.
5
+ #
6
+ # This makes it easier to use table returning functions:
7
+ #
8
+ # DB.from{table_function(1)}
9
+ # # SELECT * FROM table_function(1)
10
+ #
11
+ # To load the extension into the database:
12
+ #
13
+ # DB.extension :from_block
14
+
15
+ module Sequel
16
+ module Database::FromBlock
17
+ # If a block is given, make it affect the FROM clause:
18
+ # DB.from{table_function(1)}
19
+ # # SELECT * FROM table_function(1)
20
+ def from(*args, &block)
21
+ if block
22
+ @default_dataset.from(*args, &block)
23
+ else
24
+ super
25
+ end
26
+ end
27
+ end
28
+
29
+ Database.register_extension(:from_block, Database::FromBlock)
30
+ end
31
+
@@ -8,7 +8,7 @@
8
8
  # You can load this extension into specific datasets:
9
9
  #
10
10
  # ds = DB[:table]
11
- # ds.extension(:graph_each)
11
+ # ds = ds.extension(:graph_each)
12
12
  #
13
13
  # Or you can load it into all of a database's datasets, which
14
14
  # is probably the desired behavior if you are using this extension:
@@ -7,7 +7,7 @@
7
7
  # You can load this extension into specific datasets:
8
8
  #
9
9
  # ds = DB[:table]
10
- # ds.extension(:hash_aliases)
10
+ # ds = ds.extension(:hash_aliases)
11
11
  #
12
12
  # Or you can load it into all of a database's datasets, which
13
13
  # is probably the desired behavior if you are using this extension:
@@ -0,0 +1,78 @@
1
+ # The mssql_emulate_lateral_with_apply extension converts
2
+ # queries that use LATERAL into queries that use CROSS/OUTER
3
+ # APPLY, allowing code that works on databases that support
4
+ # LATERAL via Dataset#lateral to run on Microsoft SQL Server.
5
+ #
6
+ # This is available as a separate extension instead of
7
+ # integrated into the Microsoft SQL Server support because
8
+ # few people need it and there is a performance hit to
9
+ # code that doesn't use it.
10
+ #
11
+ # It is possible there are cases where this emulation does
12
+ # not work. Users should probably verify that correct
13
+ # results are returned when using this extension.
14
+ #
15
+ # You can load this extension into specific datasets:
16
+ #
17
+ # ds = DB[:table]
18
+ # ds = ds.extension(:mssql_emulate_lateral_with_apply)
19
+ #
20
+ # Or you can load it into all of a database's datasets:
21
+ #
22
+ # DB.extension(:mssql_emulate_lateral_with_apply)
23
+
24
+ module Sequel
25
+ module MSSQL
26
+ module EmulateLateralWithApply
27
+ # If the table is a dataset that uses LATERAL,
28
+ # convert it to a CROSS APPLY if it is a INNER
29
+ # or CROSS JOIN, and an OUTER APPLY if it is a
30
+ # LEFT JOIN.
31
+ def join_table(type, table, expr=nil, *)
32
+ if table.is_a?(Dataset) && table.opts[:lateral]
33
+ table = table.clone(:lateral=>nil)
34
+ case type
35
+ when :inner
36
+ type = :cross_apply
37
+ table = table.where(expr)
38
+ expr = nil
39
+ when :cross
40
+ type = :cross_apply
41
+ when :left, :left_outer
42
+ type = :outer_apply
43
+ table = table.where(expr)
44
+ expr = nil
45
+ end
46
+ end
47
+ super
48
+ end
49
+
50
+ # When a FROM entry uses a LATERAL subquery,
51
+ # convert that entry into a CROSS APPLY.
52
+ def from(*source, &block)
53
+ virtual_row_columns(source, block)
54
+ lateral, source = source.partition{|t| t.is_a?(Sequel::Dataset) && t.opts[:lateral] || (t.is_a?(Sequel::SQL::AliasedExpression) && t.expression.is_a?(Sequel::Dataset) && t.expression.opts[:lateral])} unless source.empty?
55
+ return super(*source, &nil) if !lateral || lateral.empty?
56
+
57
+ ds = from(*source)
58
+ lateral.each do |l|
59
+ l = if l.is_a?(Sequel::SQL::AliasedExpression)
60
+ l.expression.clone(:lateral=>nil).as(l.aliaz)
61
+ else
62
+ l.clone(:lateral=>nil)
63
+ end
64
+ ds = ds.cross_apply(l)
65
+ end
66
+ ds
67
+ end
68
+
69
+ # MSSQL can emulate lateral subqueries via CROSS/OUTER APPLY
70
+ # when using this extension.
71
+ def supports_lateral_subqueries?
72
+ true
73
+ end
74
+ end
75
+ end
76
+
77
+ Dataset.register_extension(:mssql_emulate_lateral_with_apply, MSSQL::EmulateLateralWithApply)
78
+ end
@@ -7,7 +7,7 @@
7
7
  # You can load this extension into specific datasets:
8
8
  #
9
9
  # ds = DB[:table]
10
- # ds.extension(:pagination)
10
+ # ds = ds.extension(:pagination)
11
11
  #
12
12
  # Or you can load it into all of a database's datasets, which
13
13
  # is probably the desired behavior if you are using this extension:
@@ -0,0 +1,32 @@
1
+ # The pg_loose_count extension looks at the table statistics
2
+ # in the PostgreSQL system tables to get a fast approximate
3
+ # count of the number of rows in a given table:
4
+ #
5
+ # DB.loose_count(:table) # => 123456
6
+ #
7
+ # It can also support schema qualified tables:
8
+ #
9
+ # DB.loose_count(:schema__table) # => 123456
10
+ #
11
+ # How accurate this count is depends on the number of rows
12
+ # added/deleted from the table since the last time it was
13
+ # analyzed.
14
+ #
15
+ # To load the extension into the database:
16
+ #
17
+ # DB.extension :pg_loose_count
18
+
19
+ module Sequel
20
+ module Postgres
21
+ module LooseCount
22
+ # Look at the table statistics for the given table to get
23
+ # an approximate count of the number of rows.
24
+ def loose_count(table)
25
+ from(:pg_class).where(:oid=>regclass_oid(table)).get(Sequel.cast(:reltuples, Integer))
26
+ end
27
+ end
28
+ end
29
+
30
+ Database.register_extension(:pg_loose_count, Postgres::LooseCount)
31
+ end
32
+