sequel 3.33.0 → 3.34.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 (152) hide show
  1. data/CHANGELOG +140 -0
  2. data/Rakefile +7 -0
  3. data/bin/sequel +22 -2
  4. data/doc/dataset_basics.rdoc +1 -1
  5. data/doc/mass_assignment.rdoc +3 -1
  6. data/doc/querying.rdoc +28 -4
  7. data/doc/reflection.rdoc +23 -3
  8. data/doc/release_notes/3.34.0.txt +671 -0
  9. data/doc/schema_modification.rdoc +18 -2
  10. data/doc/virtual_rows.rdoc +49 -0
  11. data/lib/sequel/adapters/do/mysql.rb +0 -5
  12. data/lib/sequel/adapters/ibmdb.rb +9 -4
  13. data/lib/sequel/adapters/jdbc.rb +9 -4
  14. data/lib/sequel/adapters/jdbc/h2.rb +8 -2
  15. data/lib/sequel/adapters/jdbc/mysql.rb +0 -5
  16. data/lib/sequel/adapters/jdbc/postgresql.rb +43 -0
  17. data/lib/sequel/adapters/jdbc/sqlite.rb +19 -0
  18. data/lib/sequel/adapters/mock.rb +24 -3
  19. data/lib/sequel/adapters/mysql.rb +29 -50
  20. data/lib/sequel/adapters/mysql2.rb +13 -28
  21. data/lib/sequel/adapters/oracle.rb +8 -2
  22. data/lib/sequel/adapters/postgres.rb +115 -20
  23. data/lib/sequel/adapters/shared/db2.rb +1 -1
  24. data/lib/sequel/adapters/shared/mssql.rb +14 -3
  25. data/lib/sequel/adapters/shared/mysql.rb +59 -11
  26. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +6 -0
  27. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  28. data/lib/sequel/adapters/shared/postgres.rb +127 -30
  29. data/lib/sequel/adapters/shared/sqlite.rb +55 -38
  30. data/lib/sequel/adapters/sqlite.rb +9 -3
  31. data/lib/sequel/adapters/swift.rb +2 -2
  32. data/lib/sequel/adapters/swift/mysql.rb +0 -5
  33. data/lib/sequel/adapters/swift/postgres.rb +10 -0
  34. data/lib/sequel/ast_transformer.rb +4 -0
  35. data/lib/sequel/connection_pool.rb +8 -0
  36. data/lib/sequel/connection_pool/sharded_single.rb +5 -0
  37. data/lib/sequel/connection_pool/sharded_threaded.rb +17 -0
  38. data/lib/sequel/connection_pool/single.rb +5 -0
  39. data/lib/sequel/connection_pool/threaded.rb +14 -0
  40. data/lib/sequel/core.rb +24 -3
  41. data/lib/sequel/database/connecting.rb +24 -14
  42. data/lib/sequel/database/dataset_defaults.rb +1 -0
  43. data/lib/sequel/database/misc.rb +16 -25
  44. data/lib/sequel/database/query.rb +20 -2
  45. data/lib/sequel/database/schema_generator.rb +2 -2
  46. data/lib/sequel/database/schema_methods.rb +120 -23
  47. data/lib/sequel/dataset/actions.rb +91 -18
  48. data/lib/sequel/dataset/features.rb +5 -0
  49. data/lib/sequel/dataset/prepared_statements.rb +6 -2
  50. data/lib/sequel/dataset/sql.rb +68 -51
  51. data/lib/sequel/extensions/_pretty_table.rb +79 -0
  52. data/lib/sequel/{core_sql.rb → extensions/core_extensions.rb} +18 -13
  53. data/lib/sequel/extensions/migration.rb +4 -0
  54. data/lib/sequel/extensions/null_dataset.rb +90 -0
  55. data/lib/sequel/extensions/pg_array.rb +460 -0
  56. data/lib/sequel/extensions/pg_array_ops.rb +220 -0
  57. data/lib/sequel/extensions/pg_auto_parameterize.rb +174 -0
  58. data/lib/sequel/extensions/pg_hstore.rb +296 -0
  59. data/lib/sequel/extensions/pg_hstore_ops.rb +259 -0
  60. data/lib/sequel/extensions/pg_statement_cache.rb +316 -0
  61. data/lib/sequel/extensions/pretty_table.rb +5 -71
  62. data/lib/sequel/extensions/query_literals.rb +79 -0
  63. data/lib/sequel/extensions/schema_caching.rb +76 -0
  64. data/lib/sequel/extensions/schema_dumper.rb +227 -31
  65. data/lib/sequel/extensions/select_remove.rb +35 -0
  66. data/lib/sequel/extensions/sql_expr.rb +4 -110
  67. data/lib/sequel/extensions/to_dot.rb +1 -1
  68. data/lib/sequel/model.rb +11 -2
  69. data/lib/sequel/model/associations.rb +35 -7
  70. data/lib/sequel/model/base.rb +159 -36
  71. data/lib/sequel/no_core_ext.rb +2 -0
  72. data/lib/sequel/plugins/caching.rb +25 -18
  73. data/lib/sequel/plugins/composition.rb +1 -1
  74. data/lib/sequel/plugins/hook_class_methods.rb +1 -1
  75. data/lib/sequel/plugins/identity_map.rb +11 -3
  76. data/lib/sequel/plugins/instance_filters.rb +10 -0
  77. data/lib/sequel/plugins/many_to_one_pk_lookup.rb +71 -0
  78. data/lib/sequel/plugins/nested_attributes.rb +4 -3
  79. data/lib/sequel/plugins/prepared_statements.rb +3 -1
  80. data/lib/sequel/plugins/prepared_statements_associations.rb +5 -1
  81. data/lib/sequel/plugins/schema.rb +7 -2
  82. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  83. data/lib/sequel/plugins/static_cache.rb +99 -0
  84. data/lib/sequel/plugins/validation_class_methods.rb +1 -1
  85. data/lib/sequel/sql.rb +417 -7
  86. data/lib/sequel/version.rb +1 -1
  87. data/spec/adapters/firebird_spec.rb +1 -1
  88. data/spec/adapters/mssql_spec.rb +12 -15
  89. data/spec/adapters/mysql_spec.rb +81 -23
  90. data/spec/adapters/postgres_spec.rb +444 -77
  91. data/spec/adapters/spec_helper.rb +2 -0
  92. data/spec/adapters/sqlite_spec.rb +8 -8
  93. data/spec/core/connection_pool_spec.rb +85 -0
  94. data/spec/core/database_spec.rb +29 -5
  95. data/spec/core/dataset_spec.rb +171 -3
  96. data/spec/core/expression_filters_spec.rb +364 -0
  97. data/spec/core/mock_adapter_spec.rb +17 -3
  98. data/spec/core/schema_spec.rb +133 -0
  99. data/spec/extensions/association_dependencies_spec.rb +13 -13
  100. data/spec/extensions/caching_spec.rb +26 -3
  101. data/spec/extensions/class_table_inheritance_spec.rb +2 -2
  102. data/spec/{core/core_sql_spec.rb → extensions/core_extensions_spec.rb} +23 -94
  103. data/spec/extensions/force_encoding_spec.rb +4 -2
  104. data/spec/extensions/hook_class_methods_spec.rb +5 -2
  105. data/spec/extensions/identity_map_spec.rb +17 -0
  106. data/spec/extensions/instance_filters_spec.rb +1 -1
  107. data/spec/extensions/lazy_attributes_spec.rb +2 -2
  108. data/spec/extensions/list_spec.rb +4 -4
  109. data/spec/extensions/many_to_one_pk_lookup_spec.rb +140 -0
  110. data/spec/extensions/migration_spec.rb +6 -2
  111. data/spec/extensions/nested_attributes_spec.rb +20 -0
  112. data/spec/extensions/null_dataset_spec.rb +85 -0
  113. data/spec/extensions/optimistic_locking_spec.rb +2 -2
  114. data/spec/extensions/pg_array_ops_spec.rb +105 -0
  115. data/spec/extensions/pg_array_spec.rb +196 -0
  116. data/spec/extensions/pg_auto_parameterize_spec.rb +64 -0
  117. data/spec/extensions/pg_hstore_ops_spec.rb +136 -0
  118. data/spec/extensions/pg_hstore_spec.rb +195 -0
  119. data/spec/extensions/pg_statement_cache_spec.rb +209 -0
  120. data/spec/extensions/prepared_statements_spec.rb +4 -0
  121. data/spec/extensions/pretty_table_spec.rb +6 -0
  122. data/spec/extensions/query_literals_spec.rb +168 -0
  123. data/spec/extensions/schema_caching_spec.rb +41 -0
  124. data/spec/extensions/schema_dumper_spec.rb +231 -11
  125. data/spec/extensions/schema_spec.rb +14 -2
  126. data/spec/extensions/select_remove_spec.rb +38 -0
  127. data/spec/extensions/sharding_spec.rb +6 -6
  128. data/spec/extensions/skip_create_refresh_spec.rb +1 -1
  129. data/spec/extensions/spec_helper.rb +2 -1
  130. data/spec/extensions/sql_expr_spec.rb +28 -19
  131. data/spec/extensions/static_cache_spec.rb +145 -0
  132. data/spec/extensions/touch_spec.rb +1 -1
  133. data/spec/extensions/typecast_on_load_spec.rb +9 -1
  134. data/spec/integration/associations_test.rb +6 -6
  135. data/spec/integration/database_test.rb +1 -1
  136. data/spec/integration/dataset_test.rb +89 -26
  137. data/spec/integration/migrator_test.rb +2 -3
  138. data/spec/integration/model_test.rb +3 -3
  139. data/spec/integration/plugin_test.rb +85 -22
  140. data/spec/integration/prepared_statement_test.rb +28 -8
  141. data/spec/integration/schema_test.rb +78 -7
  142. data/spec/integration/spec_helper.rb +1 -0
  143. data/spec/integration/timezone_test.rb +1 -1
  144. data/spec/integration/transaction_test.rb +4 -6
  145. data/spec/integration/type_test.rb +2 -2
  146. data/spec/model/associations_spec.rb +94 -8
  147. data/spec/model/base_spec.rb +4 -4
  148. data/spec/model/hooks_spec.rb +2 -2
  149. data/spec/model/model_spec.rb +19 -7
  150. data/spec/model/record_spec.rb +135 -58
  151. data/spec/model/spec_helper.rb +1 -0
  152. metadata +35 -7
