sequel 3.33.0 → 3.34.0

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