sskirby-activerecord 3.2.1
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.
- data/CHANGELOG.md +6749 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +222 -0
- data/examples/associations.png +0 -0
- data/examples/performance.rb +177 -0
- data/examples/simple.rb +14 -0
- data/lib/active_record.rb +147 -0
- data/lib/active_record/aggregations.rb +255 -0
- data/lib/active_record/associations.rb +1604 -0
- data/lib/active_record/associations/alias_tracker.rb +79 -0
- data/lib/active_record/associations/association.rb +239 -0
- data/lib/active_record/associations/association_scope.rb +119 -0
- data/lib/active_record/associations/belongs_to_association.rb +79 -0
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
- data/lib/active_record/associations/builder/association.rb +55 -0
- data/lib/active_record/associations/builder/belongs_to.rb +85 -0
- data/lib/active_record/associations/builder/collection_association.rb +75 -0
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
- data/lib/active_record/associations/builder/has_many.rb +71 -0
- data/lib/active_record/associations/builder/has_one.rb +62 -0
- data/lib/active_record/associations/builder/singular_association.rb +32 -0
- data/lib/active_record/associations/collection_association.rb +574 -0
- data/lib/active_record/associations/collection_proxy.rb +132 -0
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
- data/lib/active_record/associations/has_many_association.rb +108 -0
- data/lib/active_record/associations/has_many_through_association.rb +180 -0
- data/lib/active_record/associations/has_one_association.rb +73 -0
- data/lib/active_record/associations/has_one_through_association.rb +36 -0
- data/lib/active_record/associations/join_dependency.rb +214 -0
- data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
- data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
- data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
- data/lib/active_record/associations/join_helper.rb +55 -0
- data/lib/active_record/associations/preloader.rb +177 -0
- data/lib/active_record/associations/preloader/association.rb +127 -0
- data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
- data/lib/active_record/associations/preloader/collection_association.rb +24 -0
- data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
- data/lib/active_record/associations/preloader/has_many.rb +17 -0
- data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
- data/lib/active_record/associations/preloader/has_one.rb +23 -0
- data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
- data/lib/active_record/associations/preloader/singular_association.rb +21 -0
- data/lib/active_record/associations/preloader/through_association.rb +67 -0
- data/lib/active_record/associations/singular_association.rb +64 -0
- data/lib/active_record/associations/through_association.rb +83 -0
- data/lib/active_record/attribute_assignment.rb +221 -0
- data/lib/active_record/attribute_methods.rb +272 -0
- data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
- data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
- data/lib/active_record/attribute_methods/dirty.rb +101 -0
- data/lib/active_record/attribute_methods/primary_key.rb +114 -0
- data/lib/active_record/attribute_methods/query.rb +39 -0
- data/lib/active_record/attribute_methods/read.rb +135 -0
- data/lib/active_record/attribute_methods/serialization.rb +93 -0
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
- data/lib/active_record/attribute_methods/write.rb +69 -0
- data/lib/active_record/autosave_association.rb +422 -0
- data/lib/active_record/base.rb +716 -0
- data/lib/active_record/callbacks.rb +275 -0
- data/lib/active_record/coders/yaml_column.rb +41 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
- data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
- data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
- data/lib/active_record/connection_adapters/column.rb +270 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
- data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
- data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
- data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
- data/lib/active_record/counter_cache.rb +119 -0
- data/lib/active_record/dynamic_finder_match.rb +56 -0
- data/lib/active_record/dynamic_matchers.rb +79 -0
- data/lib/active_record/dynamic_scope_match.rb +23 -0
- data/lib/active_record/errors.rb +195 -0
- data/lib/active_record/explain.rb +85 -0
- data/lib/active_record/explain_subscriber.rb +21 -0
- data/lib/active_record/fixtures.rb +906 -0
- data/lib/active_record/fixtures/file.rb +65 -0
- data/lib/active_record/identity_map.rb +156 -0
- data/lib/active_record/inheritance.rb +167 -0
- data/lib/active_record/integration.rb +49 -0
- data/lib/active_record/locale/en.yml +40 -0
- data/lib/active_record/locking/optimistic.rb +183 -0
- data/lib/active_record/locking/pessimistic.rb +77 -0
- data/lib/active_record/log_subscriber.rb +68 -0
- data/lib/active_record/migration.rb +765 -0
- data/lib/active_record/migration/command_recorder.rb +105 -0
- data/lib/active_record/model_schema.rb +366 -0
- data/lib/active_record/nested_attributes.rb +469 -0
- data/lib/active_record/observer.rb +121 -0
- data/lib/active_record/persistence.rb +372 -0
- data/lib/active_record/query_cache.rb +74 -0
- data/lib/active_record/querying.rb +58 -0
- data/lib/active_record/railtie.rb +119 -0
- data/lib/active_record/railties/console_sandbox.rb +6 -0
- data/lib/active_record/railties/controller_runtime.rb +49 -0
- data/lib/active_record/railties/databases.rake +620 -0
- data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
- data/lib/active_record/readonly_attributes.rb +26 -0
- data/lib/active_record/reflection.rb +534 -0
- data/lib/active_record/relation.rb +534 -0
- data/lib/active_record/relation/batches.rb +90 -0
- data/lib/active_record/relation/calculations.rb +354 -0
- data/lib/active_record/relation/delegation.rb +49 -0
- data/lib/active_record/relation/finder_methods.rb +398 -0
- data/lib/active_record/relation/predicate_builder.rb +58 -0
- data/lib/active_record/relation/query_methods.rb +417 -0
- data/lib/active_record/relation/spawn_methods.rb +148 -0
- data/lib/active_record/result.rb +34 -0
- data/lib/active_record/sanitization.rb +194 -0
- data/lib/active_record/schema.rb +58 -0
- data/lib/active_record/schema_dumper.rb +204 -0
- data/lib/active_record/scoping.rb +152 -0
- data/lib/active_record/scoping/default.rb +142 -0
- data/lib/active_record/scoping/named.rb +202 -0
- data/lib/active_record/serialization.rb +18 -0
- data/lib/active_record/serializers/xml_serializer.rb +202 -0
- data/lib/active_record/session_store.rb +358 -0
- data/lib/active_record/store.rb +50 -0
- data/lib/active_record/test_case.rb +73 -0
- data/lib/active_record/timestamp.rb +113 -0
- data/lib/active_record/transactions.rb +360 -0
- data/lib/active_record/translation.rb +22 -0
- data/lib/active_record/validations.rb +83 -0
- data/lib/active_record/validations/associated.rb +43 -0
- data/lib/active_record/validations/uniqueness.rb +180 -0
- data/lib/active_record/version.rb +10 -0
- data/lib/rails/generators/active_record.rb +25 -0
- data/lib/rails/generators/active_record/migration.rb +15 -0
- data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
- data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
- data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
- data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
- data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
- data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
- data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
- data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
- metadata +242 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
|
2
|
+
require 'active_record/connection_adapters/statement_pool'
|
|
3
|
+
require 'active_support/core_ext/hash/keys'
|
|
4
|
+
|
|
5
|
+
gem 'mysql', '~> 2.8.1'
|
|
6
|
+
require 'mysql'
|
|
7
|
+
|
|
8
|
+
class Mysql
|
|
9
|
+
class Time
|
|
10
|
+
###
|
|
11
|
+
# This monkey patch is for test_additional_columns_from_join_table
|
|
12
|
+
def to_date
|
|
13
|
+
Date.new(year, month, day)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
class Stmt; include Enumerable end
|
|
17
|
+
class Result; include Enumerable end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module ActiveRecord
|
|
21
|
+
class Base
|
|
22
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
|
23
|
+
def self.mysql_connection(config) # :nodoc:
|
|
24
|
+
config = config.symbolize_keys
|
|
25
|
+
host = config[:host]
|
|
26
|
+
port = config[:port]
|
|
27
|
+
socket = config[:socket]
|
|
28
|
+
username = config[:username] ? config[:username].to_s : 'root'
|
|
29
|
+
password = config[:password].to_s
|
|
30
|
+
database = config[:database]
|
|
31
|
+
|
|
32
|
+
mysql = Mysql.init
|
|
33
|
+
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
|
|
34
|
+
|
|
35
|
+
default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
|
|
36
|
+
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
|
|
37
|
+
options = [host, username, password, database, port, socket, default_flags]
|
|
38
|
+
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ConnectionAdapters
|
|
43
|
+
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
|
|
44
|
+
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
|
|
45
|
+
#
|
|
46
|
+
# Options:
|
|
47
|
+
#
|
|
48
|
+
# * <tt>:host</tt> - Defaults to "localhost".
|
|
49
|
+
# * <tt>:port</tt> - Defaults to 3306.
|
|
50
|
+
# * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
|
|
51
|
+
# * <tt>:username</tt> - Defaults to "root"
|
|
52
|
+
# * <tt>:password</tt> - Defaults to nothing.
|
|
53
|
+
# * <tt>:database</tt> - The name of the database. No default, must be provided.
|
|
54
|
+
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
|
|
55
|
+
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
|
|
56
|
+
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
|
|
57
|
+
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
|
|
58
|
+
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
|
|
59
|
+
# * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
|
|
60
|
+
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
|
|
61
|
+
#
|
|
62
|
+
class MysqlAdapter < AbstractMysqlAdapter
|
|
63
|
+
|
|
64
|
+
class Column < AbstractMysqlAdapter::Column #:nodoc:
|
|
65
|
+
def self.string_to_time(value)
|
|
66
|
+
return super unless Mysql::Time === value
|
|
67
|
+
new_time(
|
|
68
|
+
value.year,
|
|
69
|
+
value.month,
|
|
70
|
+
value.day,
|
|
71
|
+
value.hour,
|
|
72
|
+
value.minute,
|
|
73
|
+
value.second,
|
|
74
|
+
value.second_part)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.string_to_dummy_time(v)
|
|
78
|
+
return super unless Mysql::Time === v
|
|
79
|
+
new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.string_to_date(v)
|
|
83
|
+
return super unless Mysql::Time === v
|
|
84
|
+
new_date(v.year, v.month, v.day)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def adapter
|
|
88
|
+
MysqlAdapter
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
ADAPTER_NAME = 'MySQL'
|
|
93
|
+
|
|
94
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
|
95
|
+
def initialize(connection, max = 1000)
|
|
96
|
+
super
|
|
97
|
+
@cache = Hash.new { |h,pid| h[pid] = {} }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def each(&block); cache.each(&block); end
|
|
101
|
+
def key?(key); cache.key?(key); end
|
|
102
|
+
def [](key); cache[key]; end
|
|
103
|
+
def length; cache.length; end
|
|
104
|
+
def delete(key); cache.delete(key); end
|
|
105
|
+
|
|
106
|
+
def []=(sql, key)
|
|
107
|
+
while @max <= cache.size
|
|
108
|
+
cache.shift.last[:stmt].close
|
|
109
|
+
end
|
|
110
|
+
cache[sql] = key
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def clear
|
|
114
|
+
cache.values.each do |hash|
|
|
115
|
+
hash[:stmt].close
|
|
116
|
+
end
|
|
117
|
+
cache.clear
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
def cache
|
|
122
|
+
@cache[$$]
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def initialize(connection, logger, connection_options, config)
|
|
127
|
+
super
|
|
128
|
+
@statements = StatementPool.new(@connection,
|
|
129
|
+
config.fetch(:statement_limit) { 1000 })
|
|
130
|
+
@client_encoding = nil
|
|
131
|
+
connect
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns true, since this connection adapter supports prepared statement
|
|
135
|
+
# caching.
|
|
136
|
+
def supports_statement_cache?
|
|
137
|
+
true
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# HELPER METHODS ===========================================
|
|
141
|
+
|
|
142
|
+
def each_hash(result) # :nodoc:
|
|
143
|
+
if block_given?
|
|
144
|
+
result.each_hash do |row|
|
|
145
|
+
row.symbolize_keys!
|
|
146
|
+
yield row
|
|
147
|
+
end
|
|
148
|
+
else
|
|
149
|
+
to_enum(:each_hash, result)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def new_column(field, default, type, null, collation) # :nodoc:
|
|
154
|
+
Column.new(field, default, type, null, collation)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def error_number(exception) # :nodoc:
|
|
158
|
+
exception.errno if exception.respond_to?(:errno)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# QUOTING ==================================================
|
|
162
|
+
|
|
163
|
+
def type_cast(value, column)
|
|
164
|
+
return super unless value == true || value == false
|
|
165
|
+
|
|
166
|
+
value ? 1 : 0
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def quote_string(string) #:nodoc:
|
|
170
|
+
@connection.quote(string)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# CONNECTION MANAGEMENT ====================================
|
|
174
|
+
|
|
175
|
+
def active?
|
|
176
|
+
if @connection.respond_to?(:stat)
|
|
177
|
+
@connection.stat
|
|
178
|
+
else
|
|
179
|
+
@connection.query 'select 1'
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# mysql-ruby doesn't raise an exception when stat fails.
|
|
183
|
+
if @connection.respond_to?(:errno)
|
|
184
|
+
@connection.errno.zero?
|
|
185
|
+
else
|
|
186
|
+
true
|
|
187
|
+
end
|
|
188
|
+
rescue Mysql::Error
|
|
189
|
+
false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def reconnect!
|
|
193
|
+
disconnect!
|
|
194
|
+
clear_cache!
|
|
195
|
+
connect
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Disconnects from the database if already connected. Otherwise, this
|
|
199
|
+
# method does nothing.
|
|
200
|
+
def disconnect!
|
|
201
|
+
@connection.close rescue nil
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def reset!
|
|
205
|
+
if @connection.respond_to?(:change_user)
|
|
206
|
+
# See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
|
|
207
|
+
# reset the connection is to change the user to the same user.
|
|
208
|
+
@connection.change_user(@config[:username], @config[:password], @config[:database])
|
|
209
|
+
configure_connection
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# DATABASE STATEMENTS ======================================
|
|
214
|
+
|
|
215
|
+
def select_rows(sql, name = nil)
|
|
216
|
+
@connection.query_with_result = true
|
|
217
|
+
rows = exec_without_stmt(sql, name).rows
|
|
218
|
+
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
|
|
219
|
+
rows
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Clears the prepared statements cache.
|
|
223
|
+
def clear_cache!
|
|
224
|
+
@statements.clear
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
if "<3".respond_to?(:encode)
|
|
228
|
+
# Taken from here:
|
|
229
|
+
# https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
|
|
230
|
+
# Author: TOMITA Masahiro <tommy@tmtm.org>
|
|
231
|
+
ENCODINGS = {
|
|
232
|
+
"armscii8" => nil,
|
|
233
|
+
"ascii" => Encoding::US_ASCII,
|
|
234
|
+
"big5" => Encoding::Big5,
|
|
235
|
+
"binary" => Encoding::ASCII_8BIT,
|
|
236
|
+
"cp1250" => Encoding::Windows_1250,
|
|
237
|
+
"cp1251" => Encoding::Windows_1251,
|
|
238
|
+
"cp1256" => Encoding::Windows_1256,
|
|
239
|
+
"cp1257" => Encoding::Windows_1257,
|
|
240
|
+
"cp850" => Encoding::CP850,
|
|
241
|
+
"cp852" => Encoding::CP852,
|
|
242
|
+
"cp866" => Encoding::IBM866,
|
|
243
|
+
"cp932" => Encoding::Windows_31J,
|
|
244
|
+
"dec8" => nil,
|
|
245
|
+
"eucjpms" => Encoding::EucJP_ms,
|
|
246
|
+
"euckr" => Encoding::EUC_KR,
|
|
247
|
+
"gb2312" => Encoding::EUC_CN,
|
|
248
|
+
"gbk" => Encoding::GBK,
|
|
249
|
+
"geostd8" => nil,
|
|
250
|
+
"greek" => Encoding::ISO_8859_7,
|
|
251
|
+
"hebrew" => Encoding::ISO_8859_8,
|
|
252
|
+
"hp8" => nil,
|
|
253
|
+
"keybcs2" => nil,
|
|
254
|
+
"koi8r" => Encoding::KOI8_R,
|
|
255
|
+
"koi8u" => Encoding::KOI8_U,
|
|
256
|
+
"latin1" => Encoding::ISO_8859_1,
|
|
257
|
+
"latin2" => Encoding::ISO_8859_2,
|
|
258
|
+
"latin5" => Encoding::ISO_8859_9,
|
|
259
|
+
"latin7" => Encoding::ISO_8859_13,
|
|
260
|
+
"macce" => Encoding::MacCentEuro,
|
|
261
|
+
"macroman" => Encoding::MacRoman,
|
|
262
|
+
"sjis" => Encoding::SHIFT_JIS,
|
|
263
|
+
"swe7" => nil,
|
|
264
|
+
"tis620" => Encoding::TIS_620,
|
|
265
|
+
"ucs2" => Encoding::UTF_16BE,
|
|
266
|
+
"ujis" => Encoding::EucJP_ms,
|
|
267
|
+
"utf8" => Encoding::UTF_8,
|
|
268
|
+
"utf8mb4" => Encoding::UTF_8,
|
|
269
|
+
}
|
|
270
|
+
else
|
|
271
|
+
ENCODINGS = Hash.new { |h,k| h[k] = k }
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Get the client encoding for this database
|
|
275
|
+
def client_encoding
|
|
276
|
+
return @client_encoding if @client_encoding
|
|
277
|
+
|
|
278
|
+
result = exec_query(
|
|
279
|
+
"SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
|
|
280
|
+
'SCHEMA')
|
|
281
|
+
@client_encoding = ENCODINGS[result.rows.last.last]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
|
285
|
+
log(sql, name, binds) do
|
|
286
|
+
exec_stmt(sql, name, binds) do |cols, stmt|
|
|
287
|
+
ActiveRecord::Result.new(cols, stmt.to_a) if cols
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def last_inserted_id(result)
|
|
293
|
+
@connection.insert_id
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
|
|
297
|
+
# Some queries, like SHOW CREATE TABLE don't work through the prepared
|
|
298
|
+
# statement API. For those queries, we need to use this method. :'(
|
|
299
|
+
log(sql, name) do
|
|
300
|
+
result = @connection.query(sql)
|
|
301
|
+
cols = []
|
|
302
|
+
rows = []
|
|
303
|
+
|
|
304
|
+
if result
|
|
305
|
+
cols = result.fetch_fields.map { |field| field.name }
|
|
306
|
+
rows = result.to_a
|
|
307
|
+
result.free
|
|
308
|
+
end
|
|
309
|
+
ActiveRecord::Result.new(cols, rows)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def execute_and_free(sql, name = nil)
|
|
314
|
+
result = execute(sql, name)
|
|
315
|
+
ret = yield result
|
|
316
|
+
result.free
|
|
317
|
+
ret
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
|
321
|
+
super sql, name
|
|
322
|
+
id_value || @connection.insert_id
|
|
323
|
+
end
|
|
324
|
+
alias :create :insert_sql
|
|
325
|
+
|
|
326
|
+
def exec_delete(sql, name, binds)
|
|
327
|
+
log(sql, name, binds) do
|
|
328
|
+
exec_stmt(sql, name, binds) do |cols, stmt|
|
|
329
|
+
stmt.affected_rows
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
alias :exec_update :exec_delete
|
|
334
|
+
|
|
335
|
+
def begin_db_transaction #:nodoc:
|
|
336
|
+
exec_without_stmt "BEGIN"
|
|
337
|
+
rescue Mysql::Error
|
|
338
|
+
# Transactions aren't supported
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def exec_stmt(sql, name, binds)
|
|
344
|
+
cache = {}
|
|
345
|
+
if binds.empty?
|
|
346
|
+
stmt = @connection.prepare(sql)
|
|
347
|
+
else
|
|
348
|
+
cache = @statements[sql] ||= {
|
|
349
|
+
:stmt => @connection.prepare(sql)
|
|
350
|
+
}
|
|
351
|
+
stmt = cache[:stmt]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
begin
|
|
355
|
+
stmt.execute(*binds.map { |col, val| type_cast(val, col) })
|
|
356
|
+
rescue Mysql::Error => e
|
|
357
|
+
# Older versions of MySQL leave the prepared statement in a bad
|
|
358
|
+
# place when an error occurs. To support older mysql versions, we
|
|
359
|
+
# need to close the statement and delete the statement from the
|
|
360
|
+
# cache.
|
|
361
|
+
stmt.close
|
|
362
|
+
@statements.delete sql
|
|
363
|
+
raise e
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
cols = nil
|
|
367
|
+
if metadata = stmt.result_metadata
|
|
368
|
+
cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
|
|
369
|
+
field.name
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
result = yield [cols, stmt]
|
|
374
|
+
|
|
375
|
+
stmt.result_metadata.free if cols
|
|
376
|
+
stmt.free_result
|
|
377
|
+
stmt.close if binds.empty?
|
|
378
|
+
|
|
379
|
+
result
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def connect
|
|
383
|
+
encoding = @config[:encoding]
|
|
384
|
+
if encoding
|
|
385
|
+
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if @config[:sslca] || @config[:sslkey]
|
|
389
|
+
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
@connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
|
|
393
|
+
@connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
|
|
394
|
+
@connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
|
|
395
|
+
|
|
396
|
+
@connection.real_connect(*@connection_options)
|
|
397
|
+
|
|
398
|
+
# reconnect must be set after real_connect is called, because real_connect sets it to false internally
|
|
399
|
+
@connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
|
|
400
|
+
|
|
401
|
+
configure_connection
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def configure_connection
|
|
405
|
+
encoding = @config[:encoding]
|
|
406
|
+
execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
|
|
407
|
+
|
|
408
|
+
# By default, MySQL 'where id is null' selects the last inserted id.
|
|
409
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
|
410
|
+
execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def select(sql, name = nil, binds = [])
|
|
414
|
+
@connection.query_with_result = true
|
|
415
|
+
rows = exec_query(sql, name, binds).to_a
|
|
416
|
+
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
|
|
417
|
+
rows
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Returns the version of the connected MySQL server.
|
|
421
|
+
def version
|
|
422
|
+
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
|
2
|
+
require 'active_support/core_ext/object/blank'
|
|
3
|
+
require 'active_record/connection_adapters/statement_pool'
|
|
4
|
+
|
|
5
|
+
# Make sure we're using pg high enough for PGResult#values
|
|
6
|
+
gem 'pg', '~> 0.11'
|
|
7
|
+
require 'pg'
|
|
8
|
+
|
|
9
|
+
module ActiveRecord
|
|
10
|
+
class Base
|
|
11
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
|
12
|
+
def self.postgresql_connection(config) # :nodoc:
|
|
13
|
+
config = config.symbolize_keys
|
|
14
|
+
host = config[:host]
|
|
15
|
+
port = config[:port] || 5432
|
|
16
|
+
username = config[:username].to_s if config[:username]
|
|
17
|
+
password = config[:password].to_s if config[:password]
|
|
18
|
+
|
|
19
|
+
if config.key?(:database)
|
|
20
|
+
database = config[:database]
|
|
21
|
+
else
|
|
22
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
|
26
|
+
# so just pass a nil connection object for the time being.
|
|
27
|
+
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module ConnectionAdapters
|
|
32
|
+
# PostgreSQL-specific extensions to column definitions in a table.
|
|
33
|
+
class PostgreSQLColumn < Column #:nodoc:
|
|
34
|
+
# Instantiates a new PostgreSQL column definition in a table.
|
|
35
|
+
def initialize(name, default, sql_type = nil, null = true)
|
|
36
|
+
super(name, self.class.extract_value_from_default(default), sql_type, null)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# :stopdoc:
|
|
40
|
+
class << self
|
|
41
|
+
attr_accessor :money_precision
|
|
42
|
+
def string_to_time(string)
|
|
43
|
+
return string unless String === string
|
|
44
|
+
|
|
45
|
+
case string
|
|
46
|
+
when 'infinity' then 1.0 / 0.0
|
|
47
|
+
when '-infinity' then -1.0 / 0.0
|
|
48
|
+
else
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
# :startdoc:
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
def extract_limit(sql_type)
|
|
57
|
+
case sql_type
|
|
58
|
+
when /^bigint/i; 8
|
|
59
|
+
when /^smallint/i; 2
|
|
60
|
+
else super
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Extracts the scale from PostgreSQL-specific data types.
|
|
65
|
+
def extract_scale(sql_type)
|
|
66
|
+
# Money type has a fixed scale of 2.
|
|
67
|
+
sql_type =~ /^money/ ? 2 : super
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Extracts the precision from PostgreSQL-specific data types.
|
|
71
|
+
def extract_precision(sql_type)
|
|
72
|
+
if sql_type == 'money'
|
|
73
|
+
self.class.money_precision
|
|
74
|
+
else
|
|
75
|
+
super
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Maps PostgreSQL-specific data types to logical Rails types.
|
|
80
|
+
def simplified_type(field_type)
|
|
81
|
+
case field_type
|
|
82
|
+
# Numeric and monetary types
|
|
83
|
+
when /^(?:real|double precision)$/
|
|
84
|
+
:float
|
|
85
|
+
# Monetary types
|
|
86
|
+
when 'money'
|
|
87
|
+
:decimal
|
|
88
|
+
# Character types
|
|
89
|
+
when /^(?:character varying|bpchar)(?:\(\d+\))?$/
|
|
90
|
+
:string
|
|
91
|
+
# Binary data types
|
|
92
|
+
when 'bytea'
|
|
93
|
+
:binary
|
|
94
|
+
# Date/time types
|
|
95
|
+
when /^timestamp with(?:out)? time zone$/
|
|
96
|
+
:datetime
|
|
97
|
+
when 'interval'
|
|
98
|
+
:string
|
|
99
|
+
# Geometric types
|
|
100
|
+
when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
|
|
101
|
+
:string
|
|
102
|
+
# Network address types
|
|
103
|
+
when /^(?:cidr|inet|macaddr)$/
|
|
104
|
+
:string
|
|
105
|
+
# Bit strings
|
|
106
|
+
when /^bit(?: varying)?(?:\(\d+\))?$/
|
|
107
|
+
:string
|
|
108
|
+
# XML type
|
|
109
|
+
when 'xml'
|
|
110
|
+
:xml
|
|
111
|
+
# tsvector type
|
|
112
|
+
when 'tsvector'
|
|
113
|
+
:tsvector
|
|
114
|
+
# Arrays
|
|
115
|
+
when /^\D+\[\]$/
|
|
116
|
+
:string
|
|
117
|
+
# Object identifier types
|
|
118
|
+
when 'oid'
|
|
119
|
+
:integer
|
|
120
|
+
# UUID type
|
|
121
|
+
when 'uuid'
|
|
122
|
+
:string
|
|
123
|
+
# Small and big integer types
|
|
124
|
+
when /^(?:small|big)int$/
|
|
125
|
+
:integer
|
|
126
|
+
# Pass through all types that are not specific to PostgreSQL.
|
|
127
|
+
else
|
|
128
|
+
super
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Extracts the value from a PostgreSQL column default definition.
|
|
133
|
+
def self.extract_value_from_default(default)
|
|
134
|
+
case default
|
|
135
|
+
# This is a performance optimization for Ruby 1.9.2 in development.
|
|
136
|
+
# If the value is nil, we return nil straight away without checking
|
|
137
|
+
# the regular expressions. If we check each regular expression,
|
|
138
|
+
# Regexp#=== will call NilClass#to_str, which will trigger
|
|
139
|
+
# method_missing (defined by whiny nil in ActiveSupport) which
|
|
140
|
+
# makes this method very very slow.
|
|
141
|
+
when NilClass
|
|
142
|
+
nil
|
|
143
|
+
# Numeric types
|
|
144
|
+
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
|
|
145
|
+
$1
|
|
146
|
+
# Character types
|
|
147
|
+
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
|
|
148
|
+
$1
|
|
149
|
+
# Character types (8.1 formatting)
|
|
150
|
+
when /\AE'(.*)'::(?:character varying|bpchar|text)\z/m
|
|
151
|
+
$1.gsub(/\\(\d\d\d)/) { $1.oct.chr }
|
|
152
|
+
# Binary data types
|
|
153
|
+
when /\A'(.*)'::bytea\z/m
|
|
154
|
+
$1
|
|
155
|
+
# Date/time types
|
|
156
|
+
when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
|
|
157
|
+
$1
|
|
158
|
+
when /\A'(.*)'::interval\z/
|
|
159
|
+
$1
|
|
160
|
+
# Boolean type
|
|
161
|
+
when 'true'
|
|
162
|
+
true
|
|
163
|
+
when 'false'
|
|
164
|
+
false
|
|
165
|
+
# Geometric types
|
|
166
|
+
when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
|
|
167
|
+
$1
|
|
168
|
+
# Network address types
|
|
169
|
+
when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
|
|
170
|
+
$1
|
|
171
|
+
# Bit string types
|
|
172
|
+
when /\AB'(.*)'::"?bit(?: varying)?"?\z/
|
|
173
|
+
$1
|
|
174
|
+
# XML type
|
|
175
|
+
when /\A'(.*)'::xml\z/m
|
|
176
|
+
$1
|
|
177
|
+
# Arrays
|
|
178
|
+
when /\A'(.*)'::"?\D+"?\[\]\z/
|
|
179
|
+
$1
|
|
180
|
+
# Object identifier types
|
|
181
|
+
when /\A-?\d+\z/
|
|
182
|
+
$1
|
|
183
|
+
else
|
|
184
|
+
# Anything else is blank, some user type, or some function
|
|
185
|
+
# and we can't know the value of that, so return nil.
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The PostgreSQL adapter works both with the native C (http://ruby.scripting.ca/postgres/) and the pure
|
|
192
|
+
# Ruby (available both as gem and from http://rubyforge.org/frs/?group_id=234&release_id=1944) drivers.
|
|
193
|
+
#
|
|
194
|
+
# Options:
|
|
195
|
+
#
|
|
196
|
+
# * <tt>:host</tt> - Defaults to "localhost".
|
|
197
|
+
# * <tt>:port</tt> - Defaults to 5432.
|
|
198
|
+
# * <tt>:username</tt> - Defaults to nothing.
|
|
199
|
+
# * <tt>:password</tt> - Defaults to nothing.
|
|
200
|
+
# * <tt>:database</tt> - The name of the database. No default, must be provided.
|
|
201
|
+
# * <tt>:schema_search_path</tt> - An optional schema search path for the connection given
|
|
202
|
+
# as a string of comma-separated schema names. This is backward-compatible with the <tt>:schema_order</tt> option.
|
|
203
|
+
# * <tt>:encoding</tt> - An optional client encoding that is used in a <tt>SET client_encoding TO
|
|
204
|
+
# <encoding></tt> call on the connection.
|
|
205
|
+
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
|
|
206
|
+
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
|
|
207
|
+
class PostgreSQLAdapter < AbstractAdapter
|
|
208
|
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
|
|
209
|
+
def xml(*args)
|
|
210
|
+
options = args.extract_options!
|
|
211
|
+
column(args[0], 'xml', options)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def tsvector(*args)
|
|
215
|
+
options = args.extract_options!
|
|
216
|
+
column(args[0], 'tsvector', options)
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
ADAPTER_NAME = 'PostgreSQL'
|
|
221
|
+
|
|
222
|
+
NATIVE_DATABASE_TYPES = {
|
|
223
|
+
:primary_key => "serial primary key",
|
|
224
|
+
:string => { :name => "character varying", :limit => 255 },
|
|
225
|
+
:text => { :name => "text" },
|
|
226
|
+
:integer => { :name => "integer" },
|
|
227
|
+
:float => { :name => "float" },
|
|
228
|
+
:decimal => { :name => "decimal" },
|
|
229
|
+
:datetime => { :name => "timestamp" },
|
|
230
|
+
:timestamp => { :name => "timestamp" },
|
|
231
|
+
:time => { :name => "time" },
|
|
232
|
+
:date => { :name => "date" },
|
|
233
|
+
:binary => { :name => "bytea" },
|
|
234
|
+
:boolean => { :name => "boolean" },
|
|
235
|
+
:xml => { :name => "xml" },
|
|
236
|
+
:tsvector => { :name => "tsvector" }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Returns 'PostgreSQL' as adapter name for identification purposes.
|
|
240
|
+
def adapter_name
|
|
241
|
+
ADAPTER_NAME
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns +true+, since this connection adapter supports prepared statement
|
|
245
|
+
# caching.
|
|
246
|
+
def supports_statement_cache?
|
|
247
|
+
true
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def supports_index_sort_order?
|
|
251
|
+
true
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class StatementPool < ConnectionAdapters::StatementPool
|
|
255
|
+
def initialize(connection, max)
|
|
256
|
+
super
|
|
257
|
+
@counter = 0
|
|
258
|
+
@cache = Hash.new { |h,pid| h[pid] = {} }
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def each(&block); cache.each(&block); end
|
|
262
|
+
def key?(key); cache.key?(key); end
|
|
263
|
+
def [](key); cache[key]; end
|
|
264
|
+
def length; cache.length; end
|
|
265
|
+
|
|
266
|
+
def next_key
|
|
267
|
+
"a#{@counter + 1}"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def []=(sql, key)
|
|
271
|
+
while @max <= cache.size
|
|
272
|
+
dealloc(cache.shift.last)
|
|
273
|
+
end
|
|
274
|
+
@counter += 1
|
|
275
|
+
cache[sql] = key
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def clear
|
|
279
|
+
cache.each_value do |stmt_key|
|
|
280
|
+
dealloc stmt_key
|
|
281
|
+
end
|
|
282
|
+
cache.clear
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def delete(sql_key)
|
|
286
|
+
dealloc cache[sql_key]
|
|
287
|
+
cache.delete sql_key
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
private
|
|
291
|
+
def cache
|
|
292
|
+
@cache[$$]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def dealloc(key)
|
|
296
|
+
@connection.query "DEALLOCATE #{key}" if connection_active?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def connection_active?
|
|
300
|
+
@connection.status == PGconn::CONNECTION_OK
|
|
301
|
+
rescue PGError
|
|
302
|
+
false
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Initializes and connects a PostgreSQL adapter.
|
|
307
|
+
def initialize(connection, logger, connection_parameters, config)
|
|
308
|
+
super(connection, logger)
|
|
309
|
+
@connection_parameters, @config = connection_parameters, config
|
|
310
|
+
@visitor = Arel::Visitors::PostgreSQL.new self
|
|
311
|
+
|
|
312
|
+
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
|
|
313
|
+
@local_tz = nil
|
|
314
|
+
@table_alias_length = nil
|
|
315
|
+
|
|
316
|
+
connect
|
|
317
|
+
@statements = StatementPool.new @connection,
|
|
318
|
+
config.fetch(:statement_limit) { 1000 }
|
|
319
|
+
|
|
320
|
+
if postgresql_version < 80200
|
|
321
|
+
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Clears the prepared statements cache.
|
|
328
|
+
def clear_cache!
|
|
329
|
+
@statements.clear
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Is this connection alive and ready for queries?
|
|
333
|
+
def active?
|
|
334
|
+
@connection.status == PGconn::CONNECTION_OK
|
|
335
|
+
rescue PGError
|
|
336
|
+
false
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Close then reopen the connection.
|
|
340
|
+
def reconnect!
|
|
341
|
+
clear_cache!
|
|
342
|
+
@connection.reset
|
|
343
|
+
configure_connection
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def reset!
|
|
347
|
+
clear_cache!
|
|
348
|
+
super
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Disconnects from the database if already connected. Otherwise, this
|
|
352
|
+
# method does nothing.
|
|
353
|
+
def disconnect!
|
|
354
|
+
clear_cache!
|
|
355
|
+
@connection.close rescue nil
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def native_database_types #:nodoc:
|
|
359
|
+
NATIVE_DATABASE_TYPES
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Returns true, since this connection adapter supports migrations.
|
|
363
|
+
def supports_migrations?
|
|
364
|
+
true
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Does PostgreSQL support finding primary key on non-Active Record tables?
|
|
368
|
+
def supports_primary_key? #:nodoc:
|
|
369
|
+
true
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Enable standard-conforming strings if available.
|
|
373
|
+
def set_standard_conforming_strings
|
|
374
|
+
old, self.client_min_messages = client_min_messages, 'panic'
|
|
375
|
+
execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
|
|
376
|
+
ensure
|
|
377
|
+
self.client_min_messages = old
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def supports_insert_with_returning?
|
|
381
|
+
true
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def supports_ddl_transactions?
|
|
385
|
+
true
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Returns true, since this connection adapter supports savepoints.
|
|
389
|
+
def supports_savepoints?
|
|
390
|
+
true
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Returns true.
|
|
394
|
+
def supports_explain?
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Returns the configured supported identifier length supported by PostgreSQL
|
|
399
|
+
def table_alias_length
|
|
400
|
+
@table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# QUOTING ==================================================
|
|
404
|
+
|
|
405
|
+
# Escapes binary strings for bytea input to the database.
|
|
406
|
+
def escape_bytea(value)
|
|
407
|
+
@connection.escape_bytea(value) if value
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Unescapes bytea output from a database to the binary string it represents.
|
|
411
|
+
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
|
|
412
|
+
# on escaped binary output from database drive.
|
|
413
|
+
def unescape_bytea(value)
|
|
414
|
+
@connection.unescape_bytea(value) if value
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Quotes PostgreSQL-specific data types for SQL input.
|
|
418
|
+
def quote(value, column = nil) #:nodoc:
|
|
419
|
+
return super unless column
|
|
420
|
+
|
|
421
|
+
case value
|
|
422
|
+
when Float
|
|
423
|
+
return super unless value.infinite? && column.type == :datetime
|
|
424
|
+
"'#{value.to_s.downcase}'"
|
|
425
|
+
when Numeric
|
|
426
|
+
return super unless column.sql_type == 'money'
|
|
427
|
+
# Not truly string input, so doesn't require (or allow) escape string syntax.
|
|
428
|
+
"'#{value}'"
|
|
429
|
+
when String
|
|
430
|
+
case column.sql_type
|
|
431
|
+
when 'bytea' then "'#{escape_bytea(value)}'"
|
|
432
|
+
when 'xml' then "xml '#{quote_string(value)}'"
|
|
433
|
+
when /^bit/
|
|
434
|
+
case value
|
|
435
|
+
when /^[01]*$/ then "B'#{value}'" # Bit-string notation
|
|
436
|
+
when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
|
|
437
|
+
end
|
|
438
|
+
else
|
|
439
|
+
super
|
|
440
|
+
end
|
|
441
|
+
else
|
|
442
|
+
super
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def type_cast(value, column)
|
|
447
|
+
return super unless column
|
|
448
|
+
|
|
449
|
+
case value
|
|
450
|
+
when String
|
|
451
|
+
return super unless 'bytea' == column.sql_type
|
|
452
|
+
{ :value => value, :format => 1 }
|
|
453
|
+
else
|
|
454
|
+
super
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Quotes strings for use in SQL input.
|
|
459
|
+
def quote_string(s) #:nodoc:
|
|
460
|
+
@connection.escape(s)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Checks the following cases:
|
|
464
|
+
#
|
|
465
|
+
# - table_name
|
|
466
|
+
# - "table.name"
|
|
467
|
+
# - schema_name.table_name
|
|
468
|
+
# - schema_name."table.name"
|
|
469
|
+
# - "schema.name".table_name
|
|
470
|
+
# - "schema.name"."table.name"
|
|
471
|
+
def quote_table_name(name)
|
|
472
|
+
schema, name_part = extract_pg_identifier_from_name(name.to_s)
|
|
473
|
+
|
|
474
|
+
unless name_part
|
|
475
|
+
quote_column_name(schema)
|
|
476
|
+
else
|
|
477
|
+
table_name, name_part = extract_pg_identifier_from_name(name_part)
|
|
478
|
+
"#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Quotes column names for use in SQL queries.
|
|
483
|
+
def quote_column_name(name) #:nodoc:
|
|
484
|
+
PGconn.quote_ident(name.to_s)
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Quote date/time values for use in SQL input. Includes microseconds
|
|
488
|
+
# if the value is a Time responding to usec.
|
|
489
|
+
def quoted_date(value) #:nodoc:
|
|
490
|
+
if value.acts_like?(:time) && value.respond_to?(:usec)
|
|
491
|
+
"#{super}.#{sprintf("%06d", value.usec)}"
|
|
492
|
+
else
|
|
493
|
+
super
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Set the authorized user for this session
|
|
498
|
+
def session_auth=(user)
|
|
499
|
+
clear_cache!
|
|
500
|
+
exec_query "SET SESSION AUTHORIZATION #{user}"
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# REFERENTIAL INTEGRITY ====================================
|
|
504
|
+
|
|
505
|
+
def supports_disable_referential_integrity? #:nodoc:
|
|
506
|
+
true
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def disable_referential_integrity #:nodoc:
|
|
510
|
+
if supports_disable_referential_integrity? then
|
|
511
|
+
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
|
|
512
|
+
end
|
|
513
|
+
yield
|
|
514
|
+
ensure
|
|
515
|
+
if supports_disable_referential_integrity? then
|
|
516
|
+
execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# DATABASE STATEMENTS ======================================
|
|
521
|
+
|
|
522
|
+
def explain(arel, binds = [])
|
|
523
|
+
sql = "EXPLAIN #{to_sql(arel)}"
|
|
524
|
+
ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
class ExplainPrettyPrinter # :nodoc:
|
|
528
|
+
# Pretty prints the result of a EXPLAIN in a way that resembles the output of the
|
|
529
|
+
# PostgreSQL shell:
|
|
530
|
+
#
|
|
531
|
+
# QUERY PLAN
|
|
532
|
+
# ------------------------------------------------------------------------------
|
|
533
|
+
# Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
|
|
534
|
+
# Join Filter: (posts.user_id = users.id)
|
|
535
|
+
# -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
|
|
536
|
+
# Index Cond: (id = 1)
|
|
537
|
+
# -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
|
|
538
|
+
# Filter: (posts.user_id = 1)
|
|
539
|
+
# (6 rows)
|
|
540
|
+
#
|
|
541
|
+
def pp(result)
|
|
542
|
+
header = result.columns.first
|
|
543
|
+
lines = result.rows.map(&:first)
|
|
544
|
+
|
|
545
|
+
# We add 2 because there's one char of padding at both sides, note
|
|
546
|
+
# the extra hyphens in the example above.
|
|
547
|
+
width = [header, *lines].map(&:length).max + 2
|
|
548
|
+
|
|
549
|
+
pp = []
|
|
550
|
+
|
|
551
|
+
pp << header.center(width).rstrip
|
|
552
|
+
pp << '-' * width
|
|
553
|
+
|
|
554
|
+
pp += lines.map {|line| " #{line}"}
|
|
555
|
+
|
|
556
|
+
nrows = result.rows.length
|
|
557
|
+
rows_label = nrows == 1 ? 'row' : 'rows'
|
|
558
|
+
pp << "(#{nrows} #{rows_label})"
|
|
559
|
+
|
|
560
|
+
pp.join("\n") + "\n"
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Executes a SELECT query and returns an array of rows. Each row is an
|
|
565
|
+
# array of field values.
|
|
566
|
+
def select_rows(sql, name = nil)
|
|
567
|
+
select_raw(sql, name).last
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Executes an INSERT query and returns the new record's ID
|
|
571
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
|
572
|
+
unless pk
|
|
573
|
+
# Extract the table from the insert sql. Yuck.
|
|
574
|
+
table_ref = extract_table_ref_from_insert_sql(sql)
|
|
575
|
+
pk = primary_key(table_ref) if table_ref
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
if pk
|
|
579
|
+
select_value("#{sql} RETURNING #{quote_column_name(pk)}")
|
|
580
|
+
else
|
|
581
|
+
super
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
alias :create :insert
|
|
585
|
+
|
|
586
|
+
# create a 2D array representing the result set
|
|
587
|
+
def result_as_array(res) #:nodoc:
|
|
588
|
+
# check if we have any binary column and if they need escaping
|
|
589
|
+
ftypes = Array.new(res.nfields) do |i|
|
|
590
|
+
[i, res.ftype(i)]
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
rows = res.values
|
|
594
|
+
return rows unless ftypes.any? { |_, x|
|
|
595
|
+
x == BYTEA_COLUMN_TYPE_OID || x == MONEY_COLUMN_TYPE_OID
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
typehash = ftypes.group_by { |_, type| type }
|
|
599
|
+
binaries = typehash[BYTEA_COLUMN_TYPE_OID] || []
|
|
600
|
+
monies = typehash[MONEY_COLUMN_TYPE_OID] || []
|
|
601
|
+
|
|
602
|
+
rows.each do |row|
|
|
603
|
+
# unescape string passed BYTEA field (OID == 17)
|
|
604
|
+
binaries.each do |index, _|
|
|
605
|
+
row[index] = unescape_bytea(row[index])
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
# If this is a money type column and there are any currency symbols,
|
|
609
|
+
# then strip them off. Indeed it would be prettier to do this in
|
|
610
|
+
# PostgreSQLColumn.string_to_decimal but would break form input
|
|
611
|
+
# fields that call value_before_type_cast.
|
|
612
|
+
monies.each do |index, _|
|
|
613
|
+
data = row[index]
|
|
614
|
+
# Because money output is formatted according to the locale, there are two
|
|
615
|
+
# cases to consider (note the decimal separators):
|
|
616
|
+
# (1) $12,345,678.12
|
|
617
|
+
# (2) $12.345.678,12
|
|
618
|
+
case data
|
|
619
|
+
when /^-?\D+[\d,]+\.\d{2}$/ # (1)
|
|
620
|
+
data.gsub!(/[^-\d.]/, '')
|
|
621
|
+
when /^-?\D+[\d.]+,\d{2}$/ # (2)
|
|
622
|
+
data.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
# Queries the database and returns the results in an Array-like object
|
|
630
|
+
def query(sql, name = nil) #:nodoc:
|
|
631
|
+
log(sql, name) do
|
|
632
|
+
result_as_array @connection.async_exec(sql)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Executes an SQL statement, returning a PGresult object on success
|
|
637
|
+
# or raising a PGError exception otherwise.
|
|
638
|
+
def execute(sql, name = nil)
|
|
639
|
+
log(sql, name) do
|
|
640
|
+
@connection.async_exec(sql)
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def substitute_at(column, index)
|
|
645
|
+
Arel.sql("$#{index + 1}")
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
|
649
|
+
log(sql, name, binds) do
|
|
650
|
+
result = binds.empty? ? exec_no_cache(sql, binds) :
|
|
651
|
+
exec_cache(sql, binds)
|
|
652
|
+
|
|
653
|
+
ret = ActiveRecord::Result.new(result.fields, result_as_array(result))
|
|
654
|
+
result.clear
|
|
655
|
+
return ret
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def exec_delete(sql, name = 'SQL', binds = [])
|
|
660
|
+
log(sql, name, binds) do
|
|
661
|
+
result = binds.empty? ? exec_no_cache(sql, binds) :
|
|
662
|
+
exec_cache(sql, binds)
|
|
663
|
+
affected = result.cmd_tuples
|
|
664
|
+
result.clear
|
|
665
|
+
affected
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
alias :exec_update :exec_delete
|
|
669
|
+
|
|
670
|
+
def sql_for_insert(sql, pk, id_value, sequence_name, binds)
|
|
671
|
+
unless pk
|
|
672
|
+
# Extract the table from the insert sql. Yuck.
|
|
673
|
+
table_ref = extract_table_ref_from_insert_sql(sql)
|
|
674
|
+
pk = primary_key(table_ref) if table_ref
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk
|
|
678
|
+
|
|
679
|
+
[sql, binds]
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Executes an UPDATE query and returns the number of affected tuples.
|
|
683
|
+
def update_sql(sql, name = nil)
|
|
684
|
+
super.cmd_tuples
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Begins a transaction.
|
|
688
|
+
def begin_db_transaction
|
|
689
|
+
execute "BEGIN"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# Commits a transaction.
|
|
693
|
+
def commit_db_transaction
|
|
694
|
+
execute "COMMIT"
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# Aborts a transaction.
|
|
698
|
+
def rollback_db_transaction
|
|
699
|
+
execute "ROLLBACK"
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def outside_transaction?
|
|
703
|
+
@connection.transaction_status == PGconn::PQTRANS_IDLE
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def create_savepoint
|
|
707
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def rollback_to_savepoint
|
|
711
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def release_savepoint
|
|
715
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# SCHEMA STATEMENTS ========================================
|
|
719
|
+
|
|
720
|
+
# Drops the database specified on the +name+ attribute
|
|
721
|
+
# and creates it again using the provided +options+.
|
|
722
|
+
def recreate_database(name, options = {}) #:nodoc:
|
|
723
|
+
drop_database(name)
|
|
724
|
+
create_database(name, options)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
|
|
728
|
+
# <tt>:encoding</tt>, <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
|
|
729
|
+
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
|
|
730
|
+
#
|
|
731
|
+
# Example:
|
|
732
|
+
# create_database config[:database], config
|
|
733
|
+
# create_database 'foo_development', :encoding => 'unicode'
|
|
734
|
+
def create_database(name, options = {})
|
|
735
|
+
options = options.reverse_merge(:encoding => "utf8")
|
|
736
|
+
|
|
737
|
+
option_string = options.symbolize_keys.sum do |key, value|
|
|
738
|
+
case key
|
|
739
|
+
when :owner
|
|
740
|
+
" OWNER = \"#{value}\""
|
|
741
|
+
when :template
|
|
742
|
+
" TEMPLATE = \"#{value}\""
|
|
743
|
+
when :encoding
|
|
744
|
+
" ENCODING = '#{value}'"
|
|
745
|
+
when :tablespace
|
|
746
|
+
" TABLESPACE = \"#{value}\""
|
|
747
|
+
when :connection_limit
|
|
748
|
+
" CONNECTION LIMIT = #{value}"
|
|
749
|
+
else
|
|
750
|
+
""
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
|
|
755
|
+
end
|
|
756
|
+
|
|
757
|
+
# Drops a PostgreSQL database.
|
|
758
|
+
#
|
|
759
|
+
# Example:
|
|
760
|
+
# drop_database 'matt_development'
|
|
761
|
+
def drop_database(name) #:nodoc:
|
|
762
|
+
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
# Returns the list of all tables in the schema search path or a specified schema.
|
|
766
|
+
def tables(name = nil)
|
|
767
|
+
query(<<-SQL, 'SCHEMA').map { |row| row[0] }
|
|
768
|
+
SELECT tablename
|
|
769
|
+
FROM pg_tables
|
|
770
|
+
WHERE schemaname = ANY (current_schemas(false))
|
|
771
|
+
SQL
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
# Returns true if table exists.
|
|
775
|
+
# If the schema is not specified as part of +name+ then it will only find tables within
|
|
776
|
+
# the current schema search path (regardless of permissions to access tables in other schemas)
|
|
777
|
+
def table_exists?(name)
|
|
778
|
+
schema, table = Utils.extract_schema_and_table(name.to_s)
|
|
779
|
+
return false unless table
|
|
780
|
+
|
|
781
|
+
binds = [[nil, table]]
|
|
782
|
+
binds << [nil, schema] if schema
|
|
783
|
+
|
|
784
|
+
exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0
|
|
785
|
+
SELECT COUNT(*)
|
|
786
|
+
FROM pg_class c
|
|
787
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
788
|
+
WHERE c.relkind in ('v','r')
|
|
789
|
+
AND c.relname = $1
|
|
790
|
+
AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'}
|
|
791
|
+
SQL
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Returns true if schema exists.
|
|
795
|
+
def schema_exists?(name)
|
|
796
|
+
exec_query(<<-SQL, 'SCHEMA', [[nil, name]]).rows.first[0].to_i > 0
|
|
797
|
+
SELECT COUNT(*)
|
|
798
|
+
FROM pg_namespace
|
|
799
|
+
WHERE nspname = $1
|
|
800
|
+
SQL
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
# Returns an array of indexes for the given table.
|
|
804
|
+
def indexes(table_name, name = nil)
|
|
805
|
+
result = query(<<-SQL, name)
|
|
806
|
+
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
|
|
807
|
+
FROM pg_class t
|
|
808
|
+
INNER JOIN pg_index d ON t.oid = d.indrelid
|
|
809
|
+
INNER JOIN pg_class i ON d.indexrelid = i.oid
|
|
810
|
+
WHERE i.relkind = 'i'
|
|
811
|
+
AND d.indisprimary = 'f'
|
|
812
|
+
AND t.relname = '#{table_name}'
|
|
813
|
+
AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
|
|
814
|
+
ORDER BY i.relname
|
|
815
|
+
SQL
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
result.map do |row|
|
|
819
|
+
index_name = row[0]
|
|
820
|
+
unique = row[1] == 't'
|
|
821
|
+
indkey = row[2].split(" ")
|
|
822
|
+
inddef = row[3]
|
|
823
|
+
oid = row[4]
|
|
824
|
+
|
|
825
|
+
columns = Hash[query(<<-SQL, "Columns for index #{row[0]} on #{table_name}")]
|
|
826
|
+
SELECT a.attnum, a.attname
|
|
827
|
+
FROM pg_attribute a
|
|
828
|
+
WHERE a.attrelid = #{oid}
|
|
829
|
+
AND a.attnum IN (#{indkey.join(",")})
|
|
830
|
+
SQL
|
|
831
|
+
|
|
832
|
+
column_names = columns.values_at(*indkey).compact
|
|
833
|
+
|
|
834
|
+
# add info on sort order for columns (only desc order is explicitly specified, asc is the default)
|
|
835
|
+
desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
|
|
836
|
+
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
|
|
837
|
+
|
|
838
|
+
column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders)
|
|
839
|
+
end.compact
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
# Returns the list of all column definitions for a table.
|
|
843
|
+
def columns(table_name, name = nil)
|
|
844
|
+
# Limit, precision, and scale are all handled by the superclass.
|
|
845
|
+
column_definitions(table_name).collect do |column_name, type, default, notnull|
|
|
846
|
+
PostgreSQLColumn.new(column_name, default, type, notnull == 'f')
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# Returns the current database name.
|
|
851
|
+
def current_database
|
|
852
|
+
query('select current_database()')[0][0]
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
# Returns the current schema name.
|
|
856
|
+
def current_schema
|
|
857
|
+
query('SELECT current_schema', 'SCHEMA')[0][0]
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Returns the current database encoding format.
|
|
861
|
+
def encoding
|
|
862
|
+
query(<<-end_sql)[0][0]
|
|
863
|
+
SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database
|
|
864
|
+
WHERE pg_database.datname LIKE '#{current_database}'
|
|
865
|
+
end_sql
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
# Sets the schema search path to a string of comma-separated schema names.
|
|
869
|
+
# Names beginning with $ have to be quoted (e.g. $user => '$user').
|
|
870
|
+
# See: http://www.postgresql.org/docs/current/static/ddl-schemas.html
|
|
871
|
+
#
|
|
872
|
+
# This should be not be called manually but set in database.yml.
|
|
873
|
+
def schema_search_path=(schema_csv)
|
|
874
|
+
if schema_csv
|
|
875
|
+
execute "SET search_path TO #{schema_csv}"
|
|
876
|
+
@schema_search_path = schema_csv
|
|
877
|
+
end
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# Returns the active schema search path.
|
|
881
|
+
def schema_search_path
|
|
882
|
+
@schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0]
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
# Returns the current client message level.
|
|
886
|
+
def client_min_messages
|
|
887
|
+
query('SHOW client_min_messages', 'SCHEMA')[0][0]
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Set the client message level.
|
|
891
|
+
def client_min_messages=(level)
|
|
892
|
+
execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# Returns the sequence name for a table's primary key or some other specified key.
|
|
896
|
+
def default_sequence_name(table_name, pk = nil) #:nodoc:
|
|
897
|
+
serial_sequence(table_name, pk || 'id').split('.').last
|
|
898
|
+
rescue ActiveRecord::StatementInvalid
|
|
899
|
+
"#{table_name}_#{pk || 'id'}_seq"
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def serial_sequence(table, column)
|
|
903
|
+
result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]])
|
|
904
|
+
SELECT pg_get_serial_sequence($1, $2)
|
|
905
|
+
eosql
|
|
906
|
+
result.rows.first.first
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# Resets the sequence of a table's primary key to the maximum value.
|
|
910
|
+
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
|
|
911
|
+
unless pk and sequence
|
|
912
|
+
default_pk, default_sequence = pk_and_sequence_for(table)
|
|
913
|
+
|
|
914
|
+
pk ||= default_pk
|
|
915
|
+
sequence ||= default_sequence
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
if @logger && pk && !sequence
|
|
919
|
+
@logger.warn "#{table} has primary key #{pk} with no default sequence"
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
if pk && sequence
|
|
923
|
+
quoted_sequence = quote_table_name(sequence)
|
|
924
|
+
|
|
925
|
+
select_value <<-end_sql, 'Reset sequence'
|
|
926
|
+
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
|
|
927
|
+
end_sql
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
# Returns a table's primary key and belonging sequence.
|
|
932
|
+
def pk_and_sequence_for(table) #:nodoc:
|
|
933
|
+
# First try looking for a sequence with a dependency on the
|
|
934
|
+
# given table's primary key.
|
|
935
|
+
result = exec_query(<<-end_sql, 'SCHEMA').rows.first
|
|
936
|
+
SELECT attr.attname, ns.nspname, seq.relname
|
|
937
|
+
FROM pg_class seq
|
|
938
|
+
INNER JOIN pg_depend dep ON seq.oid = dep.objid
|
|
939
|
+
INNER JOIN pg_attribute attr ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
|
|
940
|
+
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
|
|
941
|
+
INNER JOIN pg_namespace ns ON seq.relnamespace = ns.oid
|
|
942
|
+
WHERE seq.relkind = 'S'
|
|
943
|
+
AND cons.contype = 'p'
|
|
944
|
+
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
|
|
945
|
+
end_sql
|
|
946
|
+
|
|
947
|
+
# [primary_key, sequence]
|
|
948
|
+
if result.second == 'public' then
|
|
949
|
+
sequence = result.last
|
|
950
|
+
else
|
|
951
|
+
sequence = result.second+'.'+result.last
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
[result.first, sequence]
|
|
955
|
+
rescue
|
|
956
|
+
nil
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
# Returns just a table's primary key
|
|
960
|
+
def primary_key(table)
|
|
961
|
+
row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first
|
|
962
|
+
SELECT DISTINCT(attr.attname)
|
|
963
|
+
FROM pg_attribute attr
|
|
964
|
+
INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
|
|
965
|
+
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
|
|
966
|
+
WHERE cons.contype = 'p'
|
|
967
|
+
AND dep.refobjid = $1::regclass
|
|
968
|
+
end_sql
|
|
969
|
+
|
|
970
|
+
row && row.first
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
# Renames a table.
|
|
974
|
+
#
|
|
975
|
+
# Example:
|
|
976
|
+
# rename_table('octopuses', 'octopi')
|
|
977
|
+
def rename_table(name, new_name)
|
|
978
|
+
clear_cache!
|
|
979
|
+
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
# Adds a new column to the named table.
|
|
983
|
+
# See TableDefinition#column for details of the options you can use.
|
|
984
|
+
def add_column(table_name, column_name, type, options = {})
|
|
985
|
+
clear_cache!
|
|
986
|
+
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
|
987
|
+
add_column_options!(add_column_sql, options)
|
|
988
|
+
|
|
989
|
+
execute add_column_sql
|
|
990
|
+
end
|
|
991
|
+
|
|
992
|
+
# Changes the column of a table.
|
|
993
|
+
def change_column(table_name, column_name, type, options = {})
|
|
994
|
+
clear_cache!
|
|
995
|
+
quoted_table_name = quote_table_name(table_name)
|
|
996
|
+
|
|
997
|
+
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
|
998
|
+
|
|
999
|
+
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
|
|
1000
|
+
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
# Changes the default value of a table column.
|
|
1004
|
+
def change_column_default(table_name, column_name, default)
|
|
1005
|
+
clear_cache!
|
|
1006
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
|
1010
|
+
clear_cache!
|
|
1011
|
+
unless null || default.nil?
|
|
1012
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
|
1013
|
+
end
|
|
1014
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
# Renames a column in a table.
|
|
1018
|
+
def rename_column(table_name, column_name, new_column_name)
|
|
1019
|
+
clear_cache!
|
|
1020
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
|
|
1021
|
+
end
|
|
1022
|
+
|
|
1023
|
+
def remove_index!(table_name, index_name) #:nodoc:
|
|
1024
|
+
execute "DROP INDEX #{quote_table_name(index_name)}"
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
def rename_index(table_name, old_name, new_name)
|
|
1028
|
+
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
def index_name_length
|
|
1032
|
+
63
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
# Maps logical Rails types to PostgreSQL-specific data types.
|
|
1036
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
|
1037
|
+
return super unless type.to_s == 'integer'
|
|
1038
|
+
return 'integer' unless limit
|
|
1039
|
+
|
|
1040
|
+
case limit
|
|
1041
|
+
when 1, 2; 'smallint'
|
|
1042
|
+
when 3, 4; 'integer'
|
|
1043
|
+
when 5..8; 'bigint'
|
|
1044
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
|
1045
|
+
end
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
|
1049
|
+
#
|
|
1050
|
+
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
|
|
1051
|
+
# requires that the ORDER BY include the distinct column.
|
|
1052
|
+
#
|
|
1053
|
+
# distinct("posts.id", "posts.created_at desc")
|
|
1054
|
+
def distinct(columns, orders) #:nodoc:
|
|
1055
|
+
return "DISTINCT #{columns}" if orders.empty?
|
|
1056
|
+
|
|
1057
|
+
# Construct a clean list of column names from the ORDER BY clause, removing
|
|
1058
|
+
# any ASC/DESC modifiers
|
|
1059
|
+
order_columns = orders.collect { |s| s.gsub(/\s+(ASC|DESC)\s*/i, '') }
|
|
1060
|
+
order_columns.delete_if { |c| c.blank? }
|
|
1061
|
+
order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
|
|
1062
|
+
|
|
1063
|
+
"DISTINCT #{columns}, #{order_columns * ', '}"
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
module Utils
|
|
1067
|
+
extend self
|
|
1068
|
+
|
|
1069
|
+
# Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
|
|
1070
|
+
# +schema_name+ is nil if not specified in +name+.
|
|
1071
|
+
# +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
|
|
1072
|
+
# +name+ supports the range of schema/table references understood by PostgreSQL, for example:
|
|
1073
|
+
#
|
|
1074
|
+
# * <tt>table_name</tt>
|
|
1075
|
+
# * <tt>"table.name"</tt>
|
|
1076
|
+
# * <tt>schema_name.table_name</tt>
|
|
1077
|
+
# * <tt>schema_name."table.name"</tt>
|
|
1078
|
+
# * <tt>"schema.name"."table name"</tt>
|
|
1079
|
+
def extract_schema_and_table(name)
|
|
1080
|
+
table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
|
|
1081
|
+
[schema, table]
|
|
1082
|
+
end
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
protected
|
|
1086
|
+
# Returns the version of the connected PostgreSQL server.
|
|
1087
|
+
def postgresql_version
|
|
1088
|
+
@connection.server_version
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def translate_exception(exception, message)
|
|
1092
|
+
case exception.message
|
|
1093
|
+
when /duplicate key value violates unique constraint/
|
|
1094
|
+
RecordNotUnique.new(message, exception)
|
|
1095
|
+
when /violates foreign key constraint/
|
|
1096
|
+
InvalidForeignKey.new(message, exception)
|
|
1097
|
+
else
|
|
1098
|
+
super
|
|
1099
|
+
end
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
private
|
|
1103
|
+
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
|
|
1104
|
+
|
|
1105
|
+
def exec_no_cache(sql, binds)
|
|
1106
|
+
@connection.async_exec(sql)
|
|
1107
|
+
end
|
|
1108
|
+
|
|
1109
|
+
def exec_cache(sql, binds)
|
|
1110
|
+
begin
|
|
1111
|
+
stmt_key = prepare_statement sql
|
|
1112
|
+
|
|
1113
|
+
# Clear the queue
|
|
1114
|
+
@connection.get_last_result
|
|
1115
|
+
@connection.send_query_prepared(stmt_key, binds.map { |col, val|
|
|
1116
|
+
type_cast(val, col)
|
|
1117
|
+
})
|
|
1118
|
+
@connection.block
|
|
1119
|
+
@connection.get_last_result
|
|
1120
|
+
rescue PGError => e
|
|
1121
|
+
# Get the PG code for the failure. Annoyingly, the code for
|
|
1122
|
+
# prepared statements whose return value may have changed is
|
|
1123
|
+
# FEATURE_NOT_SUPPORTED. Check here for more details:
|
|
1124
|
+
# http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
|
|
1125
|
+
code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
|
|
1126
|
+
if FEATURE_NOT_SUPPORTED == code
|
|
1127
|
+
@statements.delete sql_key(sql)
|
|
1128
|
+
retry
|
|
1129
|
+
else
|
|
1130
|
+
raise e
|
|
1131
|
+
end
|
|
1132
|
+
end
|
|
1133
|
+
end
|
|
1134
|
+
|
|
1135
|
+
# Returns the statement identifier for the client side cache
|
|
1136
|
+
# of statements
|
|
1137
|
+
def sql_key(sql)
|
|
1138
|
+
"#{schema_search_path}-#{sql}"
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
# Prepare the statement if it hasn't been prepared, return
|
|
1142
|
+
# the statement key.
|
|
1143
|
+
def prepare_statement(sql)
|
|
1144
|
+
sql_key = sql_key(sql)
|
|
1145
|
+
unless @statements.key? sql_key
|
|
1146
|
+
nextkey = @statements.next_key
|
|
1147
|
+
@connection.prepare nextkey, sql
|
|
1148
|
+
@statements[sql_key] = nextkey
|
|
1149
|
+
end
|
|
1150
|
+
@statements[sql_key]
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
# The internal PostgreSQL identifier of the money data type.
|
|
1154
|
+
MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
|
|
1155
|
+
# The internal PostgreSQL identifier of the BYTEA data type.
|
|
1156
|
+
BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
|
|
1157
|
+
|
|
1158
|
+
# Connects to a PostgreSQL server and sets up the adapter depending on the
|
|
1159
|
+
# connected server's characteristics.
|
|
1160
|
+
def connect
|
|
1161
|
+
@connection = PGconn.connect(*@connection_parameters)
|
|
1162
|
+
|
|
1163
|
+
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
|
|
1164
|
+
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
|
|
1165
|
+
# should know about this but can't detect it there, so deal with it here.
|
|
1166
|
+
PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
|
|
1167
|
+
|
|
1168
|
+
configure_connection
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
|
|
1172
|
+
# This is called by #connect and should not be called manually.
|
|
1173
|
+
def configure_connection
|
|
1174
|
+
if @config[:encoding]
|
|
1175
|
+
@connection.set_client_encoding(@config[:encoding])
|
|
1176
|
+
end
|
|
1177
|
+
self.client_min_messages = @config[:min_messages] if @config[:min_messages]
|
|
1178
|
+
self.schema_search_path = @config[:schema_search_path] || @config[:schema_order]
|
|
1179
|
+
|
|
1180
|
+
# Use standard-conforming strings if available so we don't have to do the E'...' dance.
|
|
1181
|
+
set_standard_conforming_strings
|
|
1182
|
+
|
|
1183
|
+
# If using Active Record's time zone support configure the connection to return
|
|
1184
|
+
# TIMESTAMP WITH ZONE types in UTC.
|
|
1185
|
+
if ActiveRecord::Base.default_timezone == :utc
|
|
1186
|
+
execute("SET time zone 'UTC'", 'SCHEMA')
|
|
1187
|
+
elsif @local_tz
|
|
1188
|
+
execute("SET time zone '#{@local_tz}'", 'SCHEMA')
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
# Returns the current ID of a table's sequence.
|
|
1193
|
+
def last_insert_id(sequence_name) #:nodoc:
|
|
1194
|
+
r = exec_query("SELECT currval($1)", 'SQL', [[nil, sequence_name]])
|
|
1195
|
+
Integer(r.rows.first.first)
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# Executes a SELECT query and returns the results, performing any data type
|
|
1199
|
+
# conversions that are required to be performed here instead of in PostgreSQLColumn.
|
|
1200
|
+
def select(sql, name = nil, binds = [])
|
|
1201
|
+
exec_query(sql, name, binds).to_a
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
def select_raw(sql, name = nil)
|
|
1205
|
+
res = execute(sql, name)
|
|
1206
|
+
results = result_as_array(res)
|
|
1207
|
+
fields = res.fields
|
|
1208
|
+
res.clear
|
|
1209
|
+
return fields, results
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
# Returns the list of a table's column names, data types, and default values.
|
|
1213
|
+
#
|
|
1214
|
+
# The underlying query is roughly:
|
|
1215
|
+
# SELECT column.name, column.type, default.value
|
|
1216
|
+
# FROM column LEFT JOIN default
|
|
1217
|
+
# ON column.table_id = default.table_id
|
|
1218
|
+
# AND column.num = default.column_num
|
|
1219
|
+
# WHERE column.table_id = get_table_id('table_name')
|
|
1220
|
+
# AND column.num > 0
|
|
1221
|
+
# AND NOT column.is_dropped
|
|
1222
|
+
# ORDER BY column.num
|
|
1223
|
+
#
|
|
1224
|
+
# If the table name is not prefixed with a schema, the database will
|
|
1225
|
+
# take the first match from the schema search path.
|
|
1226
|
+
#
|
|
1227
|
+
# Query implementation notes:
|
|
1228
|
+
# - format_type includes the column size constraint, e.g. varchar(50)
|
|
1229
|
+
# - ::regclass is a function that gives the id for a table name
|
|
1230
|
+
def column_definitions(table_name) #:nodoc:
|
|
1231
|
+
exec_query(<<-end_sql, 'SCHEMA').rows
|
|
1232
|
+
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
|
|
1233
|
+
FROM pg_attribute a LEFT JOIN pg_attrdef d
|
|
1234
|
+
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
|
|
1235
|
+
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
|
|
1236
|
+
AND a.attnum > 0 AND NOT a.attisdropped
|
|
1237
|
+
ORDER BY a.attnum
|
|
1238
|
+
end_sql
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
def extract_pg_identifier_from_name(name)
|
|
1242
|
+
match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
|
|
1243
|
+
|
|
1244
|
+
if match_data
|
|
1245
|
+
rest = name[match_data[0].length, name.length]
|
|
1246
|
+
rest = rest[1, rest.length] if rest.start_with? "."
|
|
1247
|
+
[match_data[1], (rest.length > 0 ? rest : nil)]
|
|
1248
|
+
end
|
|
1249
|
+
end
|
|
1250
|
+
|
|
1251
|
+
def extract_table_ref_from_insert_sql(sql)
|
|
1252
|
+
sql[/into\s+([^\(]*).*values\s*\(/i]
|
|
1253
|
+
$1.strip if $1
|
|
1254
|
+
end
|
|
1255
|
+
|
|
1256
|
+
def table_definition
|
|
1257
|
+
TableDefinition.new(self)
|
|
1258
|
+
end
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1261
|
+
end
|