@@ -0,0 +1,79 @@
1
+ # The pretty_table extension adds Sequel::Dataset#print and the
2
+ # Sequel::PrettyTable class for creating nice-looking plain-text
3
+ # tables.
4
+
5
+ module Sequel
6
+ module PrettyTable
7
+ # Prints nice-looking plain-text tables via puts
8
+ #
9
+ # +--+-------+
10
+ # |id|name |
11
+ # |--+-------|
12
+ # |1 |fasdfas|
13
+ # |2 |test |
14
+ # +--+-------+
15
+ def self.print(records, columns=nil)
16
+ puts string(records, columns)
17
+ end
18
+
19
+ # Return the string that #print will print via puts.
20
+ def self.string(records, columns = nil) # records is an array of hashes
21
+ columns ||= records.first.keys.sort_by{|x|x.to_s}
22
+ sizes = column_sizes(records, columns)
23
+ sep_line = separator_line(columns, sizes)
24
+
25
+ array = [sep_line, header_line(columns, sizes), sep_line]
26
+ records.each {|r| array << data_line(columns, sizes, r)}
27
+ array << sep_line
28
+ array.join("\n")
29
+ end
30
+
31
+ ### Private Module Methods ###
32
+
33
+ # Hash of the maximum size of the value for each column
34
+ def self.column_sizes(records, columns) # :nodoc:
35
+ sizes = Hash.new {0}
36
+ columns.each do |c|
37
+ s = c.to_s.size
38
+ sizes[c.to_sym] = s if s > sizes[c.to_sym]
39
+ end
40
+ records.each do |r|
41
+ columns.each do |c|
42
+ s = r[c].to_s.size
43
+ sizes[c.to_sym] = s if s > sizes[c.to_sym]
44
+ end
45
+ end
46
+ sizes
47
+ end
48
+
49
+ # String for each data line
50
+ def self.data_line(columns, sizes, record) # :nodoc:
51
+ '|' << columns.map {|c| format_cell(sizes[c], record[c])}.join('|') << '|'
52
+ end
53
+
54
+ # Format the value so it takes up exactly size characters
55
+ def self.format_cell(size, v) # :nodoc:
56
+ case v
57
+ when Bignum, Fixnum
58
+ "%#{size}d" % v
59
+ when Float
60
+ "%#{size}g" % v
61
+ else
62
+ "%-#{size}s" % v.to_s
63
+ end
64
+ end
65
+
66
+ # String for header line
67
+ def self.header_line(columns, sizes) # :nodoc:
68
+ '|' << columns.map {|c| "%-#{sizes[c]}s" % c.to_s}.join('|') << '|'
69
+ end
70
+
71
+ # String for separtor line
72
+ def self.separator_line(columns, sizes) # :nodoc:
73
+ '+' << columns.map {|c| '-' * sizes[c]}.join('+') << '+'
74
+ end
75
+
76
+ private_class_method :column_sizes, :data_line, :format_cell, :header_line, :separator_line
77
+ end
78
+ end
79
+
@@ -1,3 +1,13 @@
1
+ # These are extensions to core classes that Sequel enables by default.
2
+ # They make using Sequel's DSL easier by adding methods to Array,
3
+ # Hash, String, and Symbol to add methods that return Sequel
4
+ # expression objects.
5
+
6
+ # This extension loads the core extensions.
7
+ def Sequel.core_extensions?
8
+ true
9
+ end
10
+
1
11
  # Sequel extends +Array+ to add methods to implement the SQL DSL.
