sequel 4.1.1 → 4.2.0

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