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,220 @@
1
+ # The pg_array_ops extension adds support to Sequel's DSL to make
2
+ # it easier to call PostgreSQL array functions and operators. The
3
+ # most common usage is taking an object that represents an SQL
4
+ # identifier (such as a :symbol), and calling #pg_array on it:
5
+ #
6
+ # ia = :int_array_column.pg_array
7
+ #
8
+ # This creates a Sequel::Postgres::ArrayOp object that can be used
9
+ # for easier querying:
10
+ #
11
+ # ia[1] # int_array_column[1]
12
+ # ia[1][2] # int_array_column[1][2]
13
+ #
14
+ # ia.contains(:other_int_array_column) # @>
15
+ # ia.contained_by(:other_int_array_column) # <@
16
+ # ia.overlaps(:other_int_array_column) # &&
17
+ # ia.concat(:other_int_array_column) # ||
18
+ #
19
+ # ia.push(1) # int_array_column || 1
20
+ # ia.unshift(1) # 1 || int_array_column
21
+ #
22
+ # ia.any # ANY(int_array_column)
23
+ # ia.all # ALL(int_array_column)
24
+ # ia.dims # array_dims(int_array_column)
25
+ # ia.length # array_length(int_array_column, 1)
26
+ # ia.length(2) # array_length(int_array_column, 2)
27
+ # ia.lower # array_lower(int_array_column, 1)
28
+ # ia.lower(2) # array_lower(int_array_column, 2)
29
+ # ia.join # array_to_string(int_array_column, '', NULL)
30
+ # ia.join(':') # array_to_string(int_array_column, ':', NULL)
31
+ # ia.join(':', ' ') # array_to_string(int_array_column, ':', ' ')
32
+ # ia.unnest # unnest(int_array_column)
33
+ #
34
+ # See the PostgreSQL array function and operator documentation for more
35
+ # details on what these functions and operators do.
36
+ #
37
+ # If you are also using the pg_array extension, you should load it before
38
+ # loading this extension. Doing so will allow you to use PGArray#op to get
39
+ # an ArrayOp, allowing you to perform array operations on array literals.
40
+ module Sequel
41
+ module Postgres
42
+ # The ArrayOp class is a simple container for a single object that
43
+ # defines methods that yield Sequel expression objects representing
44
+ # PostgreSQL array operators and functions.
45
+ #
46
+ # In the method documentation examples, assume that:
47
+ #
48
+ # array_op = :array.pg_array
49
+ class ArrayOp < Sequel::SQL::Wrapper
50
+ CONCAT = ["(".freeze, " || ".freeze, ")".freeze].freeze
51
+ CONTAINS = ["(".freeze, " @> ".freeze, ")".freeze].freeze
52
+ CONTAINED_BY = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
53
+ OVERLAPS = ["(".freeze, " && ".freeze, ")".freeze].freeze
54
+
55
+ # Access a member of the array, returns an SQL::Subscript instance:
56
+ #
57
+ # array_op[1] # array[1]
58
+ def [](key)
59
+ Sequel::SQL::Subscript.new(self, [key])
60
+ end
61
+
62
+ # Call the ALL function:
63
+ #
64
+ # array_op.all # ALL(array)
65
+ #
66
+ # Usually used like:
67
+ #
68
+ # dataset.where(1=>array_op.all)
69
+ # # WHERE (1 = ALL(array))
70
+ def all
71
+ function(:ALL)
72
+ end
73
+
74
+ # Call the ANY function:
75
+ #
76
+ # array_op.all # ANY(array)
77
+ #
78
+ # Usually used like:
79
+ #
80
+ # dataset.where(1=>array_op.any)
81
+ # # WHERE (1 = ANY(array))
82
+ def any
83
+ function(:ANY)
84
+ end
85
+
86
+ # Use the contains (@>) operator:
87
+ #
88
+ # array_op.contains(:a) # (array @> a)
89
+ def contains(other)
90
+ bool_op(CONTAINS, other)
91
+ end
92
+
93
+ # Use the contained by (<@) operator:
94
+ #
95
+ # array_op.contained_by(:a) # (array <@ a)
96
+ def contained_by(other)
97
+ bool_op(CONTAINED_BY, other)
98
+ end
99
+
100
+ # Call the array_dims method:
101
+ #
102
+ # array_op.dims # array_dims(array)
103
+ def dims
104
+ function(:array_dims)
105
+ end
106
+
107
+ # Call the array_length method:
108
+ #
109
+ # array_op.length # array_length(array, 1)
110
+ # array_op.length(2) # array_length(array, 2)
111
+ def length(dimension = 1)
112
+ function(:array_length, dimension)
113
+ end
114
+
115
+ # Call the array_lower method:
116
+ #
117
+ # array_op.lower # array_lower(array, 1)
118
+ # array_op.lower(2) # array_lower(array, 2)
119
+ def lower(dimension = 1)
120
+ function(:array_lower, dimension)
121
+ end
122
+
123
+ # Use the overlaps (&&) operator:
124
+ #
125
+ # array_op.overlaps(:a) # (array && a)
126
+ def overlaps(other)
127
+ bool_op(OVERLAPS, other)
128
+ end
129
+
130
+ # Use the concatentation (||) operator:
131
+ #
132
+ # array_op.push(:a) # (array || a)
133
+ # array_op.concat(:a) # (array || a)
134
+ def push(other)
135
+ array_op(CONCAT, [self, other])
136
+ end
137
+ alias concat push
138
+
139
+ # Return the receiver.
140
+ def pg_array
141
+ self
142
+ end
143
+
144
+ # Call the array_to_string method:
145
+ #
146
+ # array_op.join # array_to_string(array, '', NULL)
147
+ # array_op.to_string # array_to_string(array, '', NULL)
148
+ # array_op.join(":") # array_to_string(array, ':', NULL)
149
+ # array_op.join(":", "*") # array_to_string(array, ':', '*')
150
+ def to_string(joiner="", null=nil)
151
+ function(:array_to_string, joiner, null)
152
+ end
153
+ alias join to_string
154
+
155
+ # Call the unnest method:
156
+ #
157
+ # array_op.unnest # unnest(array)
158
+ def unnest
159
+ function(:unnest)
160
+ end
161
+
162
+ # Use the concatentation (||) operator, reversing the order:
163
+ #
164
+ # array_op.unshift(:a) # (a || array)
165
+ def unshift(other)
166
+ array_op(CONCAT, [other, self])
167
+ end
168
+
169
+ private
170
+
171
+ # Return a placeholder literal with the given str and args, wrapped
172
+ # in an ArrayOp, used by operators that return arrays.
173
+ def array_op(str, args)
174
+ ArrayOp.new(Sequel::SQL::PlaceholderLiteralString.new(str, args))
175
+ end
176
+
177
+ # Return a placeholder literal with the given str and args, wrapped
178
+ # in a boolean expression, used by operators that return booleans.
179
+ def bool_op(str, other)
180
+ Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(str, [value, other]))
181
+ end
182
+
183
+ # Return a function with the given name, and the receiver as the first
184
+ # argument, with any additional arguments given.
185
+ def function(name, *args)
186
+ SQL::Function.new(name, self, *args)
187
+ end
188
+ end
189
+
190
+ module ArrayOpMethods
191
+ # Wrap the receiver in an ArrayOp so you can easily use the PostgreSQL
192
+ # array functions and operators with it.
193
+ def pg_array
194
+ ArrayOp.new(self)
195
+ end
196
+ end
197
+
198
+ if defined?(PGArray)
199
+ class PGArray
200
+ # Wrap the PGArray instance in an ArrayOp, allowing you to easily use
201
+ # the PostgreSQL array functions and operators with literal arrays.
202
+ def op
203
+ ArrayOp.new(self)
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ class SQL::GenericExpression
210
+ include Sequel::Postgres::ArrayOpMethods
211
+ end
212
+
213
+ class LiteralString
214
+ include Sequel::Postgres::ArrayOpMethods
215
+ end
216
+ end
217
+
218
+ class Symbol
219
+ include Sequel::Postgres::ArrayOpMethods
220
+ end
@@ -0,0 +1,174 @@
1
+ # This extension allows Sequel's postgres adapter to automatically
2
+ # parameterize all common queries. Sequel's default behavior has always
3
+ # been to literalize all arguments unless specifically using
4
+ # parameters (via :$arg placeholders and the prepare/call methods).
5
+ # This extension makes Sequel take all string, numeric, date, and
6
+ # time types and automatically turn them into parameters. Example:
7
+ #
8
+ # # Default
9
+ # DB[:test].where(:a=>1)
10
+ # # SQL: SELECT * FROM test WHERE a = 1
11
+ #
12
+ # DB.extend Sequel::Postgres::AutoParameterize::DatabaseMethods
13
+ # DB[:test].where(:a=>1)
14
+ # # SQL: SELECT * FROM test WHERE a = $1 (args: [1])
15
+ #
16
+ # This extension is not necessarily faster or more safe than the
17
+ # default behavior. In some cases it is faster, such as when using
18
+ # large strings. However, there are also some known issues with
19
+ # this approach:
20
+ #
21
+ # 1. Because of the way it operates, it has no context to make a
22
+ # determination about whether to literalize an object or not.
23
+ # For example, if it comes across an integer, it will turn it
24
+ # into a parameter. That breaks code such as:
25
+ #
26
+ # DB[:table].select(:a, :b).order(2, 1)
27
+ #
28
+ # Since it will use the following SQL (which isn't valid):
29
+ #
30
+ # SELECT a, b FROM table ORDER BY $1, $2
31
+ #
32
+ # To work around this, you can either specify the columns
33
+ # manually or use a literal string:
34
+ #
35
+ # DB[:table].select(:a, :b).order(:b, :a)
36
+ # DB[:table].select(:a, :b).order('2, 1'.lit)
37
+ #
38
+ # 2. In order to avoid many type errors, it attempts to guess the
39
+ # appropriate type and automatically casts all placeholders.
40
+ # Unfortunately, if the type guess is incorrect, the query will
41
+ # be rejected. For example, the following works without
42
+ # automatic parameterization, but fails with it:
43
+ #
44
+ # DB[:table].insert(:interval=>'1 day')
45
+ #
46
+ # To work around this, you can just add the necessary casts
47
+ # manually:
48
+ #
49
+ # DB[:table].insert(:interval=>'1 day'.cast(:interval))
50
+ #
51
+ # You can also work around any issues that come up by disabling automatic
52
+ # parameterization by calling the no_auto_parameterize method on the
53
+ # dataset (which returns a clone of the dataset).
54
+ #
55
+ # It is likely there are other corner cases I am not yet aware of
56
+ # when using this extension, so use this extension with caution.
57
+ #
58
+ # This extension is only compatible when using the pg driver, not
59
+ # when using the old postgres driver or the postgres-pr driver.
60
+
61
+ module Sequel
62
+ module Postgres
63
+ # Enable automatically parameterizing queries by hijacking the
64
+ # SQL query string that Sequel builds to also hold the array
65
+ # of parameters.
66
+ module AutoParameterize
67
+ # String that holds an array of parameters
68
+ class StringWithArray < ::String
69
+ # The array of parameters used by this query.
70
+ attr_reader :args
71
+
72
+ # Add a new parameter to this query, which adds
73
+ # the parameter to the array of parameters, and an
74
+ # SQL placeholder to the query itself.
75
+ def add_arg(s, type)
76
+ @args ||= []
77
+ @args << s
78
+ self << "$#{@args.length}::#{type}"
79
+ end
80
+
81
+ # Show args when the string is inspected
82
+ def inspect
83
+ @args ? "#{self}; #{@args.inspect}".inspect : super
84
+ end
85
+ end
86
+
87
+ module DatabaseMethods
88
+ # Extend the database's datasets with the necessary code.
89
+ def self.extended(db)
90
+ db.extend_datasets(DatasetMethods)
91
+ end
92
+
93
+ # If the sql string has an embedded parameter array,
94
+ # extract the arguments from that.
95
+ def execute(sql, opts={})
96
+ if sql.is_a?(StringWithArray) && (args = sql.args)
97
+ opts = opts.merge(:arguments=>args)
98
+ end
99
+ super
100
+ end
101
+
102
+ # If the sql string has an embedded parameter array,
103
+ # extract the arguments from that.
104
+ def execute_insert(sql, opts={})
105
+ if sql.is_a?(StringWithArray) && (args = sql.args)
106
+ opts = opts.merge(:arguments=>args)
107
+ end
108
+ super
109
+ end
110
+ end
111
+
112
+ module DatasetMethods
113
+ # Return a clone of the dataset that will not do
114
+ # automatic parameterization.
115
+ def no_auto_parameterize
116
+ clone(:no_auto_parameterize=>true)
117
+ end
118
+
119
+ # For strings, numeric arguments, and date/time arguments, add
120
+ # them as parameters to the query instead of literalizing them
121
+ # into the SQL.
122
+ def literal_append(sql, v)
123
+ if sql.is_a?(StringWithArray)
124
+ case v
125
+ when String
126
+ case v
127
+ when LiteralString
128
+ super
129
+ when Sequel::SQL::Blob
130
+ sql.add_arg(v, :bytea)
131
+ else
132
+ sql.add_arg(v, :text)
133
+ end
134
+ when Bignum
135
+ sql.add_arg(v, :int8)
136
+ when Fixnum
137
+ sql.add_arg(v, :int4)
138
+ when Float
139
+ sql.add_arg(v, :"double precision")
140
+ when BigDecimal
141
+ sql.add_arg(v, :numeric)
142
+ when Sequel::SQLTime
143
+ sql.add_arg(v, :time)
144
+ when Time, DateTime
145
+ sql.add_arg(v, :timestamp)
146
+ when Date
147
+ sql.add_arg(v, :date)
148
+ else
149
+ super
150
+ end
151
+ else
152
+ super
153
+ end
154
+ end
155
+
156
+ protected
157
+
158
+ # Disable automatic parameterization for prepared statements,
159
+ # since they will use manual parameterization.
160
+ def to_prepared_statement(*a)
161
+ opts[:no_auto_parameterize] ? super : no_auto_parameterize.to_prepared_statement(*a)
162
+ end
163
+
164
+ private
165
+
166
+ # Unless auto parameterization is turned off, use a string that
167
+ # can store the parameterized arguments.
168
+ def sql_string_origin
169
+ opts[:no_auto_parameterize] ? super : StringWithArray.new
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,296 @@
1
+ # The pg_hstore extension adds support for the PostgreSQL hstore type
2
+ # to Sequel. hstore is an extension that ships with PostgreSQL, and
3
+ # the hstore type stores an arbitrary key-value table, where the keys
4
+ # are strings and the values are strings or NULL.
5
+ #
6
+ # This extension integrates with Sequel's native postgres adapter, so
7
+ # that when hstore fields are retrieved, they are parsed and returned
8
+ # as instances of Sequel::Postgres::HStore. HStore is
9
+ # a DelegateClass of Hash, so it mostly acts like a hash, but not
10
+ # completely (is_a?(Hash) is false). If you want the actual hash,
11
+ # you can call Hstore#to_hash. This is done so that Sequel does not
12
+ # treat a HStore like a Hash by default, which would cause issues.
13
+ #
14
+ # In addition to the parsers, this extension comes with literalizers
15
+ # for HStore using the standard Sequel literalization callbacks, so
16
+ # they work with on all adapters.
17
+ #
18
+ # To turn an existing Hash into an HStore:
19
+ #
20
+ # hash.hstore
21
+ #
22
+ # Since the hstore type only supports strings, non string keys and
23
+ # values are converted to strings
24
+ #
25
+ # {:foo=>1}.hstore.to_hash # {'foo'=>'1'}
26
+ # v = {}.hstore
27
+ # v[:foo] = 1
28
+ # v # {'foo'=>'1'}
29
+ #
30
+ # However, to make life easier, lookups by key are converted to
31
+ # strings (even when accessing the underlying hash directly):
32
+ #
33
+ # {'foo'=>'bar'}.hstore[:foo] # 'bar'
34
+ # {'foo'=>'bar'}.hstore.to_hash[:foo] # 'bar'
35
+ #
36
+ # HStore instances mostly just delegate to the underlying hash
37
+ # instance, so Hash methods that modify the receiver or returned
38
+ # modified copies of the receiver may not do string conversion.
39
+ # The following methods will handle string conversion, and more
40
+ # can be added later if desired:
41
+ #
42
+ # * \[\]
43
+ # * \[\]=
44
+ # * assoc (ruby 1.9 only)
45
+ # * delete
46
+ # * fetch
47
+ # * has_key?
48
+ # * has_value?
49
+ # * include?
50
+ # * key (ruby 1.9 only)
51
+ # * key?
52
+ # * member?
53
+ # * merge
54
+ # * merge!
55
+ # * rassoc (ruby 1.9 only)
56
+ # * replace
57
+ # * store
58
+ # * update
59
+ # * value?
60
+ #
61
+ # If you want to insert a hash into an hstore database column:
62
+ #
63
+ # DB[:table].insert(:column=>{'foo'=>'bar'}.hstore)
64
+ #
65
+ # If you would like to use hstore columns in your model objects, you
66
+ # probably want to modify the schema parsing/typecasting so that it
67
+ # recognizes and correctly handles the hstore columns, which you can
68
+ # do by:
69
+ #
70
+ # DB.extend Sequel::Postgres::HStore::DatabaseMethods
71
+ #
72
+ # If you are not using the native postgres adapter, you probably
73
+ # also want to use the typecast_on_load plugin in the model, and
74
+ # set it to typecast the hstore column(s) on load.
75
+ #
76
+ # This extension requires the delegate and strscan libraries.
77
+
78
+ require 'delegate'
79
+ require 'strscan'
80
+
81
+ module Sequel
82
+ module Postgres
83
+ class HStore < DelegateClass(Hash)
84
+ # Parser for PostgreSQL hstore output format.
85
+ class Parser < StringScanner
86
+ QUOTE_RE = /"/.freeze
87
+ KV_SEP_RE = /"\s*=>\s*/.freeze
88
+ NULL_RE = /NULL/.freeze
89
+ SEP_RE = /,\s*/.freeze
90
+ QUOTED_RE = /(\\"|[^"])*/.freeze
91
+ REPLACE_RE = /\\(.)/.freeze
92
+ REPLACE_WITH = '\1'.freeze
93
+
94
+ # Parse the output format that PostgreSQL uses for hstore
95
+ # columns. Note that this does not attempt to parse all
96
+ # input formats that PostgreSQL will accept. For instance,
97
+ # it expects all keys and non-NULL values to be quoted.
98
+ #
99
+ # Return the resulting hash of objects. This can be called
100
+ # multiple times, it will cache the parsed hash on the first
101
+ # call and use it for subsequent calls.
102
+ def parse
103
+ return @result if @result
104
+ hash = {}
105
+ while !eos?
106
+ skip(QUOTE_RE)
107
+ k = parse_quoted
108
+ skip(KV_SEP_RE)
109
+ if skip(QUOTE_RE)
110
+ v = parse_quoted
111
+ skip(QUOTE_RE)
112
+ else
113
+ scan(NULL_RE)
114
+ v = nil
115
+ end
116
+ skip(SEP_RE)
117
+ hash[k] = v
118
+ end
119
+ @result = hash
120
+ end
121
+
122
+ private
123
+
124
+ # Parse and unescape a quoted key/value.
125
+ def parse_quoted
126
+ scan(QUOTED_RE).gsub(REPLACE_RE, REPLACE_WITH)
127
+ end
128
+ end
129
+
130
+ module DatabaseMethods
131
+ # Reset the conversion procs if using the native postgres adapter.
132
+ def self.extended(db)
133
+ db.reset_conversion_procs if db.respond_to?(:reset_conversion_procs)
134
+ end
135
+
136
+ # Handle hstores in bound variables
137
+ def bound_variable_arg(arg, conn)
138
+ case arg
139
+ when HStore
140
+ arg.unquoted_literal
141
+ when Hash
142
+ HStore.new(arg).unquoted_literal
143
+ else
144
+ super
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ # Recognize the hstore database type.
151
+ def schema_column_type(db_type)
152
+ db_type == 'hstore' ? :hstore : super
153
+ end
154
+
155
+ # Typecast value correctly to HStore. If already an
156
+ # HStore instance, return as is. If a hash, return
157
+ # an HStore version of it. If a string, assume it is
158
+ # in PostgreSQL output format and parse it using the
159
+ # parser.
160
+ def typecast_value_hstore(value)
161
+ case value
162
+ when HStore
163
+ value
164
+ when Hash
165
+ HStore.new(value)
166
+ when String
167
+ HStore.parse(value)
168
+ else
169
+ raise Sequel::InvalidValue, "invalid value for hstore: #{value.inspect}"
170
+ end
171
+ end
172
+ end
173
+
174
+ # Default proc used for all underlying HStore hashes, so that even
175
+ # if you grab the underlying hash, it will still convert non-string
176
+ # keys to strings during lookup.
177
+ DEFAULT_PROC = lambda{|h, k| h[k.to_s] unless k.is_a?(String)}
178
+
179
+ QUOTE = '"'.freeze
180
+ COMMA = ",".freeze
181
+ KV_SEP = "=>".freeze
182
+ NULL = "NULL".freeze
183
+ ESCAPE_RE = /("|\\)/.freeze
184
+ ESCAPE_REPLACE = '\\\\\1'.freeze
185
+ HSTORE_CAST = '::hstore'.freeze
186
+
187
+ # Parse the given string into an HStore, assuming the str is in PostgreSQL
188
+ # hstore output format.
189
+ def self.parse(str)
190
+ new(Parser.new(str).parse)
191
+ end
192
+
193
+ # Override methods that accept key argument to convert to string.
194
+ (%w'[] delete has_key? include? key? member?' + Array((%w'assoc' if RUBY_VERSION >= '1.9.0'))).each do |m|
195
+ class_eval("def #{m}(k) super(k.to_s) end", __FILE__, __LINE__)
196
+ end
197
+
198
+ # Override methods that accept value argument to convert to string unless nil.
199
+ (%w'has_value? value?' + Array((%w'key rassoc' if RUBY_VERSION >= '1.9.0'))).each do |m|
200
+ class_eval("def #{m}(v) super(convert_value(v)) end", __FILE__, __LINE__)
201
+ end
202
+
203
+ # Override methods that accept key and value arguments to convert to string appropriately.
204
+ %w'[]= store'.each do |m|
205
+ class_eval("def #{m}(k, v) super(k.to_s, convert_value(v)) end", __FILE__, __LINE__)
206
+ end
207
+
208
+ # Override methods that take hashes to convert the hashes to using strings for keys and
209
+ # values before using them.
210
+ %w'initialize merge! update replace'.each do |m|
211
+ class_eval("def #{m}(h, &block) super(convert_hash(h), &block) end", __FILE__, __LINE__)
212
+ end
213
+
214
+ # Override to force the key argument to a string.
215
+ def fetch(key, *args, &block)
216
+ super(key.to_s, *args, &block)
217
+ end
218
+
219
+ # Convert the input hash to string keys and values before merging,
220
+ # and return a new HStore instance with the merged hash.
221
+ def merge(hash, &block)
222
+ self.class.new(super(convert_hash(hash), &block))
223
+ end
224
+
225
+ # Return the underlying hash used by this HStore instance.
226
+ alias to_hash __getobj__
227
+
228
+ # Append a literalize version of the hstore to the sql.
229
+ def sql_literal_append(ds, sql)
230
+ ds.literal_append(sql, unquoted_literal)
231
+ sql << HSTORE_CAST
232
+ end
233
+
234
+ # Return a string containing the unquoted, unstring-escaped
235
+ # literal version of the hstore. Separated out for use by
236
+ # the bound argument code.
237
+ def unquoted_literal
238
+ str = ''
239
+ comma = false
240
+ commas = COMMA
241
+ quote = QUOTE
242
+ kv_sep = KV_SEP
243
+ null = NULL
244
+ each do |k, v|
245
+ str << commas if comma
246
+ str << quote << escape_value(k) << quote
247
+ str << kv_sep
248
+ if v.nil?
249
+ str << null
250
+ else
251
+ str << quote << escape_value(v) << quote
252
+ end
253
+ comma = true
254
+ end
255
+ str
256
+ end
257
+
258
+ private
259
+
260
+ # Return a new hash based on the input hash with string
261
+ # keys and string or nil values.
262
+ def convert_hash(h)
263
+ hash = Hash.new(&DEFAULT_PROC)
264
+ h.each{|k,v| hash[k.to_s] = convert_value(v)}
265
+ hash
266
+ end
267
+
268
+ # Return value v as a string unless it is already nil.
269
+ def convert_value(v)
270
+ v.to_s unless v.nil?
271
+ end
272
+
273
+ # Escape key/value strings when literalizing to
274
+ # correctly handle backslash and quote characters.
275
+ def escape_value(k)
276
+ k.to_s.gsub(ESCAPE_RE, ESCAPE_REPLACE)
277
+ end
278
+ end
279
+
280
+ PG_NAMED_TYPES = {} unless defined?(PG_NAMED_TYPES)
281
+ # Associate the named types by default.
282
+ PG_NAMED_TYPES[:hstore] = HStore.method(:parse)
283
+ end
284
+ end
285
+
286
+ class Hash
287
+ # Create a new HStore using the receiver as the input
288
+ # hash. Note that the HStore created will not use the
289
+ # receiver as the backing store, since it has to
290
+ # modify the hash. To get the new backing store, use:
291
+ #
292
+ # hash.hstore.to_hash
293
+ def hstore
294
+ Sequel::Postgres::HStore.new(self)
295
+ end
296
+ end