2
12
  # Most of these methods require that the array not be empty and that it
3
13
  # must consist solely of other arrays that have exactly two elements.
@@ -8,7 +18,7 @@ class Array
8
18
  # ~[[:a, true]] # SQL: a IS NOT TRUE
9
19
  # ~[[:a, 1], [:b, [2, 3]]] # SQL: a != 1 OR b NOT IN (2, 3)
10
20
  def ~
11
- sql_expr_if_all_two_pairs(:OR, true)
21
+ Sequel.~(self)
12
22
  end
13
23
 
14
24
  # +true+ if the array is not empty and all of its elements are
@@ -21,6 +31,7 @@ class Array
21
31
  # [[:b]].to_a.all_two_pairs? # => false
22
32
  # [[:a, 1]].to_a.all_two_pairs? # => true
23
33
  def all_two_pairs?
34
+ warn('Array#all_two_pairs? is deprecated and will be removed in Sequel 3.35.0')
24
35
  !empty? && all?{|i| (Array === i) && (i.length == 2)}
25
36
  end
26
37
 
@@ -59,7 +70,7 @@ class Array
59
70
  # [[:a, true]].sql_expr # SQL: a IS TRUE
60
71
  # [[:a, 1], [:b, [2, 3]]].sql_expr # SQL: a = 1 AND b IN (2, 3)
61
72
  def sql_expr
62
- sql_expr_if_all_two_pairs
73
+ Sequel.expr(self)
63
74
  end
64
75
 
65
76
  # Return a <tt>Sequel::SQL::BooleanExpression</tt> created from this array, matching none
@@ -68,7 +79,7 @@ class Array
68
79
  # [[:a, true]].sql_negate # SQL: a IS NOT TRUE
69
80
  # [[:a, 1], [:b, [2, 3]]].sql_negate # SQL: a != 1 AND b NOT IN (2, 3)
70
81
  def sql_negate
71
- sql_expr_if_all_two_pairs(:AND, true)
82
+ Sequel.negate(self)
72
83
  end
73
84
 
74
85
  # Return a <tt>Sequel::SQL::BooleanExpression</tt> created from this array, matching any of the
@@ -77,10 +88,10 @@ class Array
77
88
  # [[:a, true]].sql_or # SQL: a IS TRUE
78
89
  # [[:a, 1], [:b, [2, 3]]].sql_or # SQL: a = 1 OR b IN (2, 3)
79
90
  def sql_or
80
- sql_expr_if_all_two_pairs(:OR)
91
+ Sequel.or(self)
81
92
  end
82
93
 
83
- # Return a <tt>Sequel::SQL::BooleanExpression</tt> representing an SQL string made up of the
94
+ # Return a <tt>Sequel::SQL::StringExpression</tt> representing an SQL string made up of the
84
95
  # concatenation of this array's elements. If an argument is passed
85
96
  # it is used in between each element of the array in the SQL
86
97
  # concatenation.
@@ -90,20 +101,14 @@ class Array
90
101
  # [:a, 'b'].sql_string_join # SQL: a || 'b'
91
102
  # ['a', :b].sql_string_join(' ') # SQL: 'a' || ' ' || b
92
103
  def sql_string_join(joiner=nil)
93
- if joiner
94
- args = zip([joiner]*length).flatten
95
- args.pop
96
- else
97
- args = self
98
- end
99
- args = args.collect{|a| [Symbol, ::Sequel::SQL::Expression, ::Sequel::LiteralString, TrueClass, FalseClass, NilClass].any?{|c| a.is_a?(c)} ? a : a.to_s}
100
- ::Sequel::SQL::StringExpression.new(:'||', *args)
104
+ Sequel.join(self, joiner)
101
105
  end
102
106
 
103
107
  private
104
108
 
105
109
  # Raise an error if this array is not made up all two element arrays, otherwise create a <tt>Sequel::SQL::BooleanExpression</tt> from this array.
106
110
  def sql_expr_if_all_two_pairs(*args)
111
+ warn('Array#sql_expr_if_all_two_pairs? is deprecated and will be removed in Sequel 3.35.0')
107
112
  raise(Sequel::Error, 'Not all elements of the array are arrays of size 2, so it cannot be converted to an SQL expression') unless all_two_pairs?
108
113
  ::Sequel::SQL::BooleanExpression.from_value_pairs(self, *args)
109
114
  end
@@ -184,6 +184,10 @@ module Sequel
184
184
  @actions << [:alter_table, table, MigrationAlterTableReverser.new.reverse(&block)]
185
185
  end
186
186
 
187
+ def create_join_table(*args)
188
+ @actions << [:drop_join_table, *args]
189
+ end
190
+
187
191
  def create_table(*args)
188
192
  @actions << [:drop_table, args.first]
189
193
  end
@@ -0,0 +1,90 @@
1
+ # The null_dataset extension adds the Dataset#nullify method, which
2
+ # returns a cloned dataset that will never issue a query to the
3
+ # database. It implements the null object pattern for datasets.
4
+ #
5
+ # The most common usage is probably in a method that must return
6
+ # a dataset, where the method knows the dataset shouldn't return
7
+ # anything. With standard Sequel, you'd probably just add a
8
+ # WHERE condition that is always false, but that still results
9
+ # in a query being sent to the database, and can be overridden
10
+ # using #unfiltered, the OR operator, or a UNION.
11
+ #
12
+ # Usage:
13
+ #
14
+ # ds = DB[:items].nullify.where(:a=>:b).select(:c)
15
+ # ds.sql # => "SELECT c FROM items WHERE (a = b)"
16
+ # ds.all # => [] # no query sent to the database
17
+ #
18
+ # Note that there is one case where a null dataset will sent
19
+ # a query to the database. If you call #columns on a nulled
20
+ # dataset and the dataset doesn't have an already cached
21
+ # version of the columns, it will create a new dataset with
22
+ # the same options to get the columns.
23
+
24
+ module Sequel
25
+ class Dataset
26
+ module NullDataset
27
+ # Create a new dataset from the dataset (which won't
28
+ # be nulled) to get the columns if they aren't already cached.
29
+ def columns
30
+ @columns ||= db.dataset(@opts).columns
31
+ end
32
+
33
+ # Return 0 without sending a database query.
34
+ def delete
35
+ 0
36
+ end
37
+
38
+ # Return self without sending a database query, never yielding.
39
+ def each
40
+ self
41
+ end
42
+
43
+ # Return nil without sending a database query, never yielding.
44
+ def fetch_rows(sql)
45
+ nil
46
+ end
47
+
48
+ # Return nil without sending a database query.
49
+ def insert(*)
50
+ nil
51
+ end
52
+
53
+ # Return nil without sending a database query.
54
+ def truncate
55
+ nil
56
+ end
57
+
58
+ # Return 0 without sending a database query.
59
+ def update(v={})
60
+ 0
61
+ end
62
+
63
+ protected
64
+
65
+ # Return nil without sending a database query.
66
+ def _import(columns, values, opts)
67
+ nil
68
+ end
69
+
70
+ private
71
+
72
+ # Just in case these are called directly by some internal code,
73
+ # make them noops. There's nothing we can do if the db
74
+ # is accessed directly to make a change, though.
75
+ (%w'_ddl _dui _insert' << '').each do |m|
76
+ class_eval("private; def execute#{m}(sql, opts={}) end", __FILE__, __LINE__)
77
+ end
78
+ end
79
+
80
+ # Return a cloned nullified dataset.
81
+ def nullify
82
+ clone.nullify!
83
+ end
84
+
85
+ # Nullify the current dataset
86
+ def nullify!
87
+ extend NullDataset
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,460 @@
1
+ # The pg_array extension adds support for Sequel to handle
2
+ # PostgreSQL's string and numeric array types. It supports both
3
+ # single and multi-dimensional arrays. For integer and
4
+ # float arrays, it uses a JSON-based parser which is usually written in C
5
+ # and should be fairly fast. For string and decimal arrays, it uses
6
+ # a hand coded parser written in ruby that is unoptimized and probably
7
+ # slow.
8
+ #
9
+ # This extension integrates with Sequel's native postgres adapter, so
10
+ # that when array fields are retrieved, they are parsed and returned
11
+ # as instances of Sequel::Postgres::PGArray subclasses. PGArray is
12
+ # a DelegateClass of Array, so it mostly acts like an array, but not
13
+ # completely (is_a?(Array) is false). If you want the actual array,
14
+ # you can call PGArray#to_a. This is done so that Sequel does not
15
+ # treat a PGArray like an Array by default, which would cause issues.
16
+ #
17
+ # In addition to the parsers, this extension comes with literalizers
18
+ # for PGArray using the standard Sequel literalization callbacks, so
19
+ # they work with on all adapters.
20
+ #
21
+ # To turn an existing Array into a PGArray:
22
+ #
23
+ # array.pg_array
24
+ #
25
+ # You can also provide a type, though it many cases it isn't necessary:
26
+ #
27
+ # array.pg_array(:varchar) # or :int4, :"double precision", etc.
28
+ #
29
+ # So if you want to insert an array into an int4[] database column:
30
+ #
31
+ # DB[:table].insert(:column=>[1, 2, 3].pg_array)
32
+ #
33
+ # If you would like to use PostgreSQL arrays in your model objects, you
34
+ # probably want to modify the schema parsing/typecasting so that it
35
+ # recognizes and correctly handles the arrays, which you can do by:
36
+ #
37
+ # DB.extend Sequel::Postgres::PGArray::DatabaseMethods
38
+ #
39
+ # If you are not using the native postgres adapter, you probably
40
+ # also want to use the typecast_on_load plugin in the model, and
41
+ # set it to typecast the array column(s) on load.
42
+ #
43
+ # If you want an easy way to call PostgreSQL array functions and
44
+ # operators, look into the pg_array_ops extension.
45
+ #
46
+ # This extension requires both the json and delegate libraries.
47
+ #
48
+ # == Additional License
49
+ #
50
+ # PGArray::Parser code was translated from Javascript code in the
51
+ # node-postgres project and has the following additional license:
52
+ #
53
+ # Copyright (c) 2010 Brian Carlson (brian.m.carlson@gmail.com)
54
+ #
55
+ # Permission is hereby granted, free of charge, to any person obtaining
56
+ # a copy of this software and associated documentation files (the
57
+ # "Software"), to deal in the Software without restriction, including
58
+ # without limitation the rights to use, copy, modify, merge, publish,
59
+ # distribute, sublicense, and/or sell copies of the Software, and to
60
+ # permit persons to whom the Software is furnished to do so, subject
61
+ # to the following conditions:
62
+ #
63
+ # The above copyright notice and this permission notice shall be included
64
+ # in all copies or substantial portions of the Software.
65
+ #
66
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
67
+ # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
68
+ # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
69
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
70
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
71
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
72
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
73
+
74
+ require 'delegate'
75
+ require 'json'
76
+
77
+ module Sequel
78
+ module Postgres
79
+ # Base class for the PostgreSQL array types. Subclasses generally
80
+ # just deal with parsing, so instances manually created from arrays
81
+ # can use this class correctly.
82
+ class PGArray < DelegateClass(Array)
83
+ ARRAY = "ARRAY".freeze
84
+ DOUBLE_COLON = '::'.freeze
85
+ EMPTY_BRACKET = '[]'.freeze
86
+ OPEN_BRACKET = '['.freeze
87
+ CLOSE_BRACKET = ']'.freeze
88
+ COMMA = ','.freeze
89
+ BACKSLASH = '\\'.freeze
90
+ EMPTY_STRING = ''.freeze
91
+ OPEN_BRACE = '{'.freeze
92
+ CLOSE_BRACE = '}'.freeze
93
+ NULL = 'NULL'.freeze
94
+ QUOTE = '"'.freeze
95
+
96
+ module DatabaseMethods
97
+ ESCAPE_RE = /("|\\)/.freeze
98
+ ESCAPE_REPLACEMENT = '\\\\\1'.freeze
99
+
100
+ # Reset the conversion procs when extending the Database object, so
101
+ # it will pick up the array convertors. This is only done for the native
102
+ # postgres adapter.
103
+ def self.extended(db)
104
+ db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
105
+ end
106
+
107
+ # Handle arrays in bound variables
108
+ def bound_variable_arg(arg, conn)
109
+ case arg
110
+ when PGArray
111
+ bound_variable_array(arg.to_a)
112
+ when Array
113
+ bound_variable_array(arg)
114
+ else
115
+ super
116
+ end
117
+ end
118
+
119
+ # Make the column type detection deal with string and numeric array types.
120
+ def schema_column_type(db_type)
121
+ case db_type
122
+ when /\A(character( varying)?|text).*\[\]\z/io
123
+ :string_array
124
+ when /\A(integer|bigint|smallint)\[\]\z/io
125
+ :integer_array
126
+ when /\A(real|double precision)\[\]\z/io
127
+ :float_array
128
+ when /\Anumeric.*\[\]\z/io
129
+ :decimal_array
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ private
136
+
137
+ # Format arrays used in bound variables.
138
+ def bound_variable_array(a)
139
+ case a
140
+ when Array
141
+ "{#{a.map{|i| bound_variable_array(i)}.join(COMMA)}}"
142
+ when String
143
+ "\"#{a.gsub(ESCAPE_RE, ESCAPE_REPLACEMENT)}\""
144
+ else
145
+ literal(a)
146
+ end
147
+ end
148
+
149
+ # Given a value to typecast and the type of PGArray subclass:
150
+ # * If given a PGArray, just return the value (even if different subclass)
151
+ # * If given an Array, create a new instance of the subclass
152
+ # * If given a String, call the parser for the subclass with it.
153
+ def typecast_value_pg_array(value, klass)
154
+ case value
155
+ when PGArray
156
+ value
157
+ when Array
158
+ klass.new(value)
159
+ when String
160
+ klass.parse(value)
161
+ else
162
+ raise Sequel::InvalidValue, "invalid value for #{klass}: #{value.inspect}"
163
+ end
164
+ end
165
+
166
+ # Create typecast methods for the supported array types that
167
+ # delegate to typecast_value_pg_array with the related class.
168
+ %w'string integer float decimal'.each do |t|
169
+ class_eval("def typecast_value_#{t}_array(v) typecast_value_pg_array(v, PG#{t.capitalize}Array) end", __FILE__, __LINE__)
170
+ end
171
+ end
172
+
173
+ # PostgreSQL array parser that handles both text and numeric
174
+ # input. Because PostgreSQL arrays can contain objects that
175
+ # can be literalized in any number of ways, it is not possible
176
+ # to make a fully generic parser.
177
+ #
178
+ # This parser is very simple and unoptimized, but should still
179
+ # be O(n) where n is the length of the input string.
180
+ class Parser
181
+ attr_reader :pos
182
+
183
+ # Set the source for the input, and any converter callable
184
+ # to call with objects to be created. For nested parsers
185
+ # the source may contain text after the end current parse,
186
+ # which will be ignored.
187
+ def initialize(source, converter=nil)
188
+ @source = source
189
+ @source_length = source.length
190
+ @converter = converter
191
+ @pos = -1
192
+ @entries = []
193
+ @recorded = ""
194
+ @dimension = 0
195
+ end
196
+
197
+ # Return 2 objects, whether the next character in the input
198
+ # was escaped with a backslash, and what the next character is.
199
+ def next_char
200
+ @pos += 1
201
+ if (c = @source[@pos..@pos]) == BACKSLASH
202
+ @pos += 1
203
+ [true, @source[@pos..@pos]]
204
+ else
205
+ [false, c]
206
+ end
207
+ end
208
+
209
+ # Add a new character to the buffer of recorded characters.
210
+ def record(c)
211
+ @recorded << c
212
+ end
213
+
214
+ # Take the buffer of recorded characters and add it to the array
215
+ # of entries, and use a new buffer for recorded characters.
216
+ def new_entry(include_empty=false)
217
+ if !@recorded.empty? || include_empty
218
+ entry = @recorded
219
+ if entry == NULL && !include_empty
220
+ entry = nil
221
+ elsif @converter
222
+ entry = @converter.call(entry)
223
+ end
224
+ @entries.push(entry)
225
+ @recorded = ""
226
+ end
227
+ end
228
+
229
+ # Parse the input character by character, returning an array
230
+ # of parsed (and potentially converted) objects.
231
+ def parse(nested=false)
232
+ # quote sets whether we are inside of a quoted string.
233
+ quote = false
234
+ until @pos >= @source_length
235
+ escaped, char = next_char
236
+ if char == OPEN_BRACE && !quote
237
+ @dimension += 1
238
+ if (@dimension > 1)
239
+ # Multi-dimensional array encounter, use a subparser
240
+ # to parse the next level down.
241
+ subparser = self.class.new(@source[@pos..-1], @converter)
242
+ @entries.push(subparser.parse(true))
243
+ @pos += subparser.pos - 1
244
+ end
245
+ elsif char == CLOSE_BRACE && !quote
246
+ @dimension -= 1
247
+ if (@dimension == 0)
248
+ new_entry
249
+ # Exit early if inside a subparser, since the
250
+ # text after parsing the current level should be
251
+ # ignored as it is handled by the parent parser.
252
+ return @entries if nested
253
+ end
254
+ elsif char == QUOTE && !escaped
255
+ # If already inside the quoted string, this is the
256
+ # ending quote, so add the entry. Otherwise, this
257
+ # is the opening quote, so set the quote flag.
258
+ new_entry(true) if quote
259
+ quote = !quote
260
+ elsif char == COMMA && !quote
261
+ # If not inside a string and a comma occurs, it indicates
262
+ # the end of the entry, so add the entry.
263
+ new_entry
264
+ else
265
+ # Add the character to the recorded character buffer.
266
+ record(char)
267
+ end
268
+ end
269
+ raise Sequel::Error, "array dimensions not balanced" unless @dimension == 0
270
+ @entries
271
+ end
272
+ end
273
+
274
+ # Parse the string using the generalized parser, setting the type
275
+ # if given.
276
+ def self.parse(string, type=nil)
277
+ new(Parser.new(string, method(:convert_item)).parse, type)
278
+ end
279
+
280
+ # Return the item as-is by default, making conversion a no-op.
281
+ def self.convert_item(s)
282
+ s
283
+ end
284
+ private_class_method :convert_item
285
+
286
+ # The type of this array. May be nil if no type was given. If a type
287
+ # is provided, the array is automatically casted to this type when
288
+ # literalizing. This type is the underlying type, not the array type
289
+ # itself, so for an int4[] database type, it should be :int4 or 'int4'
290
+ attr_accessor :array_type
291
+
292
+ # Set the array to delegate to, and a database type.
293
+ def initialize(array, type=nil)
294
+ super(array)
295
+ self.array_type = type
296
+ end
297
+
298
+ # The delegated object is always an array.
299
+ alias to_a __getobj__
300
+
301
+ # Append the array SQL to the given sql string.
302
+ # If the receiver has a type, add a cast to the
303
+ # database array type.
304
+ def sql_literal_append(ds, sql)
305
+ sql << ARRAY
306
+ _literal_append(sql, ds, to_a)
307
+ if at = array_type
308
+ sql << DOUBLE_COLON << at.to_s << EMPTY_BRACKET
309
+ end
310
+ end
311
+
312
+ private
313
+
314
+ # Recursive method that handles multi-dimensional
315
+ # arrays, surrounding each with [] and interspersing
316
+ # entries with ,.
317
+ def _literal_append(sql, ds, array)
318
+ sql << OPEN_BRACKET
319
+ comma = false
320
+ commas = COMMA
321
+ array.each do |i|
322
+ sql << commas if comma
323
+ if i.is_a?(Array)
324
+ _literal_append(sql, ds, i)
325
+ else
326
+ ds.literal_append(sql, i)
327
+ end
328
+ comma = true
329
+ end
330
+ sql << CLOSE_BRACKET
331
+ end
332
+ end
333
+
334
+ # PGArray subclass handling integer and float types, using a fast JSON
335
+ # parser. Does not handle numeric/decimal types, since JSON does deal
336
+ # with arbitrary precision (see PGDecimalArray for that).
337
+ class PGNumericArray < PGArray
338
+ # Character conversion map mapping input strings to JSON replacements
339
+ SUBST = {'{'.freeze=>'['.freeze, '}'.freeze=>']'.freeze, 'NULL'.freeze=>'null'.freeze}
340
+
341
+ # Regular expression matching input strings to convert
342
+ SUBST_RE = %r[\{|\}|NULL].freeze
343
+
344
+ # Parse the input string by using a gsub to convert non-JSON characters to
345
+ # JSON, running it through a regular JSON parser, and the doing a recursive
346
+ # map over the output to make sure the entries are in the correct type (mostly,
347
+ # to make sure real/double precision database types are returned as float and
348
+ # not integer).
349
+ def self.parse(string, type=nil)
350
+ new(recursive_map(JSON.parse(string.gsub(SUBST_RE){|m| SUBST[m]})), type)
351
+ end
352
+
353
+ # Convert each item in the array to the correct type, handling multi-dimensional
354
+ # arrays.
355
+ def self.recursive_map(array)
356
+ array.map do |i|
357
+ if i.is_a?(Array)
358
+ recursive_map(i)
359
+ elsif i
360
+ convert_item(i)
361
+ end
362
+ end
363
+ end
364
+ private_class_method :recursive_map
365
+ end
366
+
367
+ # PGArray subclass for decimal/numeric types. Uses the general
368
+ # parser as the JSON parser cannot handle arbitrary precision numbers.
369
+ class PGDecimalArray < PGArray
370
+ # Convert the item to a BigDecimal.
371
+ def self.convert_item(s)
372
+ BigDecimal.new(s.to_s)
373
+ end
374
+ private_class_method :convert_item
375
+
376
+ ARRAY_TYPE = 'decimal'.freeze
377
+
378
+ # Use the decimal type by default.
379
+ def array_type
380
+ super || ARRAY_TYPE
381
+ end
382
+ end
383
+
384
+ # PGArray subclass for handling real/double precision arrays.
385
+ class PGFloatArray < PGNumericArray
386
+ # Convert the item to a float.
387
+ def self.convert_item(s)
388
+ s.to_f
389
+ end
390
+ private_class_method :convert_item
391
+
392
+ ARRAY_TYPE = 'double precision'.freeze
393
+
394
+ # Use the double precision type by default.
395
+ def array_type
396
+ super || ARRAY_TYPE
397
+ end
398
+ end
399
+
400
+ # PGArray subclass for handling int2/int4/int8 arrays.
401
+ class PGIntegerArray < PGNumericArray
402
+ ARRAY_TYPE = 'int4'.freeze
403
+
404
+ # Use the int4 type by default.
405
+ def array_type
406
+ super || ARRAY_TYPE
407
+ end
408
+ end
409
+
410
+ # PGArray subclass for handling char/varchar/text arrays.
411
+ class PGStringArray < PGArray
412
+ CHAR = 'char'.freeze
413
+ VARCHAR = 'varchar'.freeze
414
+ TEXT = 'text'.freeze
415
+
416
+ # By default, use a text array. If char is given without
417
+ # a size, use varchar instead, as otherwise Postgres assumes
418
+ # length of 1, which is likely to cause data loss.
419
+ def array_type
420
+ case (c = super)
421
+ when nil
422
+ TEXT
423
+ when CHAR, :char
424
+ VARCHAR
425
+ else
426
+ c
427
+ end
428
+ end
429
+ end
430
+
431
+ PG_TYPES = {} unless defined?(PG_TYPES)
432
+
433
+ # Automatically convert the built-in numeric and text array
434
+ # types. to PGArray instances on retrieval if the native
435
+ # postgres adapter is used.
436
+ [ [1005, PGIntegerArray, 'int2'.freeze],
437
+ [1007, PGIntegerArray, 'int4'.freeze],
438
+ [1016, PGIntegerArray, 'int8'.freeze],
439
+ [1021, PGFloatArray, 'real'.freeze],
440
+ [1022, PGFloatArray, 'double precision'.freeze],
441
+ [1231, PGDecimalArray, 'numeric'.freeze],
442
+ [1009, PGStringArray, 'text'.freeze],
443
+ [1014, PGStringArray, 'char'.freeze],
444
+ [1015, PGStringArray, 'varchar'.freeze]
445
+ ].each do |ftype, klass, type|
446
+ meth = klass.method(:parse)
447
+ PG_TYPES[ftype] = lambda{|s| meth.call(s, type)}
448
+ end
449
+ end
450
+ end
451
+
452
+ class Array
453
+ # Return a PGArray proxy to the receiver, using a
454
+ # specific database type if given. This is mostly useful
455
+ # as a short cut for creating PGArray objects that didn't
456
+ # come from the database.
457
+ def pg_array(type=nil)
458
+ Sequel::Postgres::PGArray.new(self, type)
459
+ end
460
+